diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 640b4d3..0000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,43 +0,0 @@ -extends: - - eslint:recommended - - plugin:react/recommended - - plugin:@typescript-eslint/recommended -parser: '@typescript-eslint/parser' -plugins: - - react - - react-hooks - - '@typescript-eslint' -parserOptions: - sourceType: module - ecmaVersion: 2020 - ecmaFeatures: - jsx: true -env: - es6: true - browser: true - node: true - jest: true - -settings: - react: - version: detect -ignorePatterns: - - node_modules -rules: - react/prop-types: 0 - react-hooks/rules-of-hooks: "error" - # TODO: 修改添加deps后出现的死循环 - react-hooks/exhaustive-deps: 0 - '@typescript-eslint/explicit-function-return-type': 0 - '@typescript-eslint/no-explicit-any': 0 - '@typescript-eslint/camelcase': 0 - '@typescript-eslint/no-non-null-assertion': 0 - '@typescript-eslint/no-unused-vars': 0 - -overrides: - - files: ['*.js', '*.jsx'] - rules: - '@typescript-eslint/camelcase': 0 - - files: ['config/*.js', 'scripts/*.js'] - rules: - '@typescript-eslint/no-var-requires': 0 diff --git a/.gitignore b/.gitignore index 911e1ee..b24fe4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,27 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - +# Logs +logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -dist/ \ No newline at end of file +node_modules +dist +# same build output folder as fe v3 +build +dist-ssr +dev-dist +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.huskyrc b/.huskyrc deleted file mode 100644 index ecd1535..0000000 --- a/.huskyrc +++ /dev/null @@ -1 +0,0 @@ -export PATH="/usr/local/bin:$PATH" diff --git a/.prettierrc b/.prettierrc index 5fcd8a7..963354f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "tabWidth": 4 + "printWidth": 120 } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1519c10..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -node_js: - - 12.16.3 -before_script: - - yarn install -script: - - CI=false yarn run build - - yarn run test \ No newline at end of file diff --git a/README.md b/README.md index 89b278a..0d6babe 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,30 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +# React + TypeScript + Vite -## Available Scripts +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -In the project directory, you can run: +Currently, two official plugins are available: -### `yarn start` +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +## Expanding the ESLint configuration -The page will reload if you make edits.
-You will also see any lint errors in the console. +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: -### `yarn test` +- Configure the top-level `parserOptions` property like this: -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` -### `yarn build` - -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `yarn build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/config/env.js b/config/env.js deleted file mode 100644 index 211711b..0000000 --- a/config/env.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const paths = require('./paths'); - -// Make sure that including paths.js after env.js will read .env variables. -delete require.cache[require.resolve('./paths')]; - -const NODE_ENV = process.env.NODE_ENV; -if (!NODE_ENV) { - throw new Error( - 'The NODE_ENV environment variable is required but was not specified.' - ); -} - -// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -const dotenvFiles = [ - `${paths.dotenv}.${NODE_ENV}.local`, - `${paths.dotenv}.${NODE_ENV}`, - // Don't include `.env.local` for `test` environment - // since normally you expect tests to produce the same - // results for everyone - NODE_ENV !== 'test' && `${paths.dotenv}.local`, - paths.dotenv, -].filter(Boolean); - -// Load environment variables from .env* files. Suppress warnings using silent -// if this file is missing. dotenv will never modify any environment variables -// that have already been set. Variable expansion is supported in .env files. -// https://github.com/motdotla/dotenv -// https://github.com/motdotla/dotenv-expand -dotenvFiles.forEach(dotenvFile => { - if (fs.existsSync(dotenvFile)) { - require('dotenv-expand')( - require('dotenv').config({ - path: dotenvFile, - }) - ); - } -}); - -// We support resolving modules according to `NODE_PATH`. -// This lets you use absolute paths in imports inside large monorepos: -// https://github.com/facebook/create-react-app/issues/253. -// It works similar to `NODE_PATH` in Node itself: -// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders -// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. -// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. -// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 -// We also resolve them to make sure all tools using them work consistently. -const appDirectory = fs.realpathSync(process.cwd()); -process.env.NODE_PATH = (process.env.NODE_PATH || '') - .split(path.delimiter) - .filter(folder => folder && !path.isAbsolute(folder)) - .map(folder => path.resolve(appDirectory, folder)) - .join(path.delimiter); - -// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be -// injected into the application via DefinePlugin in Webpack configuration. -const REACT_APP = /^REACT_APP_/i; - -function getClientEnvironment(publicUrl) { - const raw = Object.keys(process.env) - .filter(key => REACT_APP.test(key)) - .reduce( - (env, key) => { - env[key] = process.env[key]; - return env; - }, - { - // Useful for determining whether we’re running in production mode. - // Most importantly, it switches React into the correct mode. - NODE_ENV: process.env.NODE_ENV || 'development', - // Useful for resolving the correct path to static assets in `public`. - // For example, . - // This should only be used as an escape hatch. Normally you would put - // images into the `src` and `import` them in code to get their paths. - PUBLIC_URL: publicUrl, - } - ); - // Stringify all values so we can feed into Webpack DefinePlugin - const stringified = { - 'process.env': Object.keys(raw).reduce((env, key) => { - env[key] = JSON.stringify(raw[key]); - return env; - }, {}), - }; - - return { raw, stringified }; -} - -module.exports = getClientEnvironment; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js deleted file mode 100644 index 8f65114..0000000 --- a/config/jest/cssTransform.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -// This is a custom Jest transformer turning style imports into empty objects. -// http://facebook.github.io/jest/docs/en/webpack.html - -module.exports = { - process() { - return 'module.exports = {};'; - }, - getCacheKey() { - // The output is always the same. - return 'cssTransform'; - }, -}; diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js deleted file mode 100644 index aab6761..0000000 --- a/config/jest/fileTransform.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const path = require('path'); -const camelcase = require('camelcase'); - -// This is a custom Jest transformer turning file imports into filenames. -// http://facebook.github.io/jest/docs/en/webpack.html - -module.exports = { - process(src, filename) { - const assetFilename = JSON.stringify(path.basename(filename)); - - if (filename.match(/\.svg$/)) { - // Based on how SVGR generates a component name: - // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 - const pascalCaseFilename = camelcase(path.parse(filename).name, { - pascalCase: true, - }); - const componentName = `Svg${pascalCaseFilename}`; - return `const React = require('react'); - module.exports = { - __esModule: true, - default: ${assetFilename}, - ReactComponent: React.forwardRef(function ${componentName}(props, ref) { - return { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }; - }), - };`; - } - - return `module.exports = ${assetFilename};`; - }, -}; diff --git a/config/modules.js b/config/modules.js deleted file mode 100644 index c84210a..0000000 --- a/config/modules.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const paths = require('./paths'); -const chalk = require('react-dev-utils/chalk'); -const resolve = require('resolve'); - -/** - * Get additional module paths based on the baseUrl of a compilerOptions object. - * - * @param {Object} options - */ -function getAdditionalModulePaths(options = {}) { - const baseUrl = options.baseUrl; - - // We need to explicitly check for null and undefined (and not a falsy value) because - // TypeScript treats an empty string as `.`. - if (baseUrl == null) { - // If there's no baseUrl set we respect NODE_PATH - // Note that NODE_PATH is deprecated and will be removed - // in the next major release of create-react-app. - - const nodePath = process.env.NODE_PATH || ''; - return nodePath.split(path.delimiter).filter(Boolean); - } - - const baseUrlResolved = path.resolve(paths.appPath, baseUrl); - - // We don't need to do anything if `baseUrl` is set to `node_modules`. This is - // the default behavior. - if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { - return null; - } - - // Allow the user set the `baseUrl` to `appSrc`. - if (path.relative(paths.appSrc, baseUrlResolved) === '') { - return [paths.appSrc]; - } - - // If the path is equal to the root directory we ignore it here. - // We don't want to allow importing from the root directly as source files are - // not transpiled outside of `src`. We do allow importing them with the - // absolute path (e.g. `src/Components/Button.js`) but we set that up with - // an alias. - if (path.relative(paths.appPath, baseUrlResolved) === '') { - return null; - } - - // Otherwise, throw an error. - throw new Error( - chalk.red.bold( - "Your project's `baseUrl` can only be set to `src` or `node_modules`." + - ' Create React App does not support other values at this time.' - ) - ); -} - -/** - * Get webpack aliases based on the baseUrl of a compilerOptions object. - * - * @param {*} options - */ -function getWebpackAliases(options = {}) { - const baseUrl = options.baseUrl; - - if (!baseUrl) { - return {}; - } - - const baseUrlResolved = path.resolve(paths.appPath, baseUrl); - - if (path.relative(paths.appPath, baseUrlResolved) === '') { - return { - src: paths.appSrc, - }; - } -} - -/** - * Get jest aliases based on the baseUrl of a compilerOptions object. - * - * @param {*} options - */ -function getJestAliases(options = {}) { - const baseUrl = options.baseUrl; - - if (!baseUrl) { - return {}; - } - - const baseUrlResolved = path.resolve(paths.appPath, baseUrl); - - if (path.relative(paths.appPath, baseUrlResolved) === '') { - return { - 'src/(.*)$': '/src/$1', - }; - } -} - -function getModules() { - // Check if TypeScript is setup - const hasTsConfig = fs.existsSync(paths.appTsConfig); - const hasJsConfig = fs.existsSync(paths.appJsConfig); - - if (hasTsConfig && hasJsConfig) { - throw new Error( - 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' - ); - } - - let config; - - // If there's a tsconfig.json we assume it's a - // TypeScript project and set up the config - // based on tsconfig.json - if (hasTsConfig) { - const ts = require(resolve.sync('typescript', { - basedir: paths.appNodeModules, - })); - config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; - // Otherwise we'll check if there is jsconfig.json - // for non TS projects. - } else if (hasJsConfig) { - config = require(paths.appJsConfig); - } - - config = config || {}; - const options = config.compilerOptions || {}; - - const additionalModulePaths = getAdditionalModulePaths(options); - - return { - additionalModulePaths: additionalModulePaths, - webpackAliases: getWebpackAliases(options), - jestAliases: getJestAliases(options), - hasTsConfig, - }; -} - -module.exports = getModules(); diff --git a/config/paths.js b/config/paths.js deleted file mode 100644 index 61b9240..0000000 --- a/config/paths.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; - -const path = require("path"); -const fs = require("fs"); -const url = require("url"); - -// Make sure any symlinks in the project folder are resolved: -// https://github.com/facebook/create-react-app/issues/637 -const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); - -const envPublicUrl = process.env.PUBLIC_URL; - -function ensureSlash(inputPath, needsSlash) { - const hasSlash = inputPath.endsWith("/"); - if (hasSlash && !needsSlash) { - return inputPath.substr(0, inputPath.length - 1); - } else if (!hasSlash && needsSlash) { - return `${inputPath}/`; - } else { - return inputPath; - } -} - -const getPublicUrl = (appPackageJson) => - envPublicUrl || require(appPackageJson).homepage; - -// We use `PUBLIC_URL` environment variable or "homepage" field to infer -// "public path" at which the app is served. -// Webpack needs to know it to put the right + + + + +
+ +
+
+ +
+ + + {siteScript} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..49db0df --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3300 @@ +{ + "name": "cloudreve-pro-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cloudreve-pro-frontend", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.1", + "@mui/material": "^5.15.1", + "@reduxjs/toolkit": "^2.0.1", + "axios": "^1.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^9.0.4", + "react-router-dom": "^6.21.0", + "redux": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "prettier": "3.1.1", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz", + "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.28", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.28.tgz", + "integrity": "sha512-KIoSc5sUFceeCaZTq5MQBapFzhHqMo4kj+4azWaCAjorduhcRQtN+BCgVHmo+gvEjix74bUfxwTqGifnu2fNTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.1", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.1.tgz", + "integrity": "sha512-y/nUEsWHyBzaKYp9zLtqJKrLod/zMNEWpMj488FuQY9QTmqBiyUhI2uh7PVaLqLewXRtdmG6JV0b6T5exyuYRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.1.tgz", + "integrity": "sha512-VPJdBSyap6uOxCb5BLbWbkvd6aeJCp1pQZm8DcZBITCH0NOSv8Mz9c8Zvo8xr4Od7+xyWHUAgvRSL4047pL2WQ==", + "dependencies": { + "@babel/runtime": "^7.23.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.1.tgz", + "integrity": "sha512-WA5DVyvacxDakVyAhNqu/rRT28ppuuUFFw1bLpmRzrCJ4uw/zLTATcd4WB3YbB+7MdZNEGG/SJNWTDLEIyn3xQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@mui/base": "5.0.0-beta.28", + "@mui/core-downloads-tracker": "^5.15.1", + "@mui/system": "^5.15.1", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.1", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.0.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.1.tgz", + "integrity": "sha512-wTbzuy5KjSvCPE9UVJktWHJ0b/tD5biavY9wvF+OpYDLPpdXK52vc1hTDxSbdkHIFMkJExzrwO9GvpVAHZBnFQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@mui/utils": "^5.15.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.1.tgz", + "integrity": "sha512-7WDZTJLqGexWDjqE9oAgjU8ak6hEtUw2yQU7SIYID5kLVO2Nj/Wi/KicbLsXnTsJNvSqePIlUIWTBSXwWJCPZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.1.tgz", + "integrity": "sha512-LAnP0ls69rqW9eBgI29phIx/lppv+WDGI7b3EJN7VZIqw0RezA0GD7NRpV12BgEYJABEii6z5Q9B5tg7dsX0Iw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@mui/private-theming": "^5.15.1", + "@mui/styled-engine": "^5.15.1", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.1", + "clsx": "^2.0.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.1.tgz", + "integrity": "sha512-V1/d0E3Bju5YdB59HJf2G0tnHrFEvWLN+f8hAXp9+JSNy/LC2zKyqUfPPahflR6qsI681P8G9r4mEZte/SrrYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz", + "integrity": "sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.0", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0.tgz", + "integrity": "sha512-WOHih+ClN7N8oHk9N4JUiMxQJmRVaOxcg8w7F/oHUXzJt920ekASLI/7cYX8XkntDWRhLZtsk6LbGrkgOAvi5A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz", + "integrity": "sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@swc/core": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", + "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.101", + "@swc/core-darwin-x64": "1.3.101", + "@swc/core-linux-arm-gnueabihf": "1.3.101", + "@swc/core-linux-arm64-gnu": "1.3.101", + "@swc/core-linux-arm64-musl": "1.3.101", + "@swc/core-linux-x64-gnu": "1.3.101", + "@swc/core-linux-x64-musl": "1.3.101", + "@swc/core-win32-arm64-msvc": "1.3.101", + "@swc/core-win32-ia32-msvc": "1.3.101", + "@swc/core-win32-x64-msvc": "1.3.101" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", + "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.2.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz", + "integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz", + "integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/type-utils": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz", + "integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz", + "integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz", + "integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz", + "integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz", + "integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz", + "integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz", + "integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.15.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.5.0.tgz", + "integrity": "sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.3.96" + }, + "peerDependencies": { + "vite": "^4 || ^5" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", + "integrity": "sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.10", + "@esbuild/android-arm": "0.19.10", + "@esbuild/android-arm64": "0.19.10", + "@esbuild/android-x64": "0.19.10", + "@esbuild/darwin-arm64": "0.19.10", + "@esbuild/darwin-x64": "0.19.10", + "@esbuild/freebsd-arm64": "0.19.10", + "@esbuild/freebsd-x64": "0.19.10", + "@esbuild/linux-arm": "0.19.10", + "@esbuild/linux-arm64": "0.19.10", + "@esbuild/linux-ia32": "0.19.10", + "@esbuild/linux-loong64": "0.19.10", + "@esbuild/linux-mips64el": "0.19.10", + "@esbuild/linux-ppc64": "0.19.10", + "@esbuild/linux-riscv64": "0.19.10", + "@esbuild/linux-s390x": "0.19.10", + "@esbuild/linux-x64": "0.19.10", + "@esbuild/netbsd-x64": "0.19.10", + "@esbuild/openbsd-x64": "0.19.10", + "@esbuild/sunos-x64": "0.19.10", + "@esbuild/win32-arm64": "0.19.10", + "@esbuild/win32-ia32": "0.19.10", + "@esbuild/win32-x64": "0.19.10" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.5.tgz", + "integrity": "sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.0.4.tgz", + "integrity": "sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.0.tgz", + "integrity": "sha512-hGZ0HXbwz3zw52pLZV3j3+ec+m/PQ9cTpBvqjFQmy2XVUWGn5MD+31oXHb6dVTxYzmAeaiUBYjkoNz66n3RGCg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.14.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.0.tgz", + "integrity": "sha512-1dUdVj3cwc1npzJaf23gulB562ESNvxf7E4x8upNJycqyUm5BRRZ6dd3LrlzhtLaMrwOCO8R0zoiYxdaJx4LlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.14.0", + "react-router": "6.21.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/redux": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0.tgz", + "integrity": "sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/reselect": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.1.tgz", + "integrity": "sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz", + "integrity": "sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.1", + "@rollup/rollup-android-arm64": "4.9.1", + "@rollup/rollup-darwin-arm64": "4.9.1", + "@rollup/rollup-darwin-x64": "4.9.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.1", + "@rollup/rollup-linux-arm64-gnu": "4.9.1", + "@rollup/rollup-linux-arm64-musl": "4.9.1", + "@rollup/rollup-linux-riscv64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-musl": "4.9.1", + "@rollup/rollup-win32-arm64-msvc": "4.9.1", + "@rollup/rollup-win32-ia32-msvc": "4.9.1", + "@rollup/rollup-win32-x64-msvc": "4.9.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/vite": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", + "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 7c63c90..2440140 100644 --- a/package.json +++ b/package.json @@ -1,202 +1,98 @@ { "name": "cloudreve-frontend", - "version": "3.8.3", "private": true, - "dependencies": { - "@babel/core": "7.6.0", - "@material-ui/core": "~4.11.0", - "@material-ui/icons": "^4.5.1", - "@material-ui/lab": "4.0.0-alpha.57", - "@svgr/webpack": "4.3.2", - "@types/invariant": "^2.2.32", - "@types/jest": "^25.2.2", - "@types/node": "^14.0.1", - "@types/react": "^16.9.35", - "@types/react-dom": "^16.9.8", - "@types/streamsaver": "^2.0.1", - "@types/wicg-file-system-access": "^2020.9.5", - "@typescript-eslint/eslint-plugin": "^2.33.0", - "@typescript-eslint/parser": "^2.33.0", - "ahooks": "^3.5.2", - "artplayer": "^4.3.4", - "axios": "^0.21.1", - "babel-eslint": "10.0.3", - "babel-jest": "^24.9.0", - "babel-loader": "8.0.6", - "babel-plugin-named-asset-import": "^0.3.4", - "babel-preset-react-app": "^9.0.2", - "camelcase": "^5.2.0", - "case-sensitive-paths-webpack-plugin": "2.2.0", - "classnames": "^2.2.6", - "clsx": "latest", - "connected-react-router": "^6.9.1", - "css-loader": "2.1.1", - "dayjs": "^1.10.4", - "dotenv": "6.2.0", - "dotenv-expand": "5.1.0", - "eslint": "^6.8.0", - "eslint-config-react-app": "^5.0.2", - "eslint-loader": "3.0.2", - "eslint-plugin-flowtype": "3.13.0", - "eslint-plugin-import": "2.18.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "^7.19.0", - "file-loader": "3.0.1", - "for-editor": "^0.3.5", - "fs-extra": "7.0.1", - "html-webpack-plugin": "4.0.0-beta.5", - "http-proxy-middleware": "^0.20.0", - "husky": "^4.2.5", - "i18next": "^21.6.16", - "i18next-browser-languagedetector": "^6.1.4", - "i18next-chained-backend": "^4.2.0", - "i18next-http-backend": "^1.4.0", - "i18next-localstorage-backend": "^4.1.0", - "identity-obj-proxy": "3.0.0", - "invariant": "^2.2.4", - "is-wsl": "^1.1.0", - "jest": "24.9.0", - "jest-environment-jsdom-fourteen": "0.1.0", - "jest-resolve": "24.9.0", - "jest-watch-typeahead": "0.4.0", - "material-ui-toggle-icon": "^1.1.1", - "mdi-material-ui": "^6.9.0", - "mini-css-extract-plugin": "0.8.0", - "monaco-editor-webpack-plugin": "^3.0.0", - "optimize-css-assets-webpack-plugin": "5.0.3", - "pnp-webpack-plugin": "1.5.0", - "postcss-flexbugs-fixes": "4.1.0", - "postcss-loader": "3.0.0", - "postcss-normalize": "7.0.1", - "postcss-preset-env": "6.7.0", - "postcss-safe-parser": "4.0.1", - "qrcode.react": "^3.1.0", - "react": "^16.12.0", - "react-addons-update": "^15.6.2", - "react-app-polyfill": "^1.0.4", - "react-async-script": "^1.1.1", - "react-color": "^2.18.0", - "react-content-loader": "^5.0.2", - "react-dev-utils": "9", - "react-dnd": "^9.5.1", - "react-dnd-html5-backend": "^9.5.1", - "react-dom": "^16.12.0", - "react-highlight-words": "^0.18.0", - "react-hotkeys": "^2.0.0", - "react-i18next": "^11.16.7", - "react-lazy-load-image-component": "^1.3.2", - "react-load-script": "^0.0.6", - "react-monaco-editor": "^0.36.0", - "react-pdf": "^4.1.0", - "react-photo-view": "^0.4.0", - "react-reader": "^0.21.1", - "react-redux": "^7.1.3", - "react-router": "^5.1.2", - "react-router-dom": "^5.1.2", - "react-virtuoso": "^2.8.6", - "recharts": "^2.0.6", - "redux": "^4.0.4", - "redux-mock-store": "^1.5.4", - "redux-thunk": "^2.3.0", - "resolve": "1.12.0", - "resolve-url-loader": "3.1.0", - "sass-loader": "7.2.0", - "semver": "6.3.0", - "streamsaver": "^2.0.6", - "style-loader": "1.0.0", - "styled-components": "^5.3.6", - "terser-webpack-plugin": "1.4.1", - "timeago-react": "^3.0.0", - "ts-pnp": "1.1.4", - "typescript": "^3.9.2", - "url-loader": "2.1.0", - "webpack": "4.41.0", - "webpack-dev-server": "3.11.3", - "webpack-manifest-plugin": "2.1.1", - "workbox-webpack-plugin": "4.3.1" - }, + "version": "4.0.0-next", + "type": "module", "scripts": { - "start": "node scripts/start.js", - "build": "node scripts/build.js", - "test": "node scripts/test.js", - "eslint": "eslint src --fix", - "postinstall": "node node_modules/husky/lib/installer/bin install" + "dev": "vite", + "build": "vite build", + "build-prod": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "jest": { - "roots": [ - "/src" - ], - "collectCoverageFrom": [ - "src/**/*.{js,jsx,ts,tsx}", - "!src/**/*.d.ts" - ], - "setupFiles": [ - "react-app-polyfill/jsdom" - ], - "setupFilesAfterEnv": [], - "testMatch": [ - "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", - "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" - ], - "testEnvironment": "jest-environment-jsdom-fourteen", - "transform": { - "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", - "^.+\\.css$": "/config/jest/cssTransform.js", - "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" - }, - "transformIgnorePatterns": [ - "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", - "^.+\\.module\\.(css|sass|scss)$" - ], - "modulePaths": [], - "moduleNameMapper": { - "^react-native$": "react-native-web", - "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" - }, - "moduleFileExtensions": [ - "web.js", - "js", - "web.ts", - "ts", - "web.tsx", - "tsx", - "json", - "web.jsx", - "jsx", - "node" - ], - "watchPlugins": [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname" - ] - }, - "babel": { - "presets": [ - "react-app" - ] + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.8", + "@giscus/react": "^3.1.0", + "@marsidev/react-turnstile": "^1.1.0", + "@mdxeditor/editor": "^3.4.0", + "@mui/icons-material": "^6.0.0", + "@mui/lab": "^6.0.0-beta.30", + "@mui/material": "^6.4.6", + "@mui/x-date-pickers": "^6.20.2", + "@mui/x-tree-view": "^6.17.0", + "@reduxjs/toolkit": "^2.0.1", + "@types/path-browserify": "^1.0.2", + "@types/streamsaver": "^2.0.4", + "@uiw/color-convert": "^2.1.1", + "@uiw/react-color-sketch": "^2.1.1", + "artplayer": "5.2.2", + "artplayer-plugin-chapter": "^1.0.0", + "axios": "^1.6.2", + "dayjs": "^1.11.10", + "fuse.js": "^7.0.0", + "i18next": "^23.7.11", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-chained-backend": "^4.6.2", + "i18next-http-backend": "^2.4.2", + "i18next-localstorage-backend": "^4.2.0", + "leaflet": "^1.9.4", + "lodash": "^4.17.21", + "material-ui-popup-state": "^5.0.10", + "monaco-editor": "^0.49.0", + "mui-one-time-password-input": "^2.0.1", + "notistack": "^3.0.1", + "nuqs": "^2.3.1", + "path-browserify": "^1.0.1", + "pdfjs-dist": "4.10.38", + "qrcode.react": "^4.1.0", + "react": "^18.2.0", + "react-animate-height": "^3.2.3", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", + "react-filerobot-image-editor": "^4.8.1", + "react-google-recaptcha": "^3.1.0", + "react-highlight-words": "^0.20.0", + "react-hotkeys-hook": "^4.5.1", + "react-i18next": "^14.0.0", + "react-image-crop": "^11.0.7", + "react-intersection-observer": "^9.5.3", + "react-konva": "^18.2.10", + "react-leaflet": "^4.2.1", + "react-reader": "^2.0.10", + "react-redux": "^9.0.4", + "react-router-dom": "^6.21.0", + "react-transition-group": "^4.4.5", + "react-virtuoso": "^4.10.4", + "recharts": "^2.15.1", + "redux": "^5.0.0", + "streamsaver": "^2.0.6", + "styled-components": "^6.1.11", + "timeago-react": "^3.0.6" }, "devDependencies": { - "copy-webpack-plugin": "^5.1.1", - "eslint-plugin-react-hooks": "^4.0.0" - }, - "husky": { - "hooks": { - "pre-commit": "yarn run eslint" - } - }, - "resolutions": { - "@types/react": "^16.9.35" + "@types/leaflet": "^1.9.12", + "@types/lodash": "^4.14.202", + "@types/node": "^20.10.5", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@types/webappsec-credential-management": "^0.6.9", + "@types/wicg-file-system-access": "^2023.10.5", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "less": "^4.2.0", + "prettier": "3.1.1", + "typescript": "^5.2.2", + "vite": "5.4.6", + "vite-plugin-mkcert": "^1.17.6", + "vite-plugin-pwa": "^0.21.1", + "vite-plugin-static-copy": "^2.2.0" } -} +} \ No newline at end of file diff --git a/public/assets/pdfjs/cmaps/78-EUC-H.bcmap b/public/assets/pdfjs/cmaps/78-EUC-H.bcmap new file mode 100644 index 0000000..2655fc7 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78-EUC-V.bcmap b/public/assets/pdfjs/cmaps/78-EUC-V.bcmap new file mode 100644 index 0000000..f1ed853 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78-H.bcmap b/public/assets/pdfjs/cmaps/78-H.bcmap new file mode 100644 index 0000000..39e89d3 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/78-RKSJ-H.bcmap new file mode 100644 index 0000000..e4167cb Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/78-RKSJ-V.bcmap new file mode 100644 index 0000000..50b1646 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78-V.bcmap b/public/assets/pdfjs/cmaps/78-V.bcmap new file mode 100644 index 0000000..d7af99b Binary files /dev/null and b/public/assets/pdfjs/cmaps/78-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78ms-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/78ms-RKSJ-H.bcmap new file mode 100644 index 0000000..37077d0 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78ms-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/78ms-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/78ms-RKSJ-V.bcmap new file mode 100644 index 0000000..acf2323 Binary files /dev/null and b/public/assets/pdfjs/cmaps/78ms-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/83pv-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/83pv-RKSJ-H.bcmap new file mode 100644 index 0000000..2359bc5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/83pv-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90ms-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/90ms-RKSJ-H.bcmap new file mode 100644 index 0000000..af82938 Binary files /dev/null and b/public/assets/pdfjs/cmaps/90ms-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90ms-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/90ms-RKSJ-V.bcmap new file mode 100644 index 0000000..780549d Binary files /dev/null and b/public/assets/pdfjs/cmaps/90ms-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90msp-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/90msp-RKSJ-H.bcmap new file mode 100644 index 0000000..bfd3119 Binary files /dev/null and b/public/assets/pdfjs/cmaps/90msp-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90msp-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/90msp-RKSJ-V.bcmap new file mode 100644 index 0000000..25ef14a Binary files /dev/null and b/public/assets/pdfjs/cmaps/90msp-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90pv-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/90pv-RKSJ-H.bcmap new file mode 100644 index 0000000..02f713b Binary files /dev/null and b/public/assets/pdfjs/cmaps/90pv-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/90pv-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/90pv-RKSJ-V.bcmap new file mode 100644 index 0000000..d08e0cc Binary files /dev/null and b/public/assets/pdfjs/cmaps/90pv-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Add-H.bcmap b/public/assets/pdfjs/cmaps/Add-H.bcmap new file mode 100644 index 0000000..59442ac Binary files /dev/null and b/public/assets/pdfjs/cmaps/Add-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Add-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/Add-RKSJ-H.bcmap new file mode 100644 index 0000000..a3065e4 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Add-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Add-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/Add-RKSJ-V.bcmap new file mode 100644 index 0000000..040014c Binary files /dev/null and b/public/assets/pdfjs/cmaps/Add-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Add-V.bcmap b/public/assets/pdfjs/cmaps/Add-V.bcmap new file mode 100644 index 0000000..2f816d3 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Add-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-0.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-0.bcmap new file mode 100644 index 0000000..88ec04a Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-0.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-1.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-1.bcmap new file mode 100644 index 0000000..03a5014 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-1.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-2.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-2.bcmap new file mode 100644 index 0000000..2aa9514 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-3.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-3.bcmap new file mode 100644 index 0000000..86d8b8c Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-3.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-4.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-4.bcmap new file mode 100644 index 0000000..f50fc6c Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-4.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-5.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-5.bcmap new file mode 100644 index 0000000..6caf4a8 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-5.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-6.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-6.bcmap new file mode 100644 index 0000000..b77fb07 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-6.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap b/public/assets/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap new file mode 100644 index 0000000..69d79a2 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-0.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-0.bcmap new file mode 100644 index 0000000..3610108 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-0.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-1.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-1.bcmap new file mode 100644 index 0000000..707bb10 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-1.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-2.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-2.bcmap new file mode 100644 index 0000000..f7648cc Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-3.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-3.bcmap new file mode 100644 index 0000000..8521458 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-3.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-4.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-4.bcmap new file mode 100644 index 0000000..e40c63a Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-4.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-5.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-5.bcmap new file mode 100644 index 0000000..d7623b5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-5.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap b/public/assets/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap new file mode 100644 index 0000000..7586525 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-0.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-0.bcmap new file mode 100644 index 0000000..f0e94ec Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-0.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-1.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-1.bcmap new file mode 100644 index 0000000..dad42c5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-1.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-2.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-2.bcmap new file mode 100644 index 0000000..090819a Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-3.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-3.bcmap new file mode 100644 index 0000000..087dfc1 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-3.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-4.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-4.bcmap new file mode 100644 index 0000000..46aa9bf Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-4.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-5.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-5.bcmap new file mode 100644 index 0000000..5b4b65c Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-5.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-6.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-6.bcmap new file mode 100644 index 0000000..e77d699 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-6.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap b/public/assets/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap new file mode 100644 index 0000000..128a141 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Korea1-0.bcmap b/public/assets/pdfjs/cmaps/Adobe-Korea1-0.bcmap new file mode 100644 index 0000000..cef1a99 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Korea1-0.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Korea1-1.bcmap b/public/assets/pdfjs/cmaps/Adobe-Korea1-1.bcmap new file mode 100644 index 0000000..11ffa36 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Korea1-1.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Korea1-2.bcmap b/public/assets/pdfjs/cmaps/Adobe-Korea1-2.bcmap new file mode 100644 index 0000000..3172308 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Korea1-2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap b/public/assets/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap new file mode 100644 index 0000000..f3371c0 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap differ diff --git a/public/assets/pdfjs/cmaps/B5-H.bcmap b/public/assets/pdfjs/cmaps/B5-H.bcmap new file mode 100644 index 0000000..beb4d22 Binary files /dev/null and b/public/assets/pdfjs/cmaps/B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/B5-V.bcmap b/public/assets/pdfjs/cmaps/B5-V.bcmap new file mode 100644 index 0000000..2d4f87d Binary files /dev/null and b/public/assets/pdfjs/cmaps/B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/B5pc-H.bcmap b/public/assets/pdfjs/cmaps/B5pc-H.bcmap new file mode 100644 index 0000000..ce00131 Binary files /dev/null and b/public/assets/pdfjs/cmaps/B5pc-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/B5pc-V.bcmap b/public/assets/pdfjs/cmaps/B5pc-V.bcmap new file mode 100644 index 0000000..73b99ff Binary files /dev/null and b/public/assets/pdfjs/cmaps/B5pc-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS-EUC-H.bcmap b/public/assets/pdfjs/cmaps/CNS-EUC-H.bcmap new file mode 100644 index 0000000..61d1d0c Binary files /dev/null and b/public/assets/pdfjs/cmaps/CNS-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS-EUC-V.bcmap b/public/assets/pdfjs/cmaps/CNS-EUC-V.bcmap new file mode 100644 index 0000000..1a393a5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/CNS-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS1-H.bcmap b/public/assets/pdfjs/cmaps/CNS1-H.bcmap new file mode 100644 index 0000000..f738e21 Binary files /dev/null and b/public/assets/pdfjs/cmaps/CNS1-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS1-V.bcmap b/public/assets/pdfjs/cmaps/CNS1-V.bcmap new file mode 100644 index 0000000..9c3169f Binary files /dev/null and b/public/assets/pdfjs/cmaps/CNS1-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS2-H.bcmap b/public/assets/pdfjs/cmaps/CNS2-H.bcmap new file mode 100644 index 0000000..c89b352 Binary files /dev/null and b/public/assets/pdfjs/cmaps/CNS2-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/CNS2-V.bcmap b/public/assets/pdfjs/cmaps/CNS2-V.bcmap new file mode 100644 index 0000000..7588cec --- /dev/null +++ b/public/assets/pdfjs/cmaps/CNS2-V.bcmap @@ -0,0 +1,3 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSECNS2-H \ No newline at end of file diff --git a/public/assets/pdfjs/cmaps/ETHK-B5-H.bcmap b/public/assets/pdfjs/cmaps/ETHK-B5-H.bcmap new file mode 100644 index 0000000..cb29415 Binary files /dev/null and b/public/assets/pdfjs/cmaps/ETHK-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/ETHK-B5-V.bcmap b/public/assets/pdfjs/cmaps/ETHK-B5-V.bcmap new file mode 100644 index 0000000..f09aec6 Binary files /dev/null and b/public/assets/pdfjs/cmaps/ETHK-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/ETen-B5-H.bcmap b/public/assets/pdfjs/cmaps/ETen-B5-H.bcmap new file mode 100644 index 0000000..c2d7746 Binary files /dev/null and b/public/assets/pdfjs/cmaps/ETen-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/ETen-B5-V.bcmap b/public/assets/pdfjs/cmaps/ETen-B5-V.bcmap new file mode 100644 index 0000000..89bff15 Binary files /dev/null and b/public/assets/pdfjs/cmaps/ETen-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/ETenms-B5-H.bcmap b/public/assets/pdfjs/cmaps/ETenms-B5-H.bcmap new file mode 100644 index 0000000..a7d69db --- /dev/null +++ b/public/assets/pdfjs/cmaps/ETenms-B5-H.bcmap @@ -0,0 +1,3 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE ETen-B5-H` ^ \ No newline at end of file diff --git a/public/assets/pdfjs/cmaps/ETenms-B5-V.bcmap b/public/assets/pdfjs/cmaps/ETenms-B5-V.bcmap new file mode 100644 index 0000000..adc5d61 Binary files /dev/null and b/public/assets/pdfjs/cmaps/ETenms-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/EUC-H.bcmap b/public/assets/pdfjs/cmaps/EUC-H.bcmap new file mode 100644 index 0000000..e92ea5b Binary files /dev/null and b/public/assets/pdfjs/cmaps/EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/EUC-V.bcmap b/public/assets/pdfjs/cmaps/EUC-V.bcmap new file mode 100644 index 0000000..7a7c183 Binary files /dev/null and b/public/assets/pdfjs/cmaps/EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Ext-H.bcmap b/public/assets/pdfjs/cmaps/Ext-H.bcmap new file mode 100644 index 0000000..3b5cde4 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Ext-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Ext-RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/Ext-RKSJ-H.bcmap new file mode 100644 index 0000000..ea4d2d9 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Ext-RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Ext-RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/Ext-RKSJ-V.bcmap new file mode 100644 index 0000000..3457c27 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Ext-RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Ext-V.bcmap b/public/assets/pdfjs/cmaps/Ext-V.bcmap new file mode 100644 index 0000000..4999ca4 Binary files /dev/null and b/public/assets/pdfjs/cmaps/Ext-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GB-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GB-EUC-H.bcmap new file mode 100644 index 0000000..e39908b Binary files /dev/null and b/public/assets/pdfjs/cmaps/GB-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GB-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GB-EUC-V.bcmap new file mode 100644 index 0000000..d5be544 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GB-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GB-H.bcmap b/public/assets/pdfjs/cmaps/GB-H.bcmap new file mode 100644 index 0000000..39189c5 --- /dev/null +++ b/public/assets/pdfjs/cmaps/GB-H.bcmap @@ -0,0 +1,4 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE!!]aX!!]`21> p z$]"Rd-U7* 4%+ Z {/%<9Kb1]." `],"] +"]h"]F"]$"]"]`"]>"]"]z"]X"]6"]"]r"]P"]."] "]j"]H"]&"]"]b"]@"]"]|"]Z"]8"]"]t"]R"]0"]"]l"]J"]("]"]d"]B"] "X~']W"]5"]"]q"]O"]-"] "]i"]G"]%"]"]a"]?"]"]{"]Y"]7"]"]s"]Q"]/"] "]k"]I"]'"]"]c"]A"]"]}"]["]9 \ No newline at end of file diff --git a/public/assets/pdfjs/cmaps/GB-V.bcmap b/public/assets/pdfjs/cmaps/GB-V.bcmap new file mode 100644 index 0000000..3108345 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GB-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBK-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GBK-EUC-H.bcmap new file mode 100644 index 0000000..05fff7e Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBK-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBK-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GBK-EUC-V.bcmap new file mode 100644 index 0000000..0cdf6be Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBK-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBK2K-H.bcmap b/public/assets/pdfjs/cmaps/GBK2K-H.bcmap new file mode 100644 index 0000000..46f6ba5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBK2K-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBK2K-V.bcmap b/public/assets/pdfjs/cmaps/GBK2K-V.bcmap new file mode 100644 index 0000000..d9a9479 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBK2K-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBKp-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GBKp-EUC-H.bcmap new file mode 100644 index 0000000..5cb0af6 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBKp-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBKp-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GBKp-EUC-V.bcmap new file mode 100644 index 0000000..bca93b8 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBKp-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBT-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GBT-EUC-H.bcmap new file mode 100644 index 0000000..4b4e2d3 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBT-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBT-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GBT-EUC-V.bcmap new file mode 100644 index 0000000..38f7066 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBT-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBT-H.bcmap b/public/assets/pdfjs/cmaps/GBT-H.bcmap new file mode 100644 index 0000000..8437ac3 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBT-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBT-V.bcmap b/public/assets/pdfjs/cmaps/GBT-V.bcmap new file mode 100644 index 0000000..697ab4a Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBT-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBTpc-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GBTpc-EUC-H.bcmap new file mode 100644 index 0000000..f6e50e8 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBTpc-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBTpc-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GBTpc-EUC-V.bcmap new file mode 100644 index 0000000..6c0d71a Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBTpc-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBpc-EUC-H.bcmap b/public/assets/pdfjs/cmaps/GBpc-EUC-H.bcmap new file mode 100644 index 0000000..c9edf67 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBpc-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/GBpc-EUC-V.bcmap b/public/assets/pdfjs/cmaps/GBpc-EUC-V.bcmap new file mode 100644 index 0000000..31450c9 Binary files /dev/null and b/public/assets/pdfjs/cmaps/GBpc-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/H.bcmap b/public/assets/pdfjs/cmaps/H.bcmap new file mode 100644 index 0000000..7b24ea4 Binary files /dev/null and b/public/assets/pdfjs/cmaps/H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKdla-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKdla-B5-H.bcmap new file mode 100644 index 0000000..7d30c05 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKdla-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKdla-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKdla-B5-V.bcmap new file mode 100644 index 0000000..7894694 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKdla-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKdlb-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKdlb-B5-H.bcmap new file mode 100644 index 0000000..d829a23 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKdlb-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKdlb-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKdlb-B5-V.bcmap new file mode 100644 index 0000000..2b572b5 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKdlb-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKgccs-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKgccs-B5-H.bcmap new file mode 100644 index 0000000..971a4f2 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKgccs-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKgccs-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKgccs-B5-V.bcmap new file mode 100644 index 0000000..d353ca2 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKgccs-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKm314-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKm314-B5-H.bcmap new file mode 100644 index 0000000..576dc01 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKm314-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKm314-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKm314-B5-V.bcmap new file mode 100644 index 0000000..0e96d0e Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKm314-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKm471-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKm471-B5-H.bcmap new file mode 100644 index 0000000..11d170c Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKm471-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKm471-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKm471-B5-V.bcmap new file mode 100644 index 0000000..54959bf Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKm471-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKscs-B5-H.bcmap b/public/assets/pdfjs/cmaps/HKscs-B5-H.bcmap new file mode 100644 index 0000000..6ef7857 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKscs-B5-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/HKscs-B5-V.bcmap b/public/assets/pdfjs/cmaps/HKscs-B5-V.bcmap new file mode 100644 index 0000000..1fb2fa2 Binary files /dev/null and b/public/assets/pdfjs/cmaps/HKscs-B5-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Hankaku.bcmap b/public/assets/pdfjs/cmaps/Hankaku.bcmap new file mode 100644 index 0000000..4b8ec7f Binary files /dev/null and b/public/assets/pdfjs/cmaps/Hankaku.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Hiragana.bcmap b/public/assets/pdfjs/cmaps/Hiragana.bcmap new file mode 100644 index 0000000..17e983e Binary files /dev/null and b/public/assets/pdfjs/cmaps/Hiragana.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-EUC-H.bcmap b/public/assets/pdfjs/cmaps/KSC-EUC-H.bcmap new file mode 100644 index 0000000..a45c65f Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-EUC-V.bcmap b/public/assets/pdfjs/cmaps/KSC-EUC-V.bcmap new file mode 100644 index 0000000..0e7b21f Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-H.bcmap b/public/assets/pdfjs/cmaps/KSC-H.bcmap new file mode 100644 index 0000000..b9b22b6 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-Johab-H.bcmap b/public/assets/pdfjs/cmaps/KSC-Johab-H.bcmap new file mode 100644 index 0000000..2531ffc Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-Johab-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-Johab-V.bcmap b/public/assets/pdfjs/cmaps/KSC-Johab-V.bcmap new file mode 100644 index 0000000..367ceb2 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-Johab-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSC-V.bcmap b/public/assets/pdfjs/cmaps/KSC-V.bcmap new file mode 100644 index 0000000..6ae2f0b Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCms-UHC-H.bcmap b/public/assets/pdfjs/cmaps/KSCms-UHC-H.bcmap new file mode 100644 index 0000000..a8d4240 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCms-UHC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap b/public/assets/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap new file mode 100644 index 0000000..8b4ae18 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap b/public/assets/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap new file mode 100644 index 0000000..b655dbc Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCms-UHC-V.bcmap b/public/assets/pdfjs/cmaps/KSCms-UHC-V.bcmap new file mode 100644 index 0000000..21f97f6 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCms-UHC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCpc-EUC-H.bcmap b/public/assets/pdfjs/cmaps/KSCpc-EUC-H.bcmap new file mode 100644 index 0000000..e06f361 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCpc-EUC-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/KSCpc-EUC-V.bcmap b/public/assets/pdfjs/cmaps/KSCpc-EUC-V.bcmap new file mode 100644 index 0000000..f3c9113 Binary files /dev/null and b/public/assets/pdfjs/cmaps/KSCpc-EUC-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Katakana.bcmap b/public/assets/pdfjs/cmaps/Katakana.bcmap new file mode 100644 index 0000000..524303c Binary files /dev/null and b/public/assets/pdfjs/cmaps/Katakana.bcmap differ diff --git a/public/assets/pdfjs/cmaps/LICENSE b/public/assets/pdfjs/cmaps/LICENSE new file mode 100644 index 0000000..b1ad168 --- /dev/null +++ b/public/assets/pdfjs/cmaps/LICENSE @@ -0,0 +1,36 @@ +%%Copyright: ----------------------------------------------------------- +%%Copyright: Copyright 1990-2009 Adobe Systems Incorporated. +%%Copyright: All rights reserved. +%%Copyright: +%%Copyright: Redistribution and use in source and binary forms, with or +%%Copyright: without modification, are permitted provided that the +%%Copyright: following conditions are met: +%%Copyright: +%%Copyright: Redistributions of source code must retain the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer. +%%Copyright: +%%Copyright: Redistributions in binary form must reproduce the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer in the documentation and/or other materials +%%Copyright: provided with the distribution. +%%Copyright: +%%Copyright: Neither the name of Adobe Systems Incorporated nor the names +%%Copyright: of its contributors may be used to endorse or promote +%%Copyright: products derived from this software without specific prior +%%Copyright: written permission. +%%Copyright: +%%Copyright: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +%%Copyright: CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +%%Copyright: INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +%%Copyright: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +%%Copyright: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +%%Copyright: CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +%%Copyright: SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +%%Copyright: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%Copyright: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%Copyright: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +%%Copyright: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +%%Copyright: OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +%%Copyright: SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%Copyright: ----------------------------------------------------------- diff --git a/public/assets/pdfjs/cmaps/NWP-H.bcmap b/public/assets/pdfjs/cmaps/NWP-H.bcmap new file mode 100644 index 0000000..afc5e4b Binary files /dev/null and b/public/assets/pdfjs/cmaps/NWP-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/NWP-V.bcmap b/public/assets/pdfjs/cmaps/NWP-V.bcmap new file mode 100644 index 0000000..bb5785e Binary files /dev/null and b/public/assets/pdfjs/cmaps/NWP-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/RKSJ-H.bcmap b/public/assets/pdfjs/cmaps/RKSJ-H.bcmap new file mode 100644 index 0000000..fb8d298 Binary files /dev/null and b/public/assets/pdfjs/cmaps/RKSJ-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/RKSJ-V.bcmap b/public/assets/pdfjs/cmaps/RKSJ-V.bcmap new file mode 100644 index 0000000..a2555a6 Binary files /dev/null and b/public/assets/pdfjs/cmaps/RKSJ-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/Roman.bcmap b/public/assets/pdfjs/cmaps/Roman.bcmap new file mode 100644 index 0000000..f896dcf Binary files /dev/null and b/public/assets/pdfjs/cmaps/Roman.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UCS2-H.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UCS2-H.bcmap new file mode 100644 index 0000000..d5db27c Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UCS2-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UCS2-V.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UCS2-V.bcmap new file mode 100644 index 0000000..1dc9b7a Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UCS2-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF16-H.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF16-H.bcmap new file mode 100644 index 0000000..961afef Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF16-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF16-V.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF16-V.bcmap new file mode 100644 index 0000000..df0cffe Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF16-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF32-H.bcmap new file mode 100644 index 0000000..1ab18a1 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF32-V.bcmap new file mode 100644 index 0000000..ad14662 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF8-H.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF8-H.bcmap new file mode 100644 index 0000000..83c6bd7 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF8-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniCNS-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniCNS-UTF8-V.bcmap new file mode 100644 index 0000000..22a27e4 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniCNS-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UCS2-H.bcmap b/public/assets/pdfjs/cmaps/UniGB-UCS2-H.bcmap new file mode 100644 index 0000000..5bd6228 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UCS2-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UCS2-V.bcmap b/public/assets/pdfjs/cmaps/UniGB-UCS2-V.bcmap new file mode 100644 index 0000000..53c534b Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UCS2-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF16-H.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF16-H.bcmap new file mode 100644 index 0000000..b95045b Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF16-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF16-V.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF16-V.bcmap new file mode 100644 index 0000000..51f023e Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF16-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF32-H.bcmap new file mode 100644 index 0000000..f0dbd14 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF32-V.bcmap new file mode 100644 index 0000000..ce9c30a Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF8-H.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF8-H.bcmap new file mode 100644 index 0000000..982ca46 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF8-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniGB-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniGB-UTF8-V.bcmap new file mode 100644 index 0000000..f78020d Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniGB-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UCS2-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UCS2-H.bcmap new file mode 100644 index 0000000..7daf56a Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UCS2-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap new file mode 100644 index 0000000..ac9975c Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap new file mode 100644 index 0000000..3da0a1c Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UCS2-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UCS2-V.bcmap new file mode 100644 index 0000000..c50b9dd Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UCS2-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF16-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF16-H.bcmap new file mode 100644 index 0000000..6761344 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF16-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF16-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF16-V.bcmap new file mode 100644 index 0000000..70bf90c Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF16-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF32-H.bcmap new file mode 100644 index 0000000..7a83d53 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF32-V.bcmap new file mode 100644 index 0000000..7a87135 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF8-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF8-H.bcmap new file mode 100644 index 0000000..9f0334c Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF8-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS-UTF8-V.bcmap new file mode 100644 index 0000000..808a94f Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap new file mode 100644 index 0000000..d768bf8 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap new file mode 100644 index 0000000..3d5bf6f Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap new file mode 100644 index 0000000..09eee10 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap new file mode 100644 index 0000000..6c54600 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap new file mode 100644 index 0000000..1b1a64f Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap new file mode 100644 index 0000000..994aa9e Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap b/public/assets/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap new file mode 100644 index 0000000..643f921 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap b/public/assets/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap new file mode 100644 index 0000000..c148f67 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap new file mode 100644 index 0000000..1849d80 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap new file mode 100644 index 0000000..a83a677 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap new file mode 100644 index 0000000..f527248 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap new file mode 100644 index 0000000..e1a988d Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap new file mode 100644 index 0000000..47e054a Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UCS2-H.bcmap b/public/assets/pdfjs/cmaps/UniKS-UCS2-H.bcmap new file mode 100644 index 0000000..b5b9485 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UCS2-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UCS2-V.bcmap b/public/assets/pdfjs/cmaps/UniKS-UCS2-V.bcmap new file mode 100644 index 0000000..026adca Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UCS2-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF16-H.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF16-H.bcmap new file mode 100644 index 0000000..fd4e66e Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF16-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF16-V.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF16-V.bcmap new file mode 100644 index 0000000..075efb7 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF16-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF32-H.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF32-H.bcmap new file mode 100644 index 0000000..769d214 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF32-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF32-V.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF32-V.bcmap new file mode 100644 index 0000000..bdab208 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF32-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF8-H.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF8-H.bcmap new file mode 100644 index 0000000..6ff8674 Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF8-H.bcmap differ diff --git a/public/assets/pdfjs/cmaps/UniKS-UTF8-V.bcmap b/public/assets/pdfjs/cmaps/UniKS-UTF8-V.bcmap new file mode 100644 index 0000000..8dfa76a Binary files /dev/null and b/public/assets/pdfjs/cmaps/UniKS-UTF8-V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/V.bcmap b/public/assets/pdfjs/cmaps/V.bcmap new file mode 100644 index 0000000..fdec990 Binary files /dev/null and b/public/assets/pdfjs/cmaps/V.bcmap differ diff --git a/public/assets/pdfjs/cmaps/WP-Symbol.bcmap b/public/assets/pdfjs/cmaps/WP-Symbol.bcmap new file mode 100644 index 0000000..46729bb Binary files /dev/null and b/public/assets/pdfjs/cmaps/WP-Symbol.bcmap differ diff --git a/public/assets/pdfjs/images/altText_add.svg b/public/assets/pdfjs/images/altText_add.svg new file mode 100644 index 0000000..3451b53 --- /dev/null +++ b/public/assets/pdfjs/images/altText_add.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/altText_disclaimer.svg b/public/assets/pdfjs/images/altText_disclaimer.svg new file mode 100644 index 0000000..6fe79e7 --- /dev/null +++ b/public/assets/pdfjs/images/altText_disclaimer.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/altText_done.svg b/public/assets/pdfjs/images/altText_done.svg new file mode 100644 index 0000000..f54924e --- /dev/null +++ b/public/assets/pdfjs/images/altText_done.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/altText_spinner.svg b/public/assets/pdfjs/images/altText_spinner.svg new file mode 100644 index 0000000..fedb472 --- /dev/null +++ b/public/assets/pdfjs/images/altText_spinner.svg @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/public/assets/pdfjs/images/altText_warning.svg b/public/assets/pdfjs/images/altText_warning.svg new file mode 100644 index 0000000..03014ce --- /dev/null +++ b/public/assets/pdfjs/images/altText_warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/annotation-check.svg b/public/assets/pdfjs/images/annotation-check.svg new file mode 100644 index 0000000..71cd16d --- /dev/null +++ b/public/assets/pdfjs/images/annotation-check.svg @@ -0,0 +1,11 @@ + + + + diff --git a/public/assets/pdfjs/images/annotation-comment.svg b/public/assets/pdfjs/images/annotation-comment.svg new file mode 100644 index 0000000..86f1f17 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-comment.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/public/assets/pdfjs/images/annotation-help.svg b/public/assets/pdfjs/images/annotation-help.svg new file mode 100644 index 0000000..00938fe --- /dev/null +++ b/public/assets/pdfjs/images/annotation-help.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/public/assets/pdfjs/images/annotation-insert.svg b/public/assets/pdfjs/images/annotation-insert.svg new file mode 100644 index 0000000..519ef68 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-insert.svg @@ -0,0 +1,10 @@ + + + + diff --git a/public/assets/pdfjs/images/annotation-key.svg b/public/assets/pdfjs/images/annotation-key.svg new file mode 100644 index 0000000..8d09d53 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-key.svg @@ -0,0 +1,11 @@ + + + + diff --git a/public/assets/pdfjs/images/annotation-newparagraph.svg b/public/assets/pdfjs/images/annotation-newparagraph.svg new file mode 100644 index 0000000..38d2497 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-newparagraph.svg @@ -0,0 +1,11 @@ + + + + diff --git a/public/assets/pdfjs/images/annotation-noicon.svg b/public/assets/pdfjs/images/annotation-noicon.svg new file mode 100644 index 0000000..c07d108 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-noicon.svg @@ -0,0 +1,7 @@ + + + diff --git a/public/assets/pdfjs/images/annotation-note.svg b/public/assets/pdfjs/images/annotation-note.svg new file mode 100644 index 0000000..7017365 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-note.svg @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/public/assets/pdfjs/images/annotation-paperclip.svg b/public/assets/pdfjs/images/annotation-paperclip.svg new file mode 100644 index 0000000..2bed225 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-paperclip.svg @@ -0,0 +1,6 @@ + + + + diff --git a/public/assets/pdfjs/images/annotation-paragraph.svg b/public/assets/pdfjs/images/annotation-paragraph.svg new file mode 100644 index 0000000..6ae5212 --- /dev/null +++ b/public/assets/pdfjs/images/annotation-paragraph.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/public/assets/pdfjs/images/annotation-pushpin.svg b/public/assets/pdfjs/images/annotation-pushpin.svg new file mode 100644 index 0000000..6e0896c --- /dev/null +++ b/public/assets/pdfjs/images/annotation-pushpin.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/public/assets/pdfjs/images/cursor-editorFreeHighlight.svg b/public/assets/pdfjs/images/cursor-editorFreeHighlight.svg new file mode 100644 index 0000000..513f6bd --- /dev/null +++ b/public/assets/pdfjs/images/cursor-editorFreeHighlight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/pdfjs/images/cursor-editorFreeText.svg b/public/assets/pdfjs/images/cursor-editorFreeText.svg new file mode 100644 index 0000000..de2838e --- /dev/null +++ b/public/assets/pdfjs/images/cursor-editorFreeText.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/cursor-editorInk.svg b/public/assets/pdfjs/images/cursor-editorInk.svg new file mode 100644 index 0000000..1dadb5c --- /dev/null +++ b/public/assets/pdfjs/images/cursor-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/pdfjs/images/cursor-editorTextHighlight.svg b/public/assets/pdfjs/images/cursor-editorTextHighlight.svg new file mode 100644 index 0000000..800340c --- /dev/null +++ b/public/assets/pdfjs/images/cursor-editorTextHighlight.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/assets/pdfjs/images/editor-toolbar-delete.svg b/public/assets/pdfjs/images/editor-toolbar-delete.svg new file mode 100644 index 0000000..f84520d --- /dev/null +++ b/public/assets/pdfjs/images/editor-toolbar-delete.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/public/assets/pdfjs/images/findbarButton-next.svg b/public/assets/pdfjs/images/findbarButton-next.svg new file mode 100644 index 0000000..8cb39be --- /dev/null +++ b/public/assets/pdfjs/images/findbarButton-next.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/findbarButton-previous.svg b/public/assets/pdfjs/images/findbarButton-previous.svg new file mode 100644 index 0000000..b610879 --- /dev/null +++ b/public/assets/pdfjs/images/findbarButton-previous.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/gv-toolbarButton-download.svg b/public/assets/pdfjs/images/gv-toolbarButton-download.svg new file mode 100644 index 0000000..d56cf3c --- /dev/null +++ b/public/assets/pdfjs/images/gv-toolbarButton-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/loading-icon.gif b/public/assets/pdfjs/images/loading-icon.gif new file mode 100644 index 0000000..1c72ebb Binary files /dev/null and b/public/assets/pdfjs/images/loading-icon.gif differ diff --git a/public/assets/pdfjs/images/loading.svg b/public/assets/pdfjs/images/loading.svg new file mode 100644 index 0000000..0a15ff6 --- /dev/null +++ b/public/assets/pdfjs/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/pdfjs/images/messageBar_closingButton.svg b/public/assets/pdfjs/images/messageBar_closingButton.svg new file mode 100644 index 0000000..8a40715 --- /dev/null +++ b/public/assets/pdfjs/images/messageBar_closingButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/messageBar_warning.svg b/public/assets/pdfjs/images/messageBar_warning.svg new file mode 100644 index 0000000..011cfcf --- /dev/null +++ b/public/assets/pdfjs/images/messageBar_warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-documentProperties.svg b/public/assets/pdfjs/images/secondaryToolbarButton-documentProperties.svg new file mode 100644 index 0000000..dd3917b --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-documentProperties.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-firstPage.svg b/public/assets/pdfjs/images/secondaryToolbarButton-firstPage.svg new file mode 100644 index 0000000..f5c917f --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-firstPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-handTool.svg b/public/assets/pdfjs/images/secondaryToolbarButton-handTool.svg new file mode 100644 index 0000000..b7073b5 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-handTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-lastPage.svg b/public/assets/pdfjs/images/secondaryToolbarButton-lastPage.svg new file mode 100644 index 0000000..c04f650 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-lastPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-rotateCcw.svg b/public/assets/pdfjs/images/secondaryToolbarButton-rotateCcw.svg new file mode 100644 index 0000000..da73a1b --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-rotateCcw.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-rotateCw.svg b/public/assets/pdfjs/images/secondaryToolbarButton-rotateCw.svg new file mode 100644 index 0000000..c41ce73 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-rotateCw.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-scrollHorizontal.svg b/public/assets/pdfjs/images/secondaryToolbarButton-scrollHorizontal.svg new file mode 100644 index 0000000..fb440b9 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-scrollHorizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-scrollPage.svg b/public/assets/pdfjs/images/secondaryToolbarButton-scrollPage.svg new file mode 100644 index 0000000..64a9f50 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-scrollPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-scrollVertical.svg b/public/assets/pdfjs/images/secondaryToolbarButton-scrollVertical.svg new file mode 100644 index 0000000..dc7e805 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-scrollVertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-scrollWrapped.svg b/public/assets/pdfjs/images/secondaryToolbarButton-scrollWrapped.svg new file mode 100644 index 0000000..75fe26b --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-scrollWrapped.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-selectTool.svg b/public/assets/pdfjs/images/secondaryToolbarButton-selectTool.svg new file mode 100644 index 0000000..94d5141 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-selectTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-spreadEven.svg b/public/assets/pdfjs/images/secondaryToolbarButton-spreadEven.svg new file mode 100644 index 0000000..ce201e3 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-spreadEven.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-spreadNone.svg b/public/assets/pdfjs/images/secondaryToolbarButton-spreadNone.svg new file mode 100644 index 0000000..e8d487f --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-spreadNone.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/secondaryToolbarButton-spreadOdd.svg b/public/assets/pdfjs/images/secondaryToolbarButton-spreadOdd.svg new file mode 100644 index 0000000..9211a42 --- /dev/null +++ b/public/assets/pdfjs/images/secondaryToolbarButton-spreadOdd.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-bookmark.svg b/public/assets/pdfjs/images/toolbarButton-bookmark.svg new file mode 100644 index 0000000..c4c37c9 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-currentOutlineItem.svg b/public/assets/pdfjs/images/toolbarButton-currentOutlineItem.svg new file mode 100644 index 0000000..01e6762 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-currentOutlineItem.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-download.svg b/public/assets/pdfjs/images/toolbarButton-download.svg new file mode 100644 index 0000000..e2e850a --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/pdfjs/images/toolbarButton-editorFreeText.svg b/public/assets/pdfjs/images/toolbarButton-editorFreeText.svg new file mode 100644 index 0000000..13a67bd --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-editorFreeText.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/pdfjs/images/toolbarButton-editorHighlight.svg b/public/assets/pdfjs/images/toolbarButton-editorHighlight.svg new file mode 100644 index 0000000..b3cd7fd --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-editorHighlight.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/assets/pdfjs/images/toolbarButton-editorInk.svg b/public/assets/pdfjs/images/toolbarButton-editorInk.svg new file mode 100644 index 0000000..b579eec --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/pdfjs/images/toolbarButton-editorStamp.svg b/public/assets/pdfjs/images/toolbarButton-editorStamp.svg new file mode 100644 index 0000000..a1fef49 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-editorStamp.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/assets/pdfjs/images/toolbarButton-menuArrow.svg b/public/assets/pdfjs/images/toolbarButton-menuArrow.svg new file mode 100644 index 0000000..82ffeaa --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-menuArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-openFile.svg b/public/assets/pdfjs/images/toolbarButton-openFile.svg new file mode 100644 index 0000000..e773781 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-openFile.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-pageDown.svg b/public/assets/pdfjs/images/toolbarButton-pageDown.svg new file mode 100644 index 0000000..1fc12e7 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-pageDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-pageUp.svg b/public/assets/pdfjs/images/toolbarButton-pageUp.svg new file mode 100644 index 0000000..0936b9a --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-pageUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-presentationMode.svg b/public/assets/pdfjs/images/toolbarButton-presentationMode.svg new file mode 100644 index 0000000..901d567 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-presentationMode.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-print.svg b/public/assets/pdfjs/images/toolbarButton-print.svg new file mode 100644 index 0000000..97a3904 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-print.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-search.svg b/public/assets/pdfjs/images/toolbarButton-search.svg new file mode 100644 index 0000000..0cc7ae2 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-secondaryToolbarToggle.svg b/public/assets/pdfjs/images/toolbarButton-secondaryToolbarToggle.svg new file mode 100644 index 0000000..cace863 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-secondaryToolbarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-sidebarToggle.svg b/public/assets/pdfjs/images/toolbarButton-sidebarToggle.svg new file mode 100644 index 0000000..1d8d0e4 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-sidebarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-viewAttachments.svg b/public/assets/pdfjs/images/toolbarButton-viewAttachments.svg new file mode 100644 index 0000000..ab73f6e --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-viewAttachments.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-viewLayers.svg b/public/assets/pdfjs/images/toolbarButton-viewLayers.svg new file mode 100644 index 0000000..1d72668 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-viewLayers.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-viewOutline.svg b/public/assets/pdfjs/images/toolbarButton-viewOutline.svg new file mode 100644 index 0000000..7ed1bd9 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-viewOutline.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-viewThumbnail.svg b/public/assets/pdfjs/images/toolbarButton-viewThumbnail.svg new file mode 100644 index 0000000..040d123 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-viewThumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-zoomIn.svg b/public/assets/pdfjs/images/toolbarButton-zoomIn.svg new file mode 100644 index 0000000..30ec51a --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-zoomIn.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/toolbarButton-zoomOut.svg b/public/assets/pdfjs/images/toolbarButton-zoomOut.svg new file mode 100644 index 0000000..f273b59 --- /dev/null +++ b/public/assets/pdfjs/images/toolbarButton-zoomOut.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/pdfjs/images/treeitem-collapsed.svg b/public/assets/pdfjs/images/treeitem-collapsed.svg new file mode 100644 index 0000000..831cddf --- /dev/null +++ b/public/assets/pdfjs/images/treeitem-collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/pdfjs/images/treeitem-expanded.svg b/public/assets/pdfjs/images/treeitem-expanded.svg new file mode 100644 index 0000000..2d45f0c --- /dev/null +++ b/public/assets/pdfjs/images/treeitem-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/pdfjs/locale/ach/viewer.ftl b/public/assets/pdfjs/locale/ach/viewer.ftl new file mode 100644 index 0000000..36769b7 --- /dev/null +++ b/public/assets/pdfjs/locale/ach/viewer.ftl @@ -0,0 +1,225 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pot buk mukato +pdfjs-previous-button-label = Mukato +pdfjs-next-button = + .title = Pot buk malubo +pdfjs-next-button-label = Malubo +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pot buk +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = pi { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } me { $pagesCount }) +pdfjs-zoom-out-button = + .title = Jwik Matidi +pdfjs-zoom-out-button-label = Jwik Matidi +pdfjs-zoom-in-button = + .title = Kwot Madit +pdfjs-zoom-in-button-label = Kwot Madit +pdfjs-zoom-select = + .title = Kwoti +pdfjs-presentation-mode-button = + .title = Lokke i kit me tyer +pdfjs-presentation-mode-button-label = Kit me tyer +pdfjs-open-file-button = + .title = Yab Pwail +pdfjs-open-file-button-label = Yab +pdfjs-print-button = + .title = Go +pdfjs-print-button-label = Go + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Gintic +pdfjs-tools-button-label = Gintic +pdfjs-first-page-button = + .title = Cit i pot buk mukwongo +pdfjs-first-page-button-label = Cit i pot buk mukwongo +pdfjs-last-page-button = + .title = Cit i pot buk magiko +pdfjs-last-page-button-label = Cit i pot buk magiko +pdfjs-page-rotate-cw-button = + .title = Wire i tung lacuc +pdfjs-page-rotate-cw-button-label = Wire i tung lacuc +pdfjs-page-rotate-ccw-button = + .title = Wire i tung lacam +pdfjs-page-rotate-ccw-button-label = Wire i tung lacam +pdfjs-cursor-text-select-tool-button = + .title = Cak gitic me yero coc +pdfjs-cursor-text-select-tool-button-label = Gitic me yero coc +pdfjs-cursor-hand-tool-button = + .title = Cak gitic me cing +pdfjs-cursor-hand-tool-button-label = Gitic cing + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Jami me gin acoya… +pdfjs-document-properties-button-label = Jami me gin acoya… +pdfjs-document-properties-file-name = Nying pwail: +pdfjs-document-properties-file-size = Dit pa pwail: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Wiye: +pdfjs-document-properties-author = Ngat mucoyo: +pdfjs-document-properties-subject = Subjek: +pdfjs-document-properties-keywords = Lok mapire tek: +pdfjs-document-properties-creation-date = Nino dwe me cwec: +pdfjs-document-properties-modification-date = Nino dwe me yub: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Lacwec: +pdfjs-document-properties-producer = Layub PDF: +pdfjs-document-properties-version = Kit PDF: +pdfjs-document-properties-page-count = Kwan me pot buk: +pdfjs-document-properties-page-size = Dit pa potbuk: +pdfjs-document-properties-page-size-unit-inches = i +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = atir +pdfjs-document-properties-page-size-orientation-landscape = arii +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Waraga +pdfjs-document-properties-page-size-name-legal = Cik + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = Eyo +pdfjs-document-properties-linearized-no = Pe +pdfjs-document-properties-close-button = Lor + +## Print + +pdfjs-print-progress-message = Yubo coc me agoya… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Juki +pdfjs-printing-not-supported = Ciko: Layeny ma pe teno goyo liweng. +pdfjs-printing-not-ready = Ciko: PDF pe ocane weng me agoya. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Lok gintic ma inget +pdfjs-toggle-sidebar-button-label = Lok gintic ma inget +pdfjs-document-outline-button = + .title = Nyut Wiyewiye me Gin acoya (dii-kiryo me yaro/kano jami weng) +pdfjs-document-outline-button-label = Pek pa gin acoya +pdfjs-attachments-button = + .title = Nyut twec +pdfjs-attachments-button-label = Twec +pdfjs-thumbs-button = + .title = Nyut cal +pdfjs-thumbs-button-label = Cal +pdfjs-findbar-button = + .title = Nong iye gin acoya +pdfjs-findbar-button-label = Nong + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pot buk { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Cal me pot buk { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Nong + .placeholder = Nong i dokumen… +pdfjs-find-previous-button = + .title = Nong timme pa lok mukato +pdfjs-find-previous-button-label = Mukato +pdfjs-find-next-button = + .title = Nong timme pa lok malubo +pdfjs-find-next-button-label = Malubo +pdfjs-find-highlight-checkbox = Ket Lanyut I Weng +pdfjs-find-match-case-checkbox-label = Lok marwate +pdfjs-find-reached-top = Oo iwi gin acoya, omede ki i tere +pdfjs-find-reached-bottom = Oo i agiki me gin acoya, omede ki iwiye +pdfjs-find-not-found = Lok pe ononge + +## Predefined zoom values + +pdfjs-page-scale-width = Lac me iye pot buk +pdfjs-page-scale-fit = Porre me pot buk +pdfjs-page-scale-auto = Kwot pire kene +pdfjs-page-scale-actual = Dite kikome +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Bal otime kun cano PDF. +pdfjs-invalid-file-error = Pwail me PDF ma pe atir onyo obale woko. +pdfjs-missing-file-error = Pwail me PDF tye ka rem. +pdfjs-unexpected-response-error = Lagam mape kigeno pa lapok tic. +pdfjs-rendering-error = Bal otime i kare me nyuto pot buk. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Lok angea manok] + +## Password + +pdfjs-password-label = Ket mung me donyo me yabo pwail me PDF man. +pdfjs-password-invalid = Mung me donyo pe atir. Tim ber i tem doki. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Juki +pdfjs-web-fonts-disabled = Kijuko dit pa coc me kakube woko: pe romo tic ki dit pa coc me PDF ma kiketo i kine. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/af/viewer.ftl b/public/assets/pdfjs/locale/af/viewer.ftl new file mode 100644 index 0000000..7c4346f --- /dev/null +++ b/public/assets/pdfjs/locale/af/viewer.ftl @@ -0,0 +1,212 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Vorige bladsy +pdfjs-previous-button-label = Vorige +pdfjs-next-button = + .title = Volgende bladsy +pdfjs-next-button-label = Volgende +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Bladsy +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = van { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } van { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoem uit +pdfjs-zoom-out-button-label = Zoem uit +pdfjs-zoom-in-button = + .title = Zoem in +pdfjs-zoom-in-button-label = Zoem in +pdfjs-zoom-select = + .title = Zoem +pdfjs-presentation-mode-button = + .title = Wissel na voorleggingsmodus +pdfjs-presentation-mode-button-label = Voorleggingsmodus +pdfjs-open-file-button = + .title = Open lêer +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Druk +pdfjs-print-button-label = Druk + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Nutsgoed +pdfjs-tools-button-label = Nutsgoed +pdfjs-first-page-button = + .title = Gaan na eerste bladsy +pdfjs-first-page-button-label = Gaan na eerste bladsy +pdfjs-last-page-button = + .title = Gaan na laaste bladsy +pdfjs-last-page-button-label = Gaan na laaste bladsy +pdfjs-page-rotate-cw-button = + .title = Roteer kloksgewys +pdfjs-page-rotate-cw-button-label = Roteer kloksgewys +pdfjs-page-rotate-ccw-button = + .title = Roteer anti-kloksgewys +pdfjs-page-rotate-ccw-button-label = Roteer anti-kloksgewys +pdfjs-cursor-text-select-tool-button = + .title = Aktiveer gereedskap om teks te merk +pdfjs-cursor-text-select-tool-button-label = Teksmerkgereedskap +pdfjs-cursor-hand-tool-button = + .title = Aktiveer handjie +pdfjs-cursor-hand-tool-button-label = Handjie + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumenteienskappe… +pdfjs-document-properties-button-label = Dokumenteienskappe… +pdfjs-document-properties-file-name = Lêernaam: +pdfjs-document-properties-file-size = Lêergrootte: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kG ({ $size_b } grepe) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MG ({ $size_b } grepe) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Outeur: +pdfjs-document-properties-subject = Onderwerp: +pdfjs-document-properties-keywords = Sleutelwoorde: +pdfjs-document-properties-creation-date = Skeppingsdatum: +pdfjs-document-properties-modification-date = Wysigingsdatum: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Skepper: +pdfjs-document-properties-producer = PDF-vervaardiger: +pdfjs-document-properties-version = PDF-weergawe: +pdfjs-document-properties-page-count = Aantal bladsye: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = Sluit + +## Print + +pdfjs-print-progress-message = Berei tans dokument voor om te druk… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Kanselleer +pdfjs-printing-not-supported = Waarskuwing: Dié blaaier ondersteun nie drukwerk ten volle nie. +pdfjs-printing-not-ready = Waarskuwing: Die PDF is nog nie volledig gelaai vir drukwerk nie. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Sypaneel aan/af +pdfjs-toggle-sidebar-button-label = Sypaneel aan/af +pdfjs-document-outline-button = + .title = Wys dokumentskema (dubbelklik om alle items oop/toe te vou) +pdfjs-document-outline-button-label = Dokumentoorsig +pdfjs-attachments-button = + .title = Wys aanhegsels +pdfjs-attachments-button-label = Aanhegsels +pdfjs-thumbs-button = + .title = Wys duimnaels +pdfjs-thumbs-button-label = Duimnaels +pdfjs-findbar-button = + .title = Soek in dokument +pdfjs-findbar-button-label = Vind + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Bladsy { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Duimnael van bladsy { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Vind + .placeholder = Soek in dokument… +pdfjs-find-previous-button = + .title = Vind die vorige voorkoms van die frase +pdfjs-find-previous-button-label = Vorige +pdfjs-find-next-button = + .title = Vind die volgende voorkoms van die frase +pdfjs-find-next-button-label = Volgende +pdfjs-find-highlight-checkbox = Verlig almal +pdfjs-find-match-case-checkbox-label = Kassensitief +pdfjs-find-reached-top = Bokant van dokument is bereik; gaan voort van onder af +pdfjs-find-reached-bottom = Einde van dokument is bereik; gaan voort van bo af +pdfjs-find-not-found = Frase nie gevind nie + +## Predefined zoom values + +pdfjs-page-scale-width = Bladsywydte +pdfjs-page-scale-fit = Pas bladsy +pdfjs-page-scale-auto = Outomatiese zoem +pdfjs-page-scale-actual = Werklike grootte +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = 'n Fout het voorgekom met die laai van die PDF. +pdfjs-invalid-file-error = Ongeldige of korrupte PDF-lêer. +pdfjs-missing-file-error = PDF-lêer is weg. +pdfjs-unexpected-response-error = Onverwagse antwoord van bediener. +pdfjs-rendering-error = 'n Fout het voorgekom toe die bladsy weergegee is. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-annotasie] + +## Password + +pdfjs-password-label = Gee die wagwoord om dié PDF-lêer mee te open. +pdfjs-password-invalid = Ongeldige wagwoord. Probeer gerus weer. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Kanselleer +pdfjs-web-fonts-disabled = Webfonte is gedeaktiveer: kan nie PDF-fonte wat ingebed is, gebruik nie. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/an/viewer.ftl b/public/assets/pdfjs/locale/an/viewer.ftl new file mode 100644 index 0000000..6733147 --- /dev/null +++ b/public/assets/pdfjs/locale/an/viewer.ftl @@ -0,0 +1,257 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pachina anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Pachina siguient +pdfjs-next-button-label = Siguient +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pachina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Achiquir +pdfjs-zoom-out-button-label = Achiquir +pdfjs-zoom-in-button = + .title = Agrandir +pdfjs-zoom-in-button-label = Agrandir +pdfjs-zoom-select = + .title = Grandaria +pdfjs-presentation-mode-button = + .title = Cambear t'o modo de presentación +pdfjs-presentation-mode-button-label = Modo de presentación +pdfjs-open-file-button = + .title = Ubrir o fichero +pdfjs-open-file-button-label = Ubrir +pdfjs-print-button = + .title = Imprentar +pdfjs-print-button-label = Imprentar + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ferramientas +pdfjs-tools-button-label = Ferramientas +pdfjs-first-page-button = + .title = Ir ta la primer pachina +pdfjs-first-page-button-label = Ir ta la primer pachina +pdfjs-last-page-button = + .title = Ir ta la zaguer pachina +pdfjs-last-page-button-label = Ir ta la zaguer pachina +pdfjs-page-rotate-cw-button = + .title = Chirar enta la dreita +pdfjs-page-rotate-cw-button-label = Chira enta la dreita +pdfjs-page-rotate-ccw-button = + .title = Chirar enta la zurda +pdfjs-page-rotate-ccw-button-label = Chirar enta la zurda +pdfjs-cursor-text-select-tool-button = + .title = Activar la ferramienta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Ferramienta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Activar la ferramienta man +pdfjs-cursor-hand-tool-button-label = Ferramienta man +pdfjs-scroll-vertical-button = + .title = Usar lo desplazamiento vertical +pdfjs-scroll-vertical-button-label = Desplazamiento vertical +pdfjs-scroll-horizontal-button = + .title = Usar lo desplazamiento horizontal +pdfjs-scroll-horizontal-button-label = Desplazamiento horizontal +pdfjs-scroll-wrapped-button = + .title = Activaar lo desplazamiento contino +pdfjs-scroll-wrapped-button-label = Desplazamiento contino +pdfjs-spread-none-button = + .title = No unir vistas de pachinas +pdfjs-spread-none-button-label = Una pachina nomás +pdfjs-spread-odd-button = + .title = Mostrar vista de pachinas, con as impars a la zurda +pdfjs-spread-odd-button-label = Doble pachina, impar a la zurda +pdfjs-spread-even-button = + .title = Amostrar vista de pachinas, con as pars a la zurda +pdfjs-spread-even-button-label = Doble pachina, para a la zurda + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedatz d'o documento... +pdfjs-document-properties-button-label = Propiedatz d'o documento... +pdfjs-document-properties-file-name = Nombre de fichero: +pdfjs-document-properties-file-size = Grandaria d'o fichero: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titol: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Afer: +pdfjs-document-properties-keywords = Parolas clau: +pdfjs-document-properties-creation-date = Calendata de creyación: +pdfjs-document-properties-modification-date = Calendata de modificación: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creyador: +pdfjs-document-properties-producer = Creyador de PDF: +pdfjs-document-properties-version = Versión de PDF: +pdfjs-document-properties-page-count = Numero de pachinas: +pdfjs-document-properties-page-size = Mida de pachina: +pdfjs-document-properties-page-size-unit-inches = pulgadas +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } x { $height } { $unit } { $orientation } +pdfjs-document-properties-page-size-dimension-name-string = { $width } x { $height } { $unit } { $name }, { $orientation } + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web rapida: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Zarrar + +## Print + +pdfjs-print-progress-message = Se ye preparando la documentación pa imprentar… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Pare cuenta: Iste navegador no maneya totalment as impresions. +pdfjs-printing-not-ready = Aviso: Encara no se ha cargau completament o PDF ta imprentar-lo. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Amostrar u amagar a barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Cambiar barra lateral (lo documento contiene esquema/adchuntos/capas) +pdfjs-toggle-sidebar-button-label = Amostrar a barra lateral +pdfjs-document-outline-button = + .title = Amostrar esquema d'o documento (fer doble clic pa expandir/compactar totz los items) +pdfjs-document-outline-button-label = Esquema d'o documento +pdfjs-attachments-button = + .title = Amostrar os adchuntos +pdfjs-attachments-button-label = Adchuntos +pdfjs-layers-button = + .title = Amostrar capas (doble clic para reiniciar totas las capas a lo estau per defecto) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Amostrar as miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-findbar-button = + .title = Trobar en o documento +pdfjs-findbar-button-label = Trobar +pdfjs-additional-layers = Capas adicionals + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pachina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura d'a pachina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Trobar + .placeholder = Trobar en o documento… +pdfjs-find-previous-button = + .title = Trobar l'anterior coincidencia d'a frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Trobar a siguient coincidencia d'a frase +pdfjs-find-next-button-label = Siguient +pdfjs-find-highlight-checkbox = Resaltar-lo tot +pdfjs-find-match-case-checkbox-label = Coincidencia de mayusclas/minusclas +pdfjs-find-entire-word-checkbox-label = Parolas completas +pdfjs-find-reached-top = S'ha plegau a l'inicio d'o documento, se contina dende baixo +pdfjs-find-reached-bottom = S'ha plegau a la fin d'o documento, se contina dende alto +pdfjs-find-not-found = No s'ha trobau a frase + +## Predefined zoom values + +pdfjs-page-scale-width = Amplaria d'a pachina +pdfjs-page-scale-fit = Achuste d'a pachina +pdfjs-page-scale-auto = Grandaria automatica +pdfjs-page-scale-actual = Grandaria actual +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = S'ha produciu una error en cargar o PDF. +pdfjs-invalid-file-error = O PDF no ye valido u ye estorbau. +pdfjs-missing-file-error = No i ha fichero PDF. +pdfjs-unexpected-response-error = Respuesta a lo servicio inasperada. +pdfjs-rendering-error = Ha ocurriu una error en renderizar a pachina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotación { $type }] + +## Password + +pdfjs-password-label = Introduzca a clau ta ubrir iste fichero PDF. +pdfjs-password-invalid = Clau invalida. Torna a intentar-lo. +pdfjs-password-ok-button = Acceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = As fuents web son desactivadas: no se puet incrustar fichers PDF. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ar/viewer.ftl b/public/assets/pdfjs/locale/ar/viewer.ftl new file mode 100644 index 0000000..8d14767 --- /dev/null +++ b/public/assets/pdfjs/locale/ar/viewer.ftl @@ -0,0 +1,425 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = الصفحة السابقة +pdfjs-previous-button-label = السابقة +pdfjs-next-button = + .title = الصفحة التالية +pdfjs-next-button-label = التالية +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = صفحة +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = من { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } من { $pagesCount }) +pdfjs-zoom-out-button = + .title = بعّد +pdfjs-zoom-out-button-label = بعّد +pdfjs-zoom-in-button = + .title = قرّب +pdfjs-zoom-in-button-label = قرّب +pdfjs-zoom-select = + .title = التقريب +pdfjs-presentation-mode-button = + .title = انتقل لوضع العرض التقديمي +pdfjs-presentation-mode-button-label = وضع العرض التقديمي +pdfjs-open-file-button = + .title = افتح ملفًا +pdfjs-open-file-button-label = افتح +pdfjs-print-button = + .title = اطبع +pdfjs-print-button-label = اطبع +pdfjs-save-button = + .title = احفظ +pdfjs-save-button-label = احفظ +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = نزّل +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = نزّل +pdfjs-bookmark-button = + .title = الصفحة الحالية (عرض URL من الصفحة الحالية) +pdfjs-bookmark-button-label = الصفحة الحالية + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = الأدوات +pdfjs-tools-button-label = الأدوات +pdfjs-first-page-button = + .title = انتقل إلى الصفحة الأولى +pdfjs-first-page-button-label = انتقل إلى الصفحة الأولى +pdfjs-last-page-button = + .title = انتقل إلى الصفحة الأخيرة +pdfjs-last-page-button-label = انتقل إلى الصفحة الأخيرة +pdfjs-page-rotate-cw-button = + .title = أدر باتجاه عقارب الساعة +pdfjs-page-rotate-cw-button-label = أدر باتجاه عقارب الساعة +pdfjs-page-rotate-ccw-button = + .title = أدر بعكس اتجاه عقارب الساعة +pdfjs-page-rotate-ccw-button-label = أدر بعكس اتجاه عقارب الساعة +pdfjs-cursor-text-select-tool-button = + .title = فعّل أداة اختيار النص +pdfjs-cursor-text-select-tool-button-label = أداة اختيار النص +pdfjs-cursor-hand-tool-button = + .title = فعّل أداة اليد +pdfjs-cursor-hand-tool-button-label = أداة اليد +pdfjs-scroll-page-button = + .title = استخدم تمرير الصفحة +pdfjs-scroll-page-button-label = تمرير الصفحة +pdfjs-scroll-vertical-button = + .title = استخدم التمرير الرأسي +pdfjs-scroll-vertical-button-label = التمرير الرأسي +pdfjs-scroll-horizontal-button = + .title = استخدم التمرير الأفقي +pdfjs-scroll-horizontal-button-label = التمرير الأفقي +pdfjs-scroll-wrapped-button = + .title = استخدم التمرير الملتف +pdfjs-scroll-wrapped-button-label = التمرير الملتف +pdfjs-spread-none-button = + .title = لا تدمج هوامش الصفحات مع بعضها البعض +pdfjs-spread-none-button-label = بلا هوامش +pdfjs-spread-odd-button = + .title = ادمج هوامش الصفحات الفردية +pdfjs-spread-odd-button-label = هوامش الصفحات الفردية +pdfjs-spread-even-button = + .title = ادمج هوامش الصفحات الزوجية +pdfjs-spread-even-button-label = هوامش الصفحات الزوجية + +## Document properties dialog + +pdfjs-document-properties-button = + .title = خصائص المستند… +pdfjs-document-properties-button-label = خصائص المستند… +pdfjs-document-properties-file-name = اسم الملف: +pdfjs-document-properties-file-size = حجم الملف: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } ك.بايت ({ $size_b } بايت) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } م.بايت ({ $size_b } بايت) +pdfjs-document-properties-title = العنوان: +pdfjs-document-properties-author = المؤلف: +pdfjs-document-properties-subject = الموضوع: +pdfjs-document-properties-keywords = الكلمات الأساسية: +pdfjs-document-properties-creation-date = تاريخ الإنشاء: +pdfjs-document-properties-modification-date = تاريخ التعديل: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }، { $time } +pdfjs-document-properties-creator = المنشئ: +pdfjs-document-properties-producer = منتج PDF: +pdfjs-document-properties-version = إصدارة PDF: +pdfjs-document-properties-page-count = عدد الصفحات: +pdfjs-document-properties-page-size = مقاس الورقة: +pdfjs-document-properties-page-size-unit-inches = بوصة +pdfjs-document-properties-page-size-unit-millimeters = ملم +pdfjs-document-properties-page-size-orientation-portrait = طوليّ +pdfjs-document-properties-page-size-orientation-landscape = عرضيّ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = خطاب +pdfjs-document-properties-page-size-name-legal = قانونيّ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = ‏{ $width } × ‏{ $height } ‏{ $unit } (‏{ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = ‏{ $width } × ‏{ $height } ‏{ $unit } (‏{ $name }، { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = العرض السريع عبر الوِب: +pdfjs-document-properties-linearized-yes = نعم +pdfjs-document-properties-linearized-no = لا +pdfjs-document-properties-close-button = أغلق + +## Print + +pdfjs-print-progress-message = يُحضّر المستند للطباعة… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }٪ +pdfjs-print-progress-close-button = ألغِ +pdfjs-printing-not-supported = تحذير: لا يدعم هذا المتصفح الطباعة بشكل كامل. +pdfjs-printing-not-ready = تحذير: ملف PDF لم يُحمّل كاملًا للطباعة. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = بدّل ظهور الشريط الجانبي +pdfjs-toggle-sidebar-notification-button = + .title = بدّل ظهور الشريط الجانبي (يحتوي المستند على مخطط أو مرفقات أو طبقات) +pdfjs-toggle-sidebar-button-label = بدّل ظهور الشريط الجانبي +pdfjs-document-outline-button = + .title = اعرض فهرس المستند (نقر مزدوج لتمديد أو تقليص كل العناصر) +pdfjs-document-outline-button-label = مخطط المستند +pdfjs-attachments-button = + .title = اعرض المرفقات +pdfjs-attachments-button-label = المُرفقات +pdfjs-layers-button = + .title = اعرض الطبقات (انقر مرتين لتصفير كل الطبقات إلى الحالة المبدئية) +pdfjs-layers-button-label = ‏‏الطبقات +pdfjs-thumbs-button = + .title = اعرض مُصغرات +pdfjs-thumbs-button-label = مُصغّرات +pdfjs-current-outline-item-button = + .title = ابحث عن عنصر المخطّط التفصيلي الحالي +pdfjs-current-outline-item-button-label = عنصر المخطّط التفصيلي الحالي +pdfjs-findbar-button = + .title = ابحث في المستند +pdfjs-findbar-button-label = ابحث +pdfjs-additional-layers = الطبقات الإضافية + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = صفحة { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = مصغّرة صفحة { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ابحث + .placeholder = ابحث في المستند… +pdfjs-find-previous-button = + .title = ابحث عن التّواجد السّابق للعبارة +pdfjs-find-previous-button-label = السابق +pdfjs-find-next-button = + .title = ابحث عن التّواجد التّالي للعبارة +pdfjs-find-next-button-label = التالي +pdfjs-find-highlight-checkbox = أبرِز الكل +pdfjs-find-match-case-checkbox-label = طابق حالة الأحرف +pdfjs-find-match-diacritics-checkbox-label = طابِق الحركات +pdfjs-find-entire-word-checkbox-label = كلمات كاملة +pdfjs-find-reached-top = تابعت من الأسفل بعدما وصلت إلى بداية المستند +pdfjs-find-reached-bottom = تابعت من الأعلى بعدما وصلت إلى نهاية المستند +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [zero] لا مطابقة + [one] { $current } من أصل { $total } مطابقة + [two] { $current } من أصل { $total } مطابقة + [few] { $current } من أصل { $total } مطابقة + [many] { $current } من أصل { $total } مطابقة + *[other] { $current } من أصل { $total } مطابقة + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [zero] { $limit } مطابقة + [one] أكثر من { $limit } مطابقة + [two] أكثر من { $limit } مطابقة + [few] أكثر من { $limit } مطابقة + [many] أكثر من { $limit } مطابقة + *[other] أكثر من { $limit } مطابقات + } +pdfjs-find-not-found = لا وجود للعبارة + +## Predefined zoom values + +pdfjs-page-scale-width = عرض الصفحة +pdfjs-page-scale-fit = ملائمة الصفحة +pdfjs-page-scale-auto = تقريب تلقائي +pdfjs-page-scale-actual = الحجم الفعلي +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }٪ + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = صفحة { $page } + +## Loading indicator messages + +pdfjs-loading-error = حدث عطل أثناء تحميل ملف PDF. +pdfjs-invalid-file-error = ملف PDF تالف أو غير صحيح. +pdfjs-missing-file-error = ملف PDF غير موجود. +pdfjs-unexpected-response-error = استجابة خادوم غير متوقعة. +pdfjs-rendering-error = حدث خطأ أثناء عرض الصفحة. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }، { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [تعليق { $type }] + +## Password + +pdfjs-password-label = أدخل لكلمة السر لفتح هذا الملف. +pdfjs-password-invalid = كلمة سر خطأ. من فضلك أعد المحاولة. +pdfjs-password-ok-button = حسنا +pdfjs-password-cancel-button = ألغِ +pdfjs-web-fonts-disabled = خطوط الوب مُعطّلة: تعذّر استخدام خطوط PDF المُضمّنة. + +## Editing + +pdfjs-editor-free-text-button = + .title = نص +pdfjs-editor-free-text-button-label = نص +pdfjs-editor-ink-button = + .title = ارسم +pdfjs-editor-ink-button-label = ارسم +pdfjs-editor-stamp-button = + .title = أضِف أو حرّر الصور +pdfjs-editor-stamp-button-label = أضِف أو حرّر الصور +pdfjs-editor-highlight-button = + .title = أبرِز +pdfjs-editor-highlight-button-label = أبرِز +pdfjs-highlight-floating-button1 = + .title = أبرِز + .aria-label = أبرِز +pdfjs-highlight-floating-button-label = أبرِز + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = أزِل الرسم +pdfjs-editor-remove-freetext-button = + .title = أزِل النص +pdfjs-editor-remove-stamp-button = + .title = أزِل الصورة +pdfjs-editor-remove-highlight-button = + .title = أزِل الإبراز + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = اللون +pdfjs-editor-free-text-size-input = الحجم +pdfjs-editor-ink-color-input = اللون +pdfjs-editor-ink-thickness-input = السماكة +pdfjs-editor-ink-opacity-input = العتامة +pdfjs-editor-stamp-add-image-button = + .title = أضِف صورة +pdfjs-editor-stamp-add-image-button-label = أضِف صورة +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = السماكة +pdfjs-editor-free-highlight-thickness-title = + .title = غيّر السُمك عند إبراز عناصر أُخرى غير النص +pdfjs-free-text = + .aria-label = محرِّر النص +pdfjs-free-text-default-content = ابدأ الكتابة… +pdfjs-ink = + .aria-label = محرِّر الرسم +pdfjs-ink-canvas = + .aria-label = صورة أنشأها المستخدم + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = نص بديل +pdfjs-editor-alt-text-edit-button-label = تحرير النص البديل +pdfjs-editor-alt-text-dialog-label = اختر خيار +pdfjs-editor-alt-text-dialog-description = يساعد النص البديل عندما لا يتمكن الأشخاص من رؤية الصورة أو عندما لا يتم تحميلها. +pdfjs-editor-alt-text-add-description-label = أضِف وصف +pdfjs-editor-alt-text-add-description-description = استهدف جملتين تصفان الموضوع أو الإعداد أو الإجراءات. +pdfjs-editor-alt-text-mark-decorative-label = علّمها على أنها زخرفية +pdfjs-editor-alt-text-mark-decorative-description = يُستخدم هذا في الصور المزخرفة، مثل الحدود أو العلامات المائية. +pdfjs-editor-alt-text-cancel-button = ألغِ +pdfjs-editor-alt-text-save-button = احفظ +pdfjs-editor-alt-text-decorative-tooltip = عُلّمت على أنها زخرفية +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = على سبيل المثال، "يجلس شاب على الطاولة لتناول وجبة" + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = الزاوية اليُسرى العُليا — غيّر الحجم +pdfjs-editor-resizer-label-top-middle = أعلى الوسط - غيّر الحجم +pdfjs-editor-resizer-label-top-right = الزاوية اليُمنى العُليا - غيّر الحجم +pdfjs-editor-resizer-label-middle-right = اليمين الأوسط - غيّر الحجم +pdfjs-editor-resizer-label-bottom-right = الزاوية اليُمنى السُفلى - غيّر الحجم +pdfjs-editor-resizer-label-bottom-middle = أسفل الوسط - غيّر الحجم +pdfjs-editor-resizer-label-bottom-left = الزاوية اليُسرى السُفلية - غيّر الحجم +pdfjs-editor-resizer-label-middle-left = مُنتصف اليسار - غيّر الحجم +pdfjs-editor-resizer-top-left = + .aria-label = الزاوية اليُسرى العُليا — غيّر الحجم +pdfjs-editor-resizer-top-middle = + .aria-label = أعلى الوسط - غيّر الحجم +pdfjs-editor-resizer-top-right = + .aria-label = الزاوية اليُمنى العُليا - غيّر الحجم +pdfjs-editor-resizer-middle-right = + .aria-label = اليمين الأوسط - غيّر الحجم +pdfjs-editor-resizer-bottom-right = + .aria-label = الزاوية اليُمنى السُفلى - غيّر الحجم +pdfjs-editor-resizer-bottom-middle = + .aria-label = أسفل الوسط - غيّر الحجم +pdfjs-editor-resizer-bottom-left = + .aria-label = الزاوية اليُسرى السُفلية - غيّر الحجم +pdfjs-editor-resizer-middle-left = + .aria-label = مُنتصف اليسار - غيّر الحجم + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = أبرِز اللون +pdfjs-editor-colorpicker-button = + .title = غيّر اللون +pdfjs-editor-colorpicker-dropdown = + .aria-label = اختيارات الألوان +pdfjs-editor-colorpicker-yellow = + .title = أصفر +pdfjs-editor-colorpicker-green = + .title = أخضر +pdfjs-editor-colorpicker-blue = + .title = أزرق +pdfjs-editor-colorpicker-pink = + .title = وردي +pdfjs-editor-colorpicker-red = + .title = أحمر + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = أظهِر الكل +pdfjs-editor-highlight-show-all-button = + .title = أظهِر الكل + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/ast/viewer.ftl b/public/assets/pdfjs/locale/ast/viewer.ftl new file mode 100644 index 0000000..2503caf --- /dev/null +++ b/public/assets/pdfjs/locale/ast/viewer.ftl @@ -0,0 +1,201 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Páxina anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Páxina siguiente +pdfjs-next-button-label = Siguiente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Páxina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Alloñar +pdfjs-zoom-out-button-label = Alloña +pdfjs-zoom-in-button = + .title = Averar +pdfjs-zoom-in-button-label = Avera +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Cambiar al mou de presentación +pdfjs-presentation-mode-button-label = Mou de presentación +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprentar +pdfjs-print-button-label = Imprentar + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ferramientes +pdfjs-tools-button-label = Ferramientes +pdfjs-first-page-button-label = Dir a la primer páxina +pdfjs-last-page-button-label = Dir a la última páxina +pdfjs-page-rotate-cw-button = + .title = Voltia a la derecha +pdfjs-page-rotate-cw-button-label = Voltiar a la derecha +pdfjs-page-rotate-ccw-button = + .title = Voltia a la esquierda +pdfjs-page-rotate-ccw-button-label = Voltiar a la esquierda +pdfjs-cursor-text-select-tool-button = + .title = Activa la ferramienta d'esbilla de testu +pdfjs-cursor-text-select-tool-button-label = Ferramienta d'esbilla de testu +pdfjs-cursor-hand-tool-button = + .title = Activa la ferramienta de mano +pdfjs-cursor-hand-tool-button-label = Ferramienta de mano +pdfjs-scroll-vertical-button = + .title = Usa'l desplazamientu vertical +pdfjs-scroll-vertical-button-label = Desplazamientu vertical +pdfjs-scroll-horizontal-button = + .title = Usa'l desplazamientu horizontal +pdfjs-scroll-horizontal-button-label = Desplazamientu horizontal +pdfjs-scroll-wrapped-button = + .title = Usa'l desplazamientu continuu +pdfjs-scroll-wrapped-button-label = Desplazamientu continuu +pdfjs-spread-none-button-label = Fueyes individuales +pdfjs-spread-odd-button-label = Fueyes pares +pdfjs-spread-even-button-label = Fueyes impares + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedaes del documentu… +pdfjs-document-properties-button-label = Propiedaes del documentu… +pdfjs-document-properties-file-name = Nome del ficheru: +pdfjs-document-properties-file-size = Tamañu del ficheru: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Títulu: +pdfjs-document-properties-keywords = Pallabres clave: +pdfjs-document-properties-creation-date = Data de creación: +pdfjs-document-properties-modification-date = Data de modificación: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-producer = Productor del PDF: +pdfjs-document-properties-version = Versión del PDF: +pdfjs-document-properties-page-count = Númberu de páxines: +pdfjs-document-properties-page-size = Tamañu de páxina: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web rápida: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = Non +pdfjs-document-properties-close-button = Zarrar + +## Print + +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Encaboxar + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Alternar la barra llateral +pdfjs-attachments-button = + .title = Amosar los axuntos +pdfjs-attachments-button-label = Axuntos +pdfjs-layers-button-label = Capes +pdfjs-thumbs-button = + .title = Amosar les miniatures +pdfjs-thumbs-button-label = Miniatures +pdfjs-findbar-button-label = Atopar +pdfjs-additional-layers = Capes adicionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Páxina { $page } + +## Find panel button title and messages + +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button-label = Siguiente +pdfjs-find-entire-word-checkbox-label = Pallabres completes +pdfjs-find-reached-top = Algamóse'l comienzu de la páxina, síguese dende abaxo +pdfjs-find-reached-bottom = Algamóse la fin del documentu, síguese dende arriba + +## Predefined zoom values + +pdfjs-page-scale-auto = Zoom automáticu +pdfjs-page-scale-actual = Tamañu real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Páxina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Asocedió un fallu mentanto se cargaba'l PDF. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } + +## Password + +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Encaboxar + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/az/viewer.ftl b/public/assets/pdfjs/locale/az/viewer.ftl new file mode 100644 index 0000000..773aae4 --- /dev/null +++ b/public/assets/pdfjs/locale/az/viewer.ftl @@ -0,0 +1,257 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Əvvəlki səhifə +pdfjs-previous-button-label = Əvvəlkini tap +pdfjs-next-button = + .title = Növbəti səhifə +pdfjs-next-button-label = İrəli +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Səhifə +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = Uzaqlaş +pdfjs-zoom-out-button-label = Uzaqlaş +pdfjs-zoom-in-button = + .title = Yaxınlaş +pdfjs-zoom-in-button-label = Yaxınlaş +pdfjs-zoom-select = + .title = Yaxınlaşdırma +pdfjs-presentation-mode-button = + .title = Təqdimat Rejiminə Keç +pdfjs-presentation-mode-button-label = Təqdimat Rejimi +pdfjs-open-file-button = + .title = Fayl Aç +pdfjs-open-file-button-label = Aç +pdfjs-print-button = + .title = Yazdır +pdfjs-print-button-label = Yazdır + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Alətlər +pdfjs-tools-button-label = Alətlər +pdfjs-first-page-button = + .title = İlk Səhifəyə get +pdfjs-first-page-button-label = İlk Səhifəyə get +pdfjs-last-page-button = + .title = Son Səhifəyə get +pdfjs-last-page-button-label = Son Səhifəyə get +pdfjs-page-rotate-cw-button = + .title = Saat İstiqamətində Fırlat +pdfjs-page-rotate-cw-button-label = Saat İstiqamətində Fırlat +pdfjs-page-rotate-ccw-button = + .title = Saat İstiqamətinin Əksinə Fırlat +pdfjs-page-rotate-ccw-button-label = Saat İstiqamətinin Əksinə Fırlat +pdfjs-cursor-text-select-tool-button = + .title = Yazı seçmə alətini aktivləşdir +pdfjs-cursor-text-select-tool-button-label = Yazı seçmə aləti +pdfjs-cursor-hand-tool-button = + .title = Əl alətini aktivləşdir +pdfjs-cursor-hand-tool-button-label = Əl aləti +pdfjs-scroll-vertical-button = + .title = Şaquli sürüşdürmə işlət +pdfjs-scroll-vertical-button-label = Şaquli sürüşdürmə +pdfjs-scroll-horizontal-button = + .title = Üfüqi sürüşdürmə işlət +pdfjs-scroll-horizontal-button-label = Üfüqi sürüşdürmə +pdfjs-scroll-wrapped-button = + .title = Bükülü sürüşdürmə işlət +pdfjs-scroll-wrapped-button-label = Bükülü sürüşdürmə +pdfjs-spread-none-button = + .title = Yan-yana birləşdirilmiş səhifələri işlətmə +pdfjs-spread-none-button-label = Birləşdirmə +pdfjs-spread-odd-button = + .title = Yan-yana birləşdirilmiş səhifələri tək nömrəli səhifələrdən başlat +pdfjs-spread-odd-button-label = Tək nömrəli +pdfjs-spread-even-button = + .title = Yan-yana birləşdirilmiş səhifələri cüt nömrəli səhifələrdən başlat +pdfjs-spread-even-button-label = Cüt nömrəli + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Sənəd xüsusiyyətləri… +pdfjs-document-properties-button-label = Sənəd xüsusiyyətləri… +pdfjs-document-properties-file-name = Fayl adı: +pdfjs-document-properties-file-size = Fayl ölçüsü: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bayt) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bayt) +pdfjs-document-properties-title = Başlık: +pdfjs-document-properties-author = Müəllif: +pdfjs-document-properties-subject = Mövzu: +pdfjs-document-properties-keywords = Açar sözlər: +pdfjs-document-properties-creation-date = Yaradılış Tarixi : +pdfjs-document-properties-modification-date = Dəyişdirilmə Tarixi : +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Yaradan: +pdfjs-document-properties-producer = PDF yaradıcısı: +pdfjs-document-properties-version = PDF versiyası: +pdfjs-document-properties-page-count = Səhifə sayı: +pdfjs-document-properties-page-size = Səhifə Ölçüsü: +pdfjs-document-properties-page-size-unit-inches = inç +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portret +pdfjs-document-properties-page-size-orientation-landscape = albom +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Məktub +pdfjs-document-properties-page-size-name-legal = Hüquqi + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Bəli +pdfjs-document-properties-linearized-no = Xeyr +pdfjs-document-properties-close-button = Qapat + +## Print + +pdfjs-print-progress-message = Sənəd çap üçün hazırlanır… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Ləğv et +pdfjs-printing-not-supported = Xəbərdarlıq: Çap bu səyyah tərəfindən tam olaraq dəstəklənmir. +pdfjs-printing-not-ready = Xəbərdarlıq: PDF çap üçün tam yüklənməyib. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Yan Paneli Aç/Bağla +pdfjs-toggle-sidebar-notification-button = + .title = Yan paneli çevir (sənəddə icmal/bağlamalar/laylar mövcuddur) +pdfjs-toggle-sidebar-button-label = Yan Paneli Aç/Bağla +pdfjs-document-outline-button = + .title = Sənədin eskizini göstər (bütün bəndləri açmaq/yığmaq üçün iki dəfə klikləyin) +pdfjs-document-outline-button-label = Sənəd strukturu +pdfjs-attachments-button = + .title = Bağlamaları göstər +pdfjs-attachments-button-label = Bağlamalar +pdfjs-layers-button = + .title = Layları göstər (bütün layları ilkin halına sıfırlamaq üçün iki dəfə klikləyin) +pdfjs-layers-button-label = Laylar +pdfjs-thumbs-button = + .title = Kiçik şəkilləri göstər +pdfjs-thumbs-button-label = Kiçik şəkillər +pdfjs-findbar-button = + .title = Sənəddə Tap +pdfjs-findbar-button-label = Tap +pdfjs-additional-layers = Əlavə laylar + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Səhifə{ $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } səhifəsinin kiçik vəziyyəti + +## Find panel button title and messages + +pdfjs-find-input = + .title = Tap + .placeholder = Sənəddə tap… +pdfjs-find-previous-button = + .title = Bir öncəki uyğun gələn sözü tapır +pdfjs-find-previous-button-label = Geri +pdfjs-find-next-button = + .title = Bir sonrakı uyğun gələn sözü tapır +pdfjs-find-next-button-label = İrəli +pdfjs-find-highlight-checkbox = İşarələ +pdfjs-find-match-case-checkbox-label = Böyük/kiçik hərfə həssaslıq +pdfjs-find-entire-word-checkbox-label = Tam sözlər +pdfjs-find-reached-top = Sənədin yuxarısına çatdı, aşağıdan davam edir +pdfjs-find-reached-bottom = Sənədin sonuna çatdı, yuxarıdan davam edir +pdfjs-find-not-found = Uyğunlaşma tapılmadı + +## Predefined zoom values + +pdfjs-page-scale-width = Səhifə genişliyi +pdfjs-page-scale-fit = Səhifəni sığdır +pdfjs-page-scale-auto = Avtomatik yaxınlaşdır +pdfjs-page-scale-actual = Hazırkı Həcm +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF yüklenərkən bir səhv yarandı. +pdfjs-invalid-file-error = Səhv və ya zədələnmiş olmuş PDF fayl. +pdfjs-missing-file-error = PDF fayl yoxdur. +pdfjs-unexpected-response-error = Gözlənilməz server cavabı. +pdfjs-rendering-error = Səhifə göstərilərkən səhv yarandı. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotasiyası] + +## Password + +pdfjs-password-label = Bu PDF faylı açmaq üçün parolu daxil edin. +pdfjs-password-invalid = Parol səhvdir. Bir daha yoxlayın. +pdfjs-password-ok-button = Tamam +pdfjs-password-cancel-button = Ləğv et +pdfjs-web-fonts-disabled = Web Şriftlər söndürülüb: yerləşdirilmiş PDF şriftlərini istifadə etmək mümkün deyil. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/be/viewer.ftl b/public/assets/pdfjs/locale/be/viewer.ftl new file mode 100644 index 0000000..3f029d9 --- /dev/null +++ b/public/assets/pdfjs/locale/be/viewer.ftl @@ -0,0 +1,518 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Папярэдняя старонка +pdfjs-previous-button-label = Папярэдняя +pdfjs-next-button = + .title = Наступная старонка +pdfjs-next-button-label = Наступная +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Старонка +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = з { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } з { $pagesCount }) +pdfjs-zoom-out-button = + .title = Паменшыць +pdfjs-zoom-out-button-label = Паменшыць +pdfjs-zoom-in-button = + .title = Павялічыць +pdfjs-zoom-in-button-label = Павялічыць +pdfjs-zoom-select = + .title = Павялічэнне тэксту +pdfjs-presentation-mode-button = + .title = Пераключыцца ў рэжым паказу +pdfjs-presentation-mode-button-label = Рэжым паказу +pdfjs-open-file-button = + .title = Адкрыць файл +pdfjs-open-file-button-label = Адкрыць +pdfjs-print-button = + .title = Друкаваць +pdfjs-print-button-label = Друкаваць +pdfjs-save-button = + .title = Захаваць +pdfjs-save-button-label = Захаваць +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Сцягнуць +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Сцягнуць +pdfjs-bookmark-button = + .title = Дзейная старонка (паглядзець URL-адрас з дзейнай старонкі) +pdfjs-bookmark-button-label = Цяперашняя старонка + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Прылады +pdfjs-tools-button-label = Прылады +pdfjs-first-page-button = + .title = Перайсці на першую старонку +pdfjs-first-page-button-label = Перайсці на першую старонку +pdfjs-last-page-button = + .title = Перайсці на апошнюю старонку +pdfjs-last-page-button-label = Перайсці на апошнюю старонку +pdfjs-page-rotate-cw-button = + .title = Павярнуць па сонцу +pdfjs-page-rotate-cw-button-label = Павярнуць па сонцу +pdfjs-page-rotate-ccw-button = + .title = Павярнуць супраць сонца +pdfjs-page-rotate-ccw-button-label = Павярнуць супраць сонца +pdfjs-cursor-text-select-tool-button = + .title = Уключыць прыладу выбару тэксту +pdfjs-cursor-text-select-tool-button-label = Прылада выбару тэксту +pdfjs-cursor-hand-tool-button = + .title = Уключыць ручную прыладу +pdfjs-cursor-hand-tool-button-label = Ручная прылада +pdfjs-scroll-page-button = + .title = Выкарыстоўваць пракрутку старонкi +pdfjs-scroll-page-button-label = Пракрутка старонкi +pdfjs-scroll-vertical-button = + .title = Ужываць вертыкальную пракрутку +pdfjs-scroll-vertical-button-label = Вертыкальная пракрутка +pdfjs-scroll-horizontal-button = + .title = Ужываць гарызантальную пракрутку +pdfjs-scroll-horizontal-button-label = Гарызантальная пракрутка +pdfjs-scroll-wrapped-button = + .title = Ужываць маштабавальную пракрутку +pdfjs-scroll-wrapped-button-label = Маштабавальная пракрутка +pdfjs-spread-none-button = + .title = Не выкарыстоўваць разгорнутыя старонкі +pdfjs-spread-none-button-label = Без разгорнутых старонак +pdfjs-spread-odd-button = + .title = Разгорнутыя старонкі пачынаючы з няцотных нумароў +pdfjs-spread-odd-button-label = Няцотныя старонкі злева +pdfjs-spread-even-button = + .title = Разгорнутыя старонкі пачынаючы з цотных нумароў +pdfjs-spread-even-button-label = Цотныя старонкі злева + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Уласцівасці дакумента… +pdfjs-document-properties-button-label = Уласцівасці дакумента… +pdfjs-document-properties-file-name = Назва файла: +pdfjs-document-properties-file-size = Памер файла: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } КБ ({ $b } байтаў) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байтаў) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } байт) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байт) +pdfjs-document-properties-title = Загаловак: +pdfjs-document-properties-author = Аўтар: +pdfjs-document-properties-subject = Тэма: +pdfjs-document-properties-keywords = Ключавыя словы: +pdfjs-document-properties-creation-date = Дата стварэння: +pdfjs-document-properties-modification-date = Дата змянення: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Стваральнік: +pdfjs-document-properties-producer = Вырабнік PDF: +pdfjs-document-properties-version = Версія PDF: +pdfjs-document-properties-page-count = Колькасць старонак: +pdfjs-document-properties-page-size = Памер старонкі: +pdfjs-document-properties-page-size-unit-inches = цаляў +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = кніжная +pdfjs-document-properties-page-size-orientation-landscape = альбомная +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Хуткі прагляд у Інтэрнэце: +pdfjs-document-properties-linearized-yes = Так +pdfjs-document-properties-linearized-no = Не +pdfjs-document-properties-close-button = Закрыць + +## Print + +pdfjs-print-progress-message = Падрыхтоўка дакумента да друку… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Скасаваць +pdfjs-printing-not-supported = Папярэджанне: друк не падтрымліваецца цалкам гэтым браўзерам. +pdfjs-printing-not-ready = Увага: PDF не сцягнуты цалкам для друкавання. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Паказаць/схаваць бакавую панэль +pdfjs-toggle-sidebar-notification-button = + .title = Паказаць/схаваць бакавую панэль (дакумент мае змест/укладанні/пласты) +pdfjs-toggle-sidebar-button-label = Паказаць/схаваць бакавую панэль +pdfjs-document-outline-button = + .title = Паказаць структуру дакумента (двайная пстрычка, каб разгарнуць /згарнуць усе элементы) +pdfjs-document-outline-button-label = Структура дакумента +pdfjs-attachments-button = + .title = Паказаць далучэнні +pdfjs-attachments-button-label = Далучэнні +pdfjs-layers-button = + .title = Паказаць пласты (націсніце двойчы, каб скінуць усе пласты да прадвызначанага стану) +pdfjs-layers-button-label = Пласты +pdfjs-thumbs-button = + .title = Паказ мініяцюр +pdfjs-thumbs-button-label = Мініяцюры +pdfjs-current-outline-item-button = + .title = Знайсці бягучы элемент структуры +pdfjs-current-outline-item-button-label = Бягучы элемент структуры +pdfjs-findbar-button = + .title = Пошук у дакуменце +pdfjs-findbar-button-label = Знайсці +pdfjs-additional-layers = Дадатковыя пласты + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Старонка { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Мініяцюра старонкі { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Шукаць + .placeholder = Шукаць у дакуменце… +pdfjs-find-previous-button = + .title = Знайсці папярэдні выпадак выразу +pdfjs-find-previous-button-label = Папярэдні +pdfjs-find-next-button = + .title = Знайсці наступны выпадак выразу +pdfjs-find-next-button-label = Наступны +pdfjs-find-highlight-checkbox = Падфарбаваць усе +pdfjs-find-match-case-checkbox-label = Адрозніваць вялікія/малыя літары +pdfjs-find-match-diacritics-checkbox-label = З улікам дыякрытык +pdfjs-find-entire-word-checkbox-label = Словы цалкам +pdfjs-find-reached-top = Дасягнуты пачатак дакумента, працяг з канца +pdfjs-find-reached-bottom = Дасягнуты канец дакумента, працяг з пачатку +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } з { $total } супадзенняў + [few] { $current } з { $total } супадзенняў + *[many] { $current } з { $total } супадзенняў + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Больш за { $limit } супадзенне + [few] Больш за { $limit } супадзенні + *[many] Больш за { $limit } супадзенняў + } +pdfjs-find-not-found = Выраз не знойдзены + +## Predefined zoom values + +pdfjs-page-scale-width = Шырыня старонкі +pdfjs-page-scale-fit = Уцісненне старонкі +pdfjs-page-scale-auto = Аўтаматычнае павелічэнне +pdfjs-page-scale-actual = Сапраўдны памер +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Старонка { $page } + +## Loading indicator messages + +pdfjs-loading-error = Здарылася памылка ў часе загрузкі PDF. +pdfjs-invalid-file-error = Няспраўны або пашкоджаны файл PDF. +pdfjs-missing-file-error = Адсутны файл PDF. +pdfjs-unexpected-response-error = Нечаканы адказ сервера. +pdfjs-rendering-error = Здарылася памылка падчас адлюстравання старонкі. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Увядзіце пароль, каб адкрыць гэты файл PDF. +pdfjs-password-invalid = Нядзейсны пароль. Паспрабуйце зноў. +pdfjs-password-ok-button = Добра +pdfjs-password-cancel-button = Скасаваць +pdfjs-web-fonts-disabled = Шрыфты Сеціва забаронены: немагчыма ўжываць укладзеныя шрыфты PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Тэкст +pdfjs-editor-free-text-button-label = Тэкст +pdfjs-editor-ink-button = + .title = Маляваць +pdfjs-editor-ink-button-label = Маляваць +pdfjs-editor-stamp-button = + .title = Дадаць або змяніць выявы +pdfjs-editor-stamp-button-label = Дадаць або змяніць выявы +pdfjs-editor-highlight-button = + .title = Вылучэнне +pdfjs-editor-highlight-button-label = Вылучэнне +pdfjs-highlight-floating-button1 = + .title = Падфарбаваць + .aria-label = Падфарбаваць +pdfjs-highlight-floating-button-label = Падфарбаваць + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Выдаліць малюнак +pdfjs-editor-remove-freetext-button = + .title = Выдаліць тэкст +pdfjs-editor-remove-stamp-button = + .title = Выдаліць выяву +pdfjs-editor-remove-highlight-button = + .title = Выдаліць падфарбоўку + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Колер +pdfjs-editor-free-text-size-input = Памер +pdfjs-editor-ink-color-input = Колер +pdfjs-editor-ink-thickness-input = Таўшчыня +pdfjs-editor-ink-opacity-input = Непразрыстасць +pdfjs-editor-stamp-add-image-button = + .title = Дадаць выяву +pdfjs-editor-stamp-add-image-button-label = Дадаць выяву +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Таўшчыня +pdfjs-editor-free-highlight-thickness-title = + .title = Змяняць таўшчыню пры вылучэнні іншых элементаў, акрамя тэксту +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Тэкставы рэдактар + .default-content = Пачніце ўводзіць… +pdfjs-free-text = + .aria-label = Тэкставы рэдактар +pdfjs-free-text-default-content = Пачніце набор тэксту… +pdfjs-ink = + .aria-label = Графічны рэдактар +pdfjs-ink-canvas = + .aria-label = Выява, створаная карыстальнікам + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Альтэрнатыўны тэкст +pdfjs-editor-alt-text-edit-button = + .aria-label = Змяніць альтэрнатыўны тэкст +pdfjs-editor-alt-text-edit-button-label = Змяніць альтэрнатыўны тэкст +pdfjs-editor-alt-text-dialog-label = Выберыце варыянт +pdfjs-editor-alt-text-dialog-description = Альтэрнатыўны тэкст дапамагае, калі людзі не бачаць выяву або калі яна не загружаецца. +pdfjs-editor-alt-text-add-description-label = Дадаць апісанне +pdfjs-editor-alt-text-add-description-description = Старайцеся скласці 1-2 сказы, якія апісваюць прадмет, абстаноўку або дзеянні. +pdfjs-editor-alt-text-mark-decorative-label = Пазначыць як дэкаратыўны +pdfjs-editor-alt-text-mark-decorative-description = Выкарыстоўваецца для дэкаратыўных выяваў, такіх як рамкі або вадзяныя знакі. +pdfjs-editor-alt-text-cancel-button = Скасаваць +pdfjs-editor-alt-text-save-button = Захаваць +pdfjs-editor-alt-text-decorative-tooltip = Пазначаны як дэкаратыўны +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Напрыклад, «Малады чалавек садзіцца за стол есці» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Альтэрнатыўны тэкст + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Верхні левы кут — змяніць памер +pdfjs-editor-resizer-label-top-middle = Уверсе пасярэдзіне — змяніць памер +pdfjs-editor-resizer-label-top-right = Верхні правы кут — змяніць памер +pdfjs-editor-resizer-label-middle-right = Пасярэдзіне справа — змяніць памер +pdfjs-editor-resizer-label-bottom-right = Правы ніжні кут — змяніць памер +pdfjs-editor-resizer-label-bottom-middle = Пасярэдзіне ўнізе — змяніць памер +pdfjs-editor-resizer-label-bottom-left = Левы ніжні кут — змяніць памер +pdfjs-editor-resizer-label-middle-left = Пасярэдзіне злева — змяніць памер +pdfjs-editor-resizer-top-left = + .aria-label = Верхні левы кут — змяніць памер +pdfjs-editor-resizer-top-middle = + .aria-label = Уверсе пасярэдзіне — змяніць памер +pdfjs-editor-resizer-top-right = + .aria-label = Верхні правы кут — змяніць памер +pdfjs-editor-resizer-middle-right = + .aria-label = Пасярэдзіне справа — змяніць памер +pdfjs-editor-resizer-bottom-right = + .aria-label = Правы ніжні кут — змяніць памер +pdfjs-editor-resizer-bottom-middle = + .aria-label = Пасярэдзіне ўнізе — змяніць памер +pdfjs-editor-resizer-bottom-left = + .aria-label = Левы ніжні кут — змяніць памер +pdfjs-editor-resizer-middle-left = + .aria-label = Пасярэдзіне злева — змяніць памер + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Колер падфарбоўкі +pdfjs-editor-colorpicker-button = + .title = Змяніць колер +pdfjs-editor-colorpicker-dropdown = + .aria-label = Выбар колеру +pdfjs-editor-colorpicker-yellow = + .title = Жоўты +pdfjs-editor-colorpicker-green = + .title = Зялёны +pdfjs-editor-colorpicker-blue = + .title = Блакітны +pdfjs-editor-colorpicker-pink = + .title = Ружовы +pdfjs-editor-colorpicker-red = + .title = Чырвоны + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Паказаць усе +pdfjs-editor-highlight-show-all-button = + .title = Паказаць усе + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Рэдагаваць тэкст для атрыбута alt (апісанне выявы) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Дадаць тэкст для атрыбута alt (апісанне выявы) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Напішыце сваё апісанне тут… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Кароткае апісанне для людзей, якія не бачаць выяву, ці калі выява не загружаецца. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Гэты тэкст для атрыбута alt быў створаны аўтаматычна і можа быць недакладным +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Даведацца больш +pdfjs-editor-new-alt-text-create-automatically-button-label = Ствараць тэкст для атрыбута alt аўтаматычна +pdfjs-editor-new-alt-text-not-now-button = Не зараз +pdfjs-editor-new-alt-text-error-title = Не ўдалося аўтаматычна стварыць тэкст для атрыбута alt +pdfjs-editor-new-alt-text-error-description = Калі ласка, напішыце ўласны тэкст для атрыбута alt або паўтарыце спробу пазней. +pdfjs-editor-new-alt-text-error-close-button = Закрыць +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Сцягванне мадэлі ШІ для тэксту для атрыбута alt ({ $downloadedSize } з { $totalSize } МБ) + .aria-valuetext = Сцягванне мадэлі ШІ для тэксту для атрыбута alt ({ $downloadedSize } з { $totalSize } МБ) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Тэкст для атрыбута alt дададзены +pdfjs-editor-new-alt-text-added-button-label = Тэкст для атрыбута alt дададзены +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Адсутнічае тэкст для атрыбута alt +pdfjs-editor-new-alt-text-missing-button-label = Адсутнічае тэкст для атрыбута alt +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Водгук на тэкст для атрыбута alt +pdfjs-editor-new-alt-text-to-review-button-label = Водгук на тэкст для атрыбута alt +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Створаны аўтаматычна: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Налады альтэрнатыўнага тэксту для выявы +pdfjs-image-alt-text-settings-button-label = Налады альтэрнатыўнага тэксту для выявы +pdfjs-editor-alt-text-settings-dialog-label = Налады альтэрнатыўнага тэксту для выявы +pdfjs-editor-alt-text-settings-automatic-title = Аўтаматычны тэкст для атрыбута alt +pdfjs-editor-alt-text-settings-create-model-button-label = Ствараць тэкст для атрыбута alt аўтаматычна +pdfjs-editor-alt-text-settings-create-model-description = Прапануе апісанні, каб дапамагчы людзям, якія не бачаць выяву, ці калі выява не загружаецца. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Мадэль ШІ для тэксту для атрыбута alt ({ $totalSize } МБ) +pdfjs-editor-alt-text-settings-ai-model-description = Працуе лакальна на вашай прыладзе, таму вашы звесткі застаюцца прыватнымі. Патрабуецца для аўтаматычнага альтэрнатыўнага тэксту. +pdfjs-editor-alt-text-settings-delete-model-button = Выдаліць +pdfjs-editor-alt-text-settings-download-model-button = Сцягнуць +pdfjs-editor-alt-text-settings-downloading-model-button = Сцягванне… +pdfjs-editor-alt-text-settings-editor-title = Рэдактар тэксту для атрыбута alt +pdfjs-editor-alt-text-settings-show-dialog-button-label = Адразу паказваць рэдактар тэксту для атрыбута alt пры даданні выявы +pdfjs-editor-alt-text-settings-show-dialog-description = Дапамагае пераканацца, што ўсе вашы выявы маюць альтэрнатыўны тэкст. +pdfjs-editor-alt-text-settings-close-button = Закрыць + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Падсвятленне выдалена +pdfjs-editor-undo-bar-message-freetext = Тэкст выдалены +pdfjs-editor-undo-bar-message-ink = Малюнак выдалены +pdfjs-editor-undo-bar-message-stamp = Відарыс выдалены +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } анатацыя выдалена + [few] { $count } анатацыі выдалена + *[many] { $count } анатацый выдалена + } +pdfjs-editor-undo-bar-undo-button = + .title = Адмяніць +pdfjs-editor-undo-bar-undo-button-label = Адмяніць +pdfjs-editor-undo-bar-close-button = + .title = Закрыць +pdfjs-editor-undo-bar-close-button-label = Закрыць diff --git a/public/assets/pdfjs/locale/bg/viewer.ftl b/public/assets/pdfjs/locale/bg/viewer.ftl new file mode 100644 index 0000000..8b1124e --- /dev/null +++ b/public/assets/pdfjs/locale/bg/viewer.ftl @@ -0,0 +1,418 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Предишна страница +pdfjs-previous-button-label = Предишна +pdfjs-next-button = + .title = Следваща страница +pdfjs-next-button-label = Следваща +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Страница +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = от { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } от { $pagesCount }) +pdfjs-zoom-out-button = + .title = Намаляване +pdfjs-zoom-out-button-label = Намаляване +pdfjs-zoom-in-button = + .title = Увеличаване +pdfjs-zoom-in-button-label = Увеличаване +pdfjs-zoom-select = + .title = Мащабиране +pdfjs-presentation-mode-button = + .title = Превключване към режим на представяне +pdfjs-presentation-mode-button-label = Режим на представяне +pdfjs-open-file-button = + .title = Отваряне на файл +pdfjs-open-file-button-label = Отваряне +pdfjs-print-button = + .title = Отпечатване +pdfjs-print-button-label = Отпечатване +pdfjs-save-button = + .title = Запазване +pdfjs-save-button-label = Запазване +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Изтегляне +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Изтегляне +pdfjs-bookmark-button = + .title = Текуща страница (преглед на адреса на страницата) +pdfjs-bookmark-button-label = Текуща страница + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Инструменти +pdfjs-tools-button-label = Инструменти +pdfjs-first-page-button = + .title = Към първата страница +pdfjs-first-page-button-label = Към първата страница +pdfjs-last-page-button = + .title = Към последната страница +pdfjs-last-page-button-label = Към последната страница +pdfjs-page-rotate-cw-button = + .title = Завъртане по час. стрелка +pdfjs-page-rotate-cw-button-label = Завъртане по часовниковата стрелка +pdfjs-page-rotate-ccw-button = + .title = Завъртане обратно на час. стрелка +pdfjs-page-rotate-ccw-button-label = Завъртане обратно на часовниковата стрелка +pdfjs-cursor-text-select-tool-button = + .title = Включване на инструмента за избор на текст +pdfjs-cursor-text-select-tool-button-label = Инструмент за избор на текст +pdfjs-cursor-hand-tool-button = + .title = Включване на инструмента ръка +pdfjs-cursor-hand-tool-button-label = Инструмент ръка +pdfjs-scroll-page-button = + .title = Използване на плъзгане на страници +pdfjs-scroll-page-button-label = Плъзгане на страници +pdfjs-scroll-vertical-button = + .title = Използване на вертикално плъзгане +pdfjs-scroll-vertical-button-label = Вертикално плъзгане +pdfjs-scroll-horizontal-button = + .title = Използване на хоризонтално +pdfjs-scroll-horizontal-button-label = Хоризонтално плъзгане +pdfjs-scroll-wrapped-button = + .title = Използване на мащабируемо плъзгане +pdfjs-scroll-wrapped-button-label = Мащабируемо плъзгане +pdfjs-spread-none-button = + .title = Режимът на сдвояване е изключен +pdfjs-spread-none-button-label = Без сдвояване +pdfjs-spread-odd-button = + .title = Сдвояване, започвайки от нечетните страници +pdfjs-spread-odd-button-label = Нечетните отляво +pdfjs-spread-even-button = + .title = Сдвояване, започвайки от четните страници +pdfjs-spread-even-button-label = Четните отляво + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Свойства на документа… +pdfjs-document-properties-button-label = Свойства на документа… +pdfjs-document-properties-file-name = Име на файл: +pdfjs-document-properties-file-size = Големина на файл: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } КБ ({ $b } байта) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байта) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } байта) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байта) +pdfjs-document-properties-title = Заглавие: +pdfjs-document-properties-author = Автор: +pdfjs-document-properties-subject = Тема: +pdfjs-document-properties-keywords = Ключови думи: +pdfjs-document-properties-creation-date = Дата на създаване: +pdfjs-document-properties-modification-date = Дата на промяна: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Създател: +pdfjs-document-properties-producer = PDF произведен от: +pdfjs-document-properties-version = Издание на PDF: +pdfjs-document-properties-page-count = Брой страници: +pdfjs-document-properties-page-size = Размер на страницата: +pdfjs-document-properties-page-size-unit-inches = инч +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = портрет +pdfjs-document-properties-page-size-orientation-landscape = пейзаж +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Правни въпроси + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Бърз преглед: +pdfjs-document-properties-linearized-yes = Да +pdfjs-document-properties-linearized-no = Не +pdfjs-document-properties-close-button = Затваряне + +## Print + +pdfjs-print-progress-message = Подготвяне на документа за отпечатване… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Отказ +pdfjs-printing-not-supported = Внимание: Този четец няма пълна поддръжка на отпечатване. +pdfjs-printing-not-ready = Внимание: Този PDF файл не е напълно зареден за печат. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Превключване на страничната лента +pdfjs-toggle-sidebar-notification-button = + .title = Превключване на страничната лента (документът има структура/прикачени файлове/слоеве) +pdfjs-toggle-sidebar-button-label = Превключване на страничната лента +pdfjs-document-outline-button = + .title = Показване на структурата на документа (двукратно щракване за свиване/разгъване на всичко) +pdfjs-document-outline-button-label = Структура на документа +pdfjs-attachments-button = + .title = Показване на притурките +pdfjs-attachments-button-label = Притурки +pdfjs-layers-button = + .title = Показване на слоевете (двукратно щракване за възстановяване на всички слоеве към състоянието по подразбиране) +pdfjs-layers-button-label = Слоеве +pdfjs-thumbs-button = + .title = Показване на миниатюрите +pdfjs-thumbs-button-label = Миниатюри +pdfjs-current-outline-item-button = + .title = Намиране на текущия елемент от структурата +pdfjs-current-outline-item-button-label = Текущ елемент от структурата +pdfjs-findbar-button = + .title = Намиране в документа +pdfjs-findbar-button-label = Търсене +pdfjs-additional-layers = Допълнителни слоеве + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Страница { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Миниатюра на страница { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Търсене + .placeholder = Търсене в документа… +pdfjs-find-previous-button = + .title = Намиране на предишно съвпадение на фразата +pdfjs-find-previous-button-label = Предишна +pdfjs-find-next-button = + .title = Намиране на следващо съвпадение на фразата +pdfjs-find-next-button-label = Следваща +pdfjs-find-highlight-checkbox = Открояване на всички +pdfjs-find-match-case-checkbox-label = Съвпадение на регистъра +pdfjs-find-match-diacritics-checkbox-label = Без производни букви +pdfjs-find-entire-word-checkbox-label = Цели думи +pdfjs-find-reached-top = Достигнато е началото на документа, продължаване от края +pdfjs-find-reached-bottom = Достигнат е краят на документа, продължаване от началото +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } от { $total } съвпадение + *[other] { $current } от { $total } съвпадения + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Повече от { $limit } съвпадение + *[other] Повече от { $limit } съвпадения + } +pdfjs-find-not-found = Фразата не е намерена + +## Predefined zoom values + +pdfjs-page-scale-width = Ширина на страницата +pdfjs-page-scale-fit = Вместване в страницата +pdfjs-page-scale-auto = Автоматично мащабиране +pdfjs-page-scale-actual = Действителен размер +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Страница { $page } + +## Loading indicator messages + +pdfjs-loading-error = Получи се грешка при зареждане на PDF-а. +pdfjs-invalid-file-error = Невалиден или повреден PDF файл. +pdfjs-missing-file-error = Липсващ PDF файл. +pdfjs-unexpected-response-error = Неочакван отговор от сървъра. +pdfjs-rendering-error = Грешка при изчертаване на страницата. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Анотация { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Въведете парола за отваряне на този PDF файл. +pdfjs-password-invalid = Невалидна парола. Моля, опитайте отново. +pdfjs-password-ok-button = Добре +pdfjs-password-cancel-button = Отказ +pdfjs-web-fonts-disabled = Уеб-шрифтовете са забранени: разрешаване на използването на вградените PDF шрифтове. + +## Editing + +pdfjs-editor-free-text-button = + .title = Текст +pdfjs-editor-free-text-button-label = Текст +pdfjs-editor-ink-button = + .title = Рисуване +pdfjs-editor-ink-button-label = Рисуване +pdfjs-editor-stamp-button = + .title = Добавяне или променяне на изображения +pdfjs-editor-stamp-button-label = Добавяне или променяне на изображения + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Премахване на рисунката +pdfjs-editor-remove-freetext-button = + .title = Премахване на текста +pdfjs-editor-remove-stamp-button = + .title = Пермахване на изображението +pdfjs-editor-remove-highlight-button = + .title = Премахване на открояването + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Цвят +pdfjs-editor-free-text-size-input = Размер +pdfjs-editor-ink-color-input = Цвят +pdfjs-editor-ink-thickness-input = Дебелина +pdfjs-editor-ink-opacity-input = Прозрачност +pdfjs-editor-stamp-add-image-button = + .title = Добавяне на изображение +pdfjs-editor-stamp-add-image-button-label = Добавяне на изображение +pdfjs-free-text = + .aria-label = Текстов редактор +pdfjs-free-text-default-content = Започнете да пишете… +pdfjs-ink = + .aria-label = Промяна на рисунка +pdfjs-ink-canvas = + .aria-label = Изображение, създадено от потребител + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = Алтернативен текст +pdfjs-editor-alt-text-edit-button-label = Промяна на алтернативния текст +pdfjs-editor-alt-text-dialog-label = Изберете от възможностите +pdfjs-editor-alt-text-dialog-description = Алтернативният текст помага на потребителите, когато не могат да видят изображението или то не се зарежда. +pdfjs-editor-alt-text-add-description-label = Добавяне на описание +pdfjs-editor-alt-text-add-description-description = Стремете се към 1-2 изречения, описващи предмета, настройката или действията. +pdfjs-editor-alt-text-mark-decorative-label = Отбелязване като декоративно +pdfjs-editor-alt-text-mark-decorative-description = Използва се за орнаменти или декоративни изображения, като контури и водни знаци. +pdfjs-editor-alt-text-cancel-button = Отказ +pdfjs-editor-alt-text-save-button = Запазване +pdfjs-editor-alt-text-decorative-tooltip = Отбелязване като декоративно +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Например, „Млад мъж седи на маса и се храни“ + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Горен ляв ъгъл — преоразмеряване +pdfjs-editor-resizer-label-top-middle = Горе в средата — преоразмеряване +pdfjs-editor-resizer-label-top-right = Горен десен ъгъл — преоразмеряване +pdfjs-editor-resizer-label-middle-right = Дясно в средата — преоразмеряване +pdfjs-editor-resizer-label-bottom-right = Долен десен ъгъл — преоразмеряване +pdfjs-editor-resizer-label-bottom-middle = Долу в средата — преоразмеряване +pdfjs-editor-resizer-label-bottom-left = Долен ляв ъгъл — преоразмеряване +pdfjs-editor-resizer-label-middle-left = Ляво в средата — преоразмеряване +pdfjs-editor-resizer-top-left = + .aria-label = Горен ляв ъгъл — преоразмеряване +pdfjs-editor-resizer-top-middle = + .aria-label = Горе в средата — преоразмеряване +pdfjs-editor-resizer-top-right = + .aria-label = Горен десен ъгъл — преоразмеряване +pdfjs-editor-resizer-middle-right = + .aria-label = Дясно в средата — преоразмеряване +pdfjs-editor-resizer-bottom-right = + .aria-label = Долен десен ъгъл — преоразмеряване +pdfjs-editor-resizer-bottom-middle = + .aria-label = Долу в средата — преоразмеряване +pdfjs-editor-resizer-bottom-left = + .aria-label = Долен ляв ъгъл — преоразмеряване +pdfjs-editor-resizer-middle-left = + .aria-label = Ляво в средата — преоразмеряване + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Цвят на открояване +pdfjs-editor-colorpicker-button = + .title = Промяна на цвят +pdfjs-editor-colorpicker-dropdown = + .aria-label = Избор на цвят +pdfjs-editor-colorpicker-yellow = + .title = Жълто +pdfjs-editor-colorpicker-green = + .title = Зелено +pdfjs-editor-colorpicker-blue = + .title = Синьо +pdfjs-editor-colorpicker-pink = + .title = Розово +pdfjs-editor-colorpicker-red = + .title = Червено + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +pdfjs-editor-new-alt-text-not-now-button = Не сега + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/bn/viewer.ftl b/public/assets/pdfjs/locale/bn/viewer.ftl new file mode 100644 index 0000000..1e20ecb --- /dev/null +++ b/public/assets/pdfjs/locale/bn/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = পূর্ববর্তী পাতা +pdfjs-previous-button-label = পূর্ববর্তী +pdfjs-next-button = + .title = পরবর্তী পাতা +pdfjs-next-button-label = পরবর্তী +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = পাতা +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } এর +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } এর { $pageNumber }) +pdfjs-zoom-out-button = + .title = ছোট আকারে প্রদর্শন +pdfjs-zoom-out-button-label = ছোট আকারে প্রদর্শন +pdfjs-zoom-in-button = + .title = বড় আকারে প্রদর্শন +pdfjs-zoom-in-button-label = বড় আকারে প্রদর্শন +pdfjs-zoom-select = + .title = বড় আকারে প্রদর্শন +pdfjs-presentation-mode-button = + .title = উপস্থাপনা মোডে স্যুইচ করুন +pdfjs-presentation-mode-button-label = উপস্থাপনা মোড +pdfjs-open-file-button = + .title = ফাইল খুলুন +pdfjs-open-file-button-label = খুলুন +pdfjs-print-button = + .title = মুদ্রণ +pdfjs-print-button-label = মুদ্রণ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = টুল +pdfjs-tools-button-label = টুল +pdfjs-first-page-button = + .title = প্রথম পাতায় যাও +pdfjs-first-page-button-label = প্রথম পাতায় যাও +pdfjs-last-page-button = + .title = শেষ পাতায় যাও +pdfjs-last-page-button-label = শেষ পাতায় যাও +pdfjs-page-rotate-cw-button = + .title = ঘড়ির কাঁটার দিকে ঘোরাও +pdfjs-page-rotate-cw-button-label = ঘড়ির কাঁটার দিকে ঘোরাও +pdfjs-page-rotate-ccw-button = + .title = ঘড়ির কাঁটার বিপরীতে ঘোরাও +pdfjs-page-rotate-ccw-button-label = ঘড়ির কাঁটার বিপরীতে ঘোরাও +pdfjs-cursor-text-select-tool-button = + .title = লেখা নির্বাচক টুল সক্রিয় করুন +pdfjs-cursor-text-select-tool-button-label = লেখা নির্বাচক টুল +pdfjs-cursor-hand-tool-button = + .title = হ্যান্ড টুল সক্রিয় করুন +pdfjs-cursor-hand-tool-button-label = হ্যান্ড টুল +pdfjs-scroll-vertical-button = + .title = উলম্ব স্ক্রলিং ব্যবহার করুন +pdfjs-scroll-vertical-button-label = উলম্ব স্ক্রলিং +pdfjs-scroll-horizontal-button = + .title = অনুভূমিক স্ক্রলিং ব্যবহার করুন +pdfjs-scroll-horizontal-button-label = অনুভূমিক স্ক্রলিং +pdfjs-scroll-wrapped-button = + .title = Wrapped স্ক্রোলিং ব্যবহার করুন +pdfjs-scroll-wrapped-button-label = Wrapped স্ক্রোলিং +pdfjs-spread-none-button = + .title = পেজ স্প্রেডগুলোতে যোগদান করবেন না +pdfjs-spread-none-button-label = Spreads নেই +pdfjs-spread-odd-button-label = বিজোড় Spreads +pdfjs-spread-even-button-label = জোড় Spreads + +## Document properties dialog + +pdfjs-document-properties-button = + .title = নথি বৈশিষ্ট্য… +pdfjs-document-properties-button-label = নথি বৈশিষ্ট্য… +pdfjs-document-properties-file-name = ফাইলের নাম: +pdfjs-document-properties-file-size = ফাইলের আকার: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } কেবি ({ $size_b } বাইট) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } এমবি ({ $size_b } বাইট) +pdfjs-document-properties-title = শিরোনাম: +pdfjs-document-properties-author = লেখক: +pdfjs-document-properties-subject = বিষয়: +pdfjs-document-properties-keywords = কীওয়ার্ড: +pdfjs-document-properties-creation-date = তৈরির তারিখ: +pdfjs-document-properties-modification-date = পরিবর্তনের তারিখ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = প্রস্তুতকারক: +pdfjs-document-properties-producer = পিডিএফ প্রস্তুতকারক: +pdfjs-document-properties-version = পিডিএফ সংষ্করণ: +pdfjs-document-properties-page-count = মোট পাতা: +pdfjs-document-properties-page-size = পাতার সাইজ: +pdfjs-document-properties-page-size-unit-inches = এর মধ্যে +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = উলম্ব +pdfjs-document-properties-page-size-orientation-landscape = অনুভূমিক +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = লেটার +pdfjs-document-properties-page-size-name-legal = লীগাল + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = হ্যাঁ +pdfjs-document-properties-linearized-no = না +pdfjs-document-properties-close-button = বন্ধ + +## Print + +pdfjs-print-progress-message = মুদ্রণের জন্য নথি প্রস্তুত করা হচ্ছে… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = বাতিল +pdfjs-printing-not-supported = সতর্কতা: এই ব্রাউজারে মুদ্রণ সম্পূর্ণভাবে সমর্থিত নয়। +pdfjs-printing-not-ready = সতর্কীকরণ: পিডিএফটি মুদ্রণের জন্য সম্পূর্ণ লোড হয়নি। + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = সাইডবার টগল করুন +pdfjs-toggle-sidebar-button-label = সাইডবার টগল করুন +pdfjs-document-outline-button = + .title = নথির আউটলাইন দেখাও (সব আইটেম প্রসারিত/সঙ্কুচিত করতে ডবল ক্লিক করুন) +pdfjs-document-outline-button-label = নথির রূপরেখা +pdfjs-attachments-button = + .title = সংযুক্তি দেখাও +pdfjs-attachments-button-label = সংযুক্তি +pdfjs-thumbs-button = + .title = থাম্বনেইল সমূহ প্রদর্শন করুন +pdfjs-thumbs-button-label = থাম্বনেইল সমূহ +pdfjs-findbar-button = + .title = নথির মধ্যে খুঁজুন +pdfjs-findbar-button-label = খুঁজুন + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = পাতা { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } পাতার থাম্বনেইল + +## Find panel button title and messages + +pdfjs-find-input = + .title = খুঁজুন + .placeholder = নথির মধ্যে খুঁজুন… +pdfjs-find-previous-button = + .title = বাক্যাংশের পূর্ববর্তী উপস্থিতি অনুসন্ধান +pdfjs-find-previous-button-label = পূর্ববর্তী +pdfjs-find-next-button = + .title = বাক্যাংশের পরবর্তী উপস্থিতি অনুসন্ধান +pdfjs-find-next-button-label = পরবর্তী +pdfjs-find-highlight-checkbox = সব হাইলাইট করুন +pdfjs-find-match-case-checkbox-label = অক্ষরের ছাঁদ মেলানো +pdfjs-find-entire-word-checkbox-label = সম্পূর্ণ শব্দ +pdfjs-find-reached-top = পাতার শুরুতে পৌছে গেছে, নীচ থেকে আরম্ভ করা হয়েছে +pdfjs-find-reached-bottom = পাতার শেষে পৌছে গেছে, উপর থেকে আরম্ভ করা হয়েছে +pdfjs-find-not-found = বাক্যাংশ পাওয়া যায়নি + +## Predefined zoom values + +pdfjs-page-scale-width = পাতার প্রস্থ +pdfjs-page-scale-fit = পাতা ফিট করুন +pdfjs-page-scale-auto = স্বয়ংক্রিয় জুম +pdfjs-page-scale-actual = প্রকৃত আকার +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = পিডিএফ লোড করার সময় ত্রুটি দেখা দিয়েছে। +pdfjs-invalid-file-error = অকার্যকর অথবা ক্ষতিগ্রস্ত পিডিএফ ফাইল। +pdfjs-missing-file-error = নিখোঁজ PDF ফাইল। +pdfjs-unexpected-response-error = অপ্রত্যাশীত সার্ভার প্রতিক্রিয়া। +pdfjs-rendering-error = পাতা উপস্থাপনার সময় ত্রুটি দেখা দিয়েছে। + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } টীকা] + +## Password + +pdfjs-password-label = পিডিএফ ফাইলটি ওপেন করতে পাসওয়ার্ড দিন। +pdfjs-password-invalid = ভুল পাসওয়ার্ড। অনুগ্রহ করে আবার চেষ্টা করুন। +pdfjs-password-ok-button = ঠিক আছে +pdfjs-password-cancel-button = বাতিল +pdfjs-web-fonts-disabled = ওয়েব ফন্ট নিষ্ক্রিয়: সংযুক্ত পিডিএফ ফন্ট ব্যবহার করা যাচ্ছে না। + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/bo/viewer.ftl b/public/assets/pdfjs/locale/bo/viewer.ftl new file mode 100644 index 0000000..824eab4 --- /dev/null +++ b/public/assets/pdfjs/locale/bo/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = དྲ་ངོས་སྔོན་མ +pdfjs-previous-button-label = སྔོན་མ +pdfjs-next-button = + .title = དྲ་ངོས་རྗེས་མ +pdfjs-next-button-label = རྗེས་མ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ཤོག་ངོས +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = of { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom Out +pdfjs-zoom-out-button-label = Zoom Out +pdfjs-zoom-in-button = + .title = Zoom In +pdfjs-zoom-in-button-label = Zoom In +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Switch to Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Open File +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Print +pdfjs-print-button-label = Print + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Go to First Page +pdfjs-first-page-button-label = Go to First Page +pdfjs-last-page-button = + .title = Go to Last Page +pdfjs-last-page-button-label = Go to Last Page +pdfjs-page-rotate-cw-button = + .title = Rotate Clockwise +pdfjs-page-rotate-cw-button-label = Rotate Clockwise +pdfjs-page-rotate-ccw-button = + .title = Rotate Counterclockwise +pdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise +pdfjs-cursor-text-select-tool-button = + .title = Enable Text Selection Tool +pdfjs-cursor-text-select-tool-button-label = Text Selection Tool +pdfjs-cursor-hand-tool-button = + .title = Enable Hand Tool +pdfjs-cursor-hand-tool-button-label = Hand Tool +pdfjs-scroll-vertical-button = + .title = Use Vertical Scrolling +pdfjs-scroll-vertical-button-label = Vertical Scrolling +pdfjs-scroll-horizontal-button = + .title = Use Horizontal Scrolling +pdfjs-scroll-horizontal-button-label = Horizontal Scrolling +pdfjs-scroll-wrapped-button = + .title = Use Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = Do not join page spreads +pdfjs-spread-none-button-label = No Spreads +pdfjs-spread-odd-button = + .title = Join page spreads starting with odd-numbered pages +pdfjs-spread-odd-button-label = Odd Spreads +pdfjs-spread-even-button = + .title = Join page spreads starting with even-numbered pages +pdfjs-spread-even-button-label = Even Spreads + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Document Properties… +pdfjs-document-properties-button-label = Document Properties… +pdfjs-document-properties-file-name = File name: +pdfjs-document-properties-file-size = File size: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Title: +pdfjs-document-properties-author = Author: +pdfjs-document-properties-subject = Subject: +pdfjs-document-properties-keywords = Keywords: +pdfjs-document-properties-creation-date = Creation Date: +pdfjs-document-properties-modification-date = Modification Date: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creator: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Page Count: +pdfjs-document-properties-page-size = Page Size: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = landscape +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Yes +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Close + +## Print + +pdfjs-print-progress-message = Preparing document for printing… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancel +pdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser. +pdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggle Sidebar +pdfjs-toggle-sidebar-button-label = Toggle Sidebar +pdfjs-document-outline-button = + .title = Show Document Outline (double-click to expand/collapse all items) +pdfjs-document-outline-button-label = Document Outline +pdfjs-attachments-button = + .title = Show Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-thumbs-button = + .title = Show Thumbnails +pdfjs-thumbs-button-label = Thumbnails +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = Find + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail of Page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find in document… +pdfjs-find-previous-button = + .title = Find the previous occurrence of the phrase +pdfjs-find-previous-button-label = Previous +pdfjs-find-next-button = + .title = Find the next occurrence of the phrase +pdfjs-find-next-button-label = Next +pdfjs-find-highlight-checkbox = Highlight all +pdfjs-find-match-case-checkbox-label = Match case +pdfjs-find-entire-word-checkbox-label = Whole words +pdfjs-find-reached-top = Reached top of document, continued from bottom +pdfjs-find-reached-bottom = Reached end of document, continued from top +pdfjs-find-not-found = Phrase not found + +## Predefined zoom values + +pdfjs-page-scale-width = Page Width +pdfjs-page-scale-fit = Page Fit +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Actual Size +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = An error occurred while loading the PDF. +pdfjs-invalid-file-error = Invalid or corrupted PDF file. +pdfjs-missing-file-error = Missing PDF file. +pdfjs-unexpected-response-error = Unexpected server response. +pdfjs-rendering-error = An error occurred while rendering the page. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = Enter the password to open this PDF file. +pdfjs-password-invalid = Invalid password. Please try again. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancel +pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/br/viewer.ftl b/public/assets/pdfjs/locale/br/viewer.ftl new file mode 100644 index 0000000..60a3df0 --- /dev/null +++ b/public/assets/pdfjs/locale/br/viewer.ftl @@ -0,0 +1,340 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pajenn a-raok +pdfjs-previous-button-label = A-raok +pdfjs-next-button = + .title = Pajenn war-lerc'h +pdfjs-next-button-label = War-lerc'h +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pajenn +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = eus { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } war { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoum bihanaat +pdfjs-zoom-out-button-label = Zoum bihanaat +pdfjs-zoom-in-button = + .title = Zoum brasaat +pdfjs-zoom-in-button-label = Zoum brasaat +pdfjs-zoom-select = + .title = Zoum +pdfjs-presentation-mode-button = + .title = Trec'haoliñ etrezek ar mod kinnigadenn +pdfjs-presentation-mode-button-label = Mod kinnigadenn +pdfjs-open-file-button = + .title = Digeriñ ur restr +pdfjs-open-file-button-label = Digeriñ ur restr +pdfjs-print-button = + .title = Moullañ +pdfjs-print-button-label = Moullañ +pdfjs-save-button = + .title = Enrollañ +pdfjs-save-button-label = Enrollañ +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Pellgargañ +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Pellgargañ +pdfjs-bookmark-button-label = Pajenn a-vremañ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ostilhoù +pdfjs-tools-button-label = Ostilhoù +pdfjs-first-page-button = + .title = Mont d'ar bajenn gentañ +pdfjs-first-page-button-label = Mont d'ar bajenn gentañ +pdfjs-last-page-button = + .title = Mont d'ar bajenn diwezhañ +pdfjs-last-page-button-label = Mont d'ar bajenn diwezhañ +pdfjs-page-rotate-cw-button = + .title = C'hwelañ gant roud ar bizied +pdfjs-page-rotate-cw-button-label = C'hwelañ gant roud ar bizied +pdfjs-page-rotate-ccw-button = + .title = C'hwelañ gant roud gin ar bizied +pdfjs-page-rotate-ccw-button-label = C'hwelañ gant roud gin ar bizied +pdfjs-cursor-text-select-tool-button = + .title = Gweredekaat an ostilh diuzañ testenn +pdfjs-cursor-text-select-tool-button-label = Ostilh diuzañ testenn +pdfjs-cursor-hand-tool-button = + .title = Gweredekaat an ostilh dorn +pdfjs-cursor-hand-tool-button-label = Ostilh dorn +pdfjs-scroll-vertical-button = + .title = Arverañ an dibunañ a-blom +pdfjs-scroll-vertical-button-label = Dibunañ a-serzh +pdfjs-scroll-horizontal-button = + .title = Arverañ an dibunañ a-blaen +pdfjs-scroll-horizontal-button-label = Dibunañ a-blaen +pdfjs-scroll-wrapped-button = + .title = Arverañ an dibunañ paket +pdfjs-scroll-wrapped-button-label = Dibunañ paket +pdfjs-spread-none-button = + .title = Chom hep stagañ ar skignadurioù +pdfjs-spread-none-button-label = Skignadenn ebet +pdfjs-spread-odd-button = + .title = Lakaat ar pajennadoù en ur gregiñ gant ar pajennoù ampar +pdfjs-spread-odd-button-label = Pajennoù ampar +pdfjs-spread-even-button = + .title = Lakaat ar pajennadoù en ur gregiñ gant ar pajennoù par +pdfjs-spread-even-button-label = Pajennoù par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Perzhioù an teul… +pdfjs-document-properties-button-label = Perzhioù an teul… +pdfjs-document-properties-file-name = Anv restr: +pdfjs-document-properties-file-size = Ment ar restr: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } Ke ({ $size_b } eizhbit) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } Me ({ $size_b } eizhbit) +pdfjs-document-properties-title = Titl: +pdfjs-document-properties-author = Aozer: +pdfjs-document-properties-subject = Danvez: +pdfjs-document-properties-keywords = Gerioù-alc'hwez: +pdfjs-document-properties-creation-date = Deiziad krouiñ: +pdfjs-document-properties-modification-date = Deiziad kemmañ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Krouer: +pdfjs-document-properties-producer = Kenderc'her PDF: +pdfjs-document-properties-version = Handelv PDF: +pdfjs-document-properties-page-count = Niver a bajennoù: +pdfjs-document-properties-page-size = Ment ar bajenn: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = poltred +pdfjs-document-properties-page-size-orientation-landscape = gweledva +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Lizher +pdfjs-document-properties-page-size-name-legal = Lezennel + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Gwel Web Herrek: +pdfjs-document-properties-linearized-yes = Ya +pdfjs-document-properties-linearized-no = Ket +pdfjs-document-properties-close-button = Serriñ + +## Print + +pdfjs-print-progress-message = O prientiñ an teul evit moullañ... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Nullañ +pdfjs-printing-not-supported = Kemenn: N'eo ket skoret penn-da-benn ar moullañ gant ar merdeer-mañ. +pdfjs-printing-not-ready = Kemenn: N'hall ket bezañ moullet ar restr PDF rak n'eo ket karget penn-da-benn. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Diskouez/kuzhat ar varrenn gostez +pdfjs-toggle-sidebar-notification-button = + .title = Trec'haoliñ ar varrenn-gostez (ur steuñv pe stagadennoù a zo en teul) +pdfjs-toggle-sidebar-button-label = Diskouez/kuzhat ar varrenn gostez +pdfjs-document-outline-button = + .title = Diskouez steuñv an teul (daouglikit evit brasaat/bihanaat an holl elfennoù) +pdfjs-document-outline-button-label = Sinedoù an teuliad +pdfjs-attachments-button = + .title = Diskouez ar c'henstagadurioù +pdfjs-attachments-button-label = Kenstagadurioù +pdfjs-layers-button = + .title = Diskouez ar gwiskadoù (daou-glikañ evit adderaouekaat an holl gwiskadoù d'o stad dre ziouer) +pdfjs-layers-button-label = Gwiskadoù +pdfjs-thumbs-button = + .title = Diskouez ar melvennoù +pdfjs-thumbs-button-label = Melvennoù +pdfjs-findbar-button = + .title = Klask e-barzh an teuliad +pdfjs-findbar-button-label = Klask +pdfjs-additional-layers = Gwiskadoù ouzhpenn + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pajenn { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Melvenn ar bajenn { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Klask + .placeholder = Klask e-barzh an teuliad +pdfjs-find-previous-button = + .title = Kavout an tamm frazenn kent o klotañ ganti +pdfjs-find-previous-button-label = Kent +pdfjs-find-next-button = + .title = Kavout an tamm frazenn war-lerc'h o klotañ ganti +pdfjs-find-next-button-label = War-lerc'h +pdfjs-find-highlight-checkbox = Usskediñ pep tra +pdfjs-find-match-case-checkbox-label = Teurel evezh ouzh ar pennlizherennoù +pdfjs-find-match-diacritics-checkbox-label = Doujañ d’an tiredoù +pdfjs-find-entire-word-checkbox-label = Gerioù a-bezh +pdfjs-find-reached-top = Tizhet eo bet derou ar bajenn, kenderc'hel diouzh an diaz +pdfjs-find-reached-bottom = Tizhet eo bet dibenn ar bajenn, kenderc'hel diouzh ar c'hrec'h +pdfjs-find-not-found = N'haller ket kavout ar frazenn + +## Predefined zoom values + +pdfjs-page-scale-width = Led ar bajenn +pdfjs-page-scale-fit = Pajenn a-bezh +pdfjs-page-scale-auto = Zoum emgefreek +pdfjs-page-scale-actual = Ment wir +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pajenn { $page } + +## Loading indicator messages + +pdfjs-loading-error = Degouezhet ez eus bet ur fazi e-pad kargañ ar PDF. +pdfjs-invalid-file-error = Restr PDF didalvoudek pe kontronet. +pdfjs-missing-file-error = Restr PDF o vankout. +pdfjs-unexpected-response-error = Respont dic'hortoz a-berzh an dafariad +pdfjs-rendering-error = Degouezhet ez eus bet ur fazi e-pad skrammañ ar bajennad. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Notennañ] + +## Password + +pdfjs-password-label = Enankit ar ger-tremen evit digeriñ ar restr PDF-mañ. +pdfjs-password-invalid = Ger-tremen didalvoudek. Klaskit en-dro mar plij. +pdfjs-password-ok-button = Mat eo +pdfjs-password-cancel-button = Nullañ +pdfjs-web-fonts-disabled = Diweredekaet eo an nodrezhoù web: n'haller ket arverañ an nodrezhoù PDF enframmet. + +## Editing + +pdfjs-editor-free-text-button = + .title = Testenn +pdfjs-editor-free-text-button-label = Testenn +pdfjs-editor-ink-button = + .title = Tresañ +pdfjs-editor-ink-button-label = Tresañ +pdfjs-editor-stamp-button = + .title = Ouzhpennañ pe aozañ skeudennoù +pdfjs-editor-stamp-button-label = Ouzhpennañ pe aozañ skeudennoù + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Liv +pdfjs-editor-free-text-size-input = Ment +pdfjs-editor-ink-color-input = Liv +pdfjs-editor-ink-thickness-input = Tevder +pdfjs-editor-ink-opacity-input = Boullder +pdfjs-editor-stamp-add-image-button = + .title = Ouzhpennañ ur skeudenn +pdfjs-editor-stamp-add-image-button-label = Ouzhpennañ ur skeudenn +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tevded +pdfjs-free-text = + .aria-label = Aozer testennoù +pdfjs-ink = + .aria-label = Aozer tresoù +pdfjs-ink-canvas = + .aria-label = Skeudenn bet krouet gant an implijer·ez + +## Alt-text dialog + +pdfjs-editor-alt-text-add-description-label = Ouzhpennañ un deskrivadur +pdfjs-editor-alt-text-cancel-button = Nullañ +pdfjs-editor-alt-text-save-button = Enrollañ + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + +pdfjs-editor-colorpicker-button = + .title = Cheñch liv +pdfjs-editor-colorpicker-yellow = + .title = Melen +pdfjs-editor-colorpicker-blue = + .title = Glas +pdfjs-editor-colorpicker-pink = + .title = Roz +pdfjs-editor-colorpicker-red = + .title = Ruz + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Diskouez pep tra +pdfjs-editor-highlight-show-all-button = + .title = Diskouez pep tra + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Gouzout hiroc’h +pdfjs-editor-new-alt-text-error-close-button = Serriñ + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-delete-model-button = Dilemel +pdfjs-editor-alt-text-settings-download-model-button = Pellgargañ +pdfjs-editor-alt-text-settings-downloading-model-button = O pellgargañ… +pdfjs-editor-alt-text-settings-close-button = Serriñ diff --git a/public/assets/pdfjs/locale/brx/viewer.ftl b/public/assets/pdfjs/locale/brx/viewer.ftl new file mode 100644 index 0000000..53ff72c --- /dev/null +++ b/public/assets/pdfjs/locale/brx/viewer.ftl @@ -0,0 +1,218 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = आगोलनि बिलाइ +pdfjs-previous-button-label = आगोलनि +pdfjs-next-button = + .title = उननि बिलाइ +pdfjs-next-button-label = उननि +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = बिलाइ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } नि +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } नि { $pageNumber }) +pdfjs-zoom-out-button = + .title = फिसायै जुम खालाम +pdfjs-zoom-out-button-label = फिसायै जुम खालाम +pdfjs-zoom-in-button = + .title = गेदेरै जुम खालाम +pdfjs-zoom-in-button-label = गेदेरै जुम खालाम +pdfjs-zoom-select = + .title = जुम खालाम +pdfjs-presentation-mode-button = + .title = दिन्थिफुंनाय म'डआव थां +pdfjs-presentation-mode-button-label = दिन्थिफुंनाय म'ड +pdfjs-open-file-button = + .title = फाइलखौ खेव +pdfjs-open-file-button-label = खेव +pdfjs-print-button = + .title = साफाय +pdfjs-print-button-label = साफाय + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = टुल +pdfjs-tools-button-label = टुल +pdfjs-first-page-button = + .title = गिबि बिलाइआव थां +pdfjs-first-page-button-label = गिबि बिलाइआव थां +pdfjs-last-page-button = + .title = जोबथा बिलाइआव थां +pdfjs-last-page-button-label = जोबथा बिलाइआव थां +pdfjs-page-rotate-cw-button = + .title = घरि गिदिंनाय फार्से फिदिं +pdfjs-page-rotate-cw-button-label = घरि गिदिंनाय फार्से फिदिं +pdfjs-page-rotate-ccw-button = + .title = घरि गिदिंनाय उल्था फार्से फिदिं +pdfjs-page-rotate-ccw-button-label = घरि गिदिंनाय उल्था फार्से फिदिं + +## Document properties dialog + +pdfjs-document-properties-button = + .title = फोरमान बिलाइनि आखुथाय... +pdfjs-document-properties-button-label = फोरमान बिलाइनि आखुथाय... +pdfjs-document-properties-file-name = फाइलनि मुं: +pdfjs-document-properties-file-size = फाइलनि महर: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } बाइट) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } बाइट) +pdfjs-document-properties-title = बिमुं: +pdfjs-document-properties-author = लिरगिरि: +pdfjs-document-properties-subject = आयदा: +pdfjs-document-properties-keywords = गाहाय सोदोब: +pdfjs-document-properties-creation-date = सोरजिनाय अक्ट': +pdfjs-document-properties-modification-date = सुद्रायनाय अक्ट': +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = सोरजिग्रा: +pdfjs-document-properties-producer = PDF दिहुनग्रा: +pdfjs-document-properties-version = PDF बिसान: +pdfjs-document-properties-page-count = बिलाइनि हिसाब: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = प'र्ट्रेट +pdfjs-document-properties-page-size-orientation-landscape = लेण्डस्केप +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = लायजाम + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = नंगौ +pdfjs-document-properties-linearized-no = नङा +pdfjs-document-properties-close-button = बन्द खालाम + +## Print + +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = नेवसि +pdfjs-printing-not-supported = सांग्रांथि: साफायनाया बे ब्राउजारजों आबुङै हेफाजाब होजाया। +pdfjs-printing-not-ready = सांग्रांथि: PDF खौ साफायनायनि थाखाय फुरायै ल'ड खालामाखै। + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = टग्गल साइडबार +pdfjs-toggle-sidebar-button-label = टग्गल साइडबार +pdfjs-document-outline-button-label = फोरमान बिलाइ सिमा हांखो +pdfjs-attachments-button = + .title = नांजाब होनायखौ दिन्थि +pdfjs-attachments-button-label = नांजाब होनाय +pdfjs-thumbs-button = + .title = थामनेइलखौ दिन्थि +pdfjs-thumbs-button-label = थामनेइल +pdfjs-findbar-button = + .title = फोरमान बिलाइआव नागिरना दिहुन +pdfjs-findbar-button-label = नायगिरना दिहुन + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = बिलाइ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = बिलाइ { $page } नि थामनेइल + +## Find panel button title and messages + +pdfjs-find-input = + .title = नायगिरना दिहुन + .placeholder = फोरमान बिलाइआव नागिरना दिहुन... +pdfjs-find-previous-button = + .title = बाथ्रा खोन्दोबनि सिगांनि नुजाथिनायखौ नागिर +pdfjs-find-previous-button-label = आगोलनि +pdfjs-find-next-button = + .title = बाथ्रा खोन्दोबनि उननि नुजाथिनायखौ नागिर +pdfjs-find-next-button-label = उननि +pdfjs-find-highlight-checkbox = गासैखौबो हाइलाइट खालाम +pdfjs-find-match-case-checkbox-label = गोरोबनाय केस +pdfjs-find-reached-top = थालो निफ्राय जागायनानै फोरमान बिलाइनि बिजौआव सौहैबाय +pdfjs-find-reached-bottom = बिजौ निफ्राय जागायनानै फोरमान बिलाइनि बिजौआव सौहैबाय +pdfjs-find-not-found = बाथ्रा खोन्दोब मोनाखै + +## Predefined zoom values + +pdfjs-page-scale-width = बिलाइनि गुवार +pdfjs-page-scale-fit = बिलाइ गोरोबनाय +pdfjs-page-scale-auto = गावनोगाव जुम +pdfjs-page-scale-actual = थार महर +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF ल'ड खालामनाय समाव मोनसे गोरोन्थि जाबाय। +pdfjs-invalid-file-error = बाहायजायै एबा गाज्रि जानाय PDF फाइल +pdfjs-missing-file-error = गोमानाय PDF फाइल +pdfjs-unexpected-response-error = मिजिंथियै सार्भार फिननाय। +pdfjs-rendering-error = बिलाइखौ राव सोलायनाय समाव मोनसे गोरोन्थि जादों। + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } सोदोब बेखेवनाय] + +## Password + +pdfjs-password-label = बे PDF फाइलखौ खेवनो पासवार्ड हाबहो। +pdfjs-password-invalid = बाहायजायै पासवार्ड। अननानै फिन नाजा। +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = नेवसि +pdfjs-web-fonts-disabled = वेब फन्टखौ लोरबां खालामबाय: अरजाबहोनाय PDF फन्टखौ बाहायनो हायाखै। + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/bs/viewer.ftl b/public/assets/pdfjs/locale/bs/viewer.ftl new file mode 100644 index 0000000..3944042 --- /dev/null +++ b/public/assets/pdfjs/locale/bs/viewer.ftl @@ -0,0 +1,223 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Prethodna strana +pdfjs-previous-button-label = Prethodna +pdfjs-next-button = + .title = Sljedeća strna +pdfjs-next-button-label = Sljedeća +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Strana +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = od { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } od { $pagesCount }) +pdfjs-zoom-out-button = + .title = Umanji +pdfjs-zoom-out-button-label = Umanji +pdfjs-zoom-in-button = + .title = Uvećaj +pdfjs-zoom-in-button-label = Uvećaj +pdfjs-zoom-select = + .title = Uvećanje +pdfjs-presentation-mode-button = + .title = Prebaci se u prezentacijski režim +pdfjs-presentation-mode-button-label = Prezentacijski režim +pdfjs-open-file-button = + .title = Otvori fajl +pdfjs-open-file-button-label = Otvori +pdfjs-print-button = + .title = Štampaj +pdfjs-print-button-label = Štampaj + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Alati +pdfjs-tools-button-label = Alati +pdfjs-first-page-button = + .title = Idi na prvu stranu +pdfjs-first-page-button-label = Idi na prvu stranu +pdfjs-last-page-button = + .title = Idi na zadnju stranu +pdfjs-last-page-button-label = Idi na zadnju stranu +pdfjs-page-rotate-cw-button = + .title = Rotiraj u smjeru kazaljke na satu +pdfjs-page-rotate-cw-button-label = Rotiraj u smjeru kazaljke na satu +pdfjs-page-rotate-ccw-button = + .title = Rotiraj suprotno smjeru kazaljke na satu +pdfjs-page-rotate-ccw-button-label = Rotiraj suprotno smjeru kazaljke na satu +pdfjs-cursor-text-select-tool-button = + .title = Omogući alat za označavanje teksta +pdfjs-cursor-text-select-tool-button-label = Alat za označavanje teksta +pdfjs-cursor-hand-tool-button = + .title = Omogući ručni alat +pdfjs-cursor-hand-tool-button-label = Ručni alat + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Svojstva dokumenta... +pdfjs-document-properties-button-label = Svojstva dokumenta... +pdfjs-document-properties-file-name = Naziv fajla: +pdfjs-document-properties-file-size = Veličina fajla: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajta) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajta) +pdfjs-document-properties-title = Naslov: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Predmet: +pdfjs-document-properties-keywords = Ključne riječi: +pdfjs-document-properties-creation-date = Datum kreiranja: +pdfjs-document-properties-modification-date = Datum promjene: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Kreator: +pdfjs-document-properties-producer = PDF stvaratelj: +pdfjs-document-properties-version = PDF verzija: +pdfjs-document-properties-page-count = Broj stranica: +pdfjs-document-properties-page-size = Veličina stranice: +pdfjs-document-properties-page-size-unit-inches = u +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = uspravno +pdfjs-document-properties-page-size-orientation-landscape = vodoravno +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Pismo +pdfjs-document-properties-page-size-name-legal = Pravni + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-close-button = Zatvori + +## Print + +pdfjs-print-progress-message = Pripremam dokument za štampu… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Otkaži +pdfjs-printing-not-supported = Upozorenje: Štampanje nije u potpunosti podržano u ovom browseru. +pdfjs-printing-not-ready = Upozorenje: PDF nije u potpunosti učitan za štampanje. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Uključi/isključi bočnu traku +pdfjs-toggle-sidebar-button-label = Uključi/isključi bočnu traku +pdfjs-document-outline-button = + .title = Prikaži outline dokumenta (dvoklik za skupljanje/širenje svih stavki) +pdfjs-document-outline-button-label = Konture dokumenta +pdfjs-attachments-button = + .title = Prikaži priloge +pdfjs-attachments-button-label = Prilozi +pdfjs-thumbs-button = + .title = Prikaži thumbnailove +pdfjs-thumbs-button-label = Thumbnailovi +pdfjs-findbar-button = + .title = Pronađi u dokumentu +pdfjs-findbar-button-label = Pronađi + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Strana { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail strane { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Pronađi + .placeholder = Pronađi u dokumentu… +pdfjs-find-previous-button = + .title = Pronađi prethodno pojavljivanje fraze +pdfjs-find-previous-button-label = Prethodno +pdfjs-find-next-button = + .title = Pronađi sljedeće pojavljivanje fraze +pdfjs-find-next-button-label = Sljedeće +pdfjs-find-highlight-checkbox = Označi sve +pdfjs-find-match-case-checkbox-label = Osjetljivost na karaktere +pdfjs-find-reached-top = Dostigao sam vrh dokumenta, nastavljam sa dna +pdfjs-find-reached-bottom = Dostigao sam kraj dokumenta, nastavljam sa vrha +pdfjs-find-not-found = Fraza nije pronađena + +## Predefined zoom values + +pdfjs-page-scale-width = Širina strane +pdfjs-page-scale-fit = Uklopi stranu +pdfjs-page-scale-auto = Automatsko uvećanje +pdfjs-page-scale-actual = Stvarna veličina +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Došlo je do greške prilikom učitavanja PDF-a. +pdfjs-invalid-file-error = Neispravan ili oštećen PDF fajl. +pdfjs-missing-file-error = Nedostaje PDF fajl. +pdfjs-unexpected-response-error = Neočekivani odgovor servera. +pdfjs-rendering-error = Došlo je do greške prilikom renderiranja strane. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } pribilješka] + +## Password + +pdfjs-password-label = Upišite lozinku da biste otvorili ovaj PDF fajl. +pdfjs-password-invalid = Pogrešna lozinka. Pokušajte ponovo. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Otkaži +pdfjs-web-fonts-disabled = Web fontovi su onemogućeni: nemoguće koristiti ubačene PDF fontove. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ca/viewer.ftl b/public/assets/pdfjs/locale/ca/viewer.ftl new file mode 100644 index 0000000..7417741 --- /dev/null +++ b/public/assets/pdfjs/locale/ca/viewer.ftl @@ -0,0 +1,313 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pàgina anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Pàgina següent +pdfjs-next-button-label = Següent +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pàgina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Redueix +pdfjs-zoom-out-button-label = Redueix +pdfjs-zoom-in-button = + .title = Amplia +pdfjs-zoom-in-button-label = Amplia +pdfjs-zoom-select = + .title = Escala +pdfjs-presentation-mode-button = + .title = Canvia al mode de presentació +pdfjs-presentation-mode-button-label = Mode de presentació +pdfjs-open-file-button = + .title = Obre el fitxer +pdfjs-open-file-button-label = Obre +pdfjs-print-button = + .title = Imprimeix +pdfjs-print-button-label = Imprimeix +pdfjs-save-button = + .title = Desa +pdfjs-save-button-label = Desa +pdfjs-bookmark-button = + .title = Pàgina actual (mostra l'URL de la pàgina actual) +pdfjs-bookmark-button-label = Pàgina actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Eines +pdfjs-tools-button-label = Eines +pdfjs-first-page-button = + .title = Vés a la primera pàgina +pdfjs-first-page-button-label = Vés a la primera pàgina +pdfjs-last-page-button = + .title = Vés a l'última pàgina +pdfjs-last-page-button-label = Vés a l'última pàgina +pdfjs-page-rotate-cw-button = + .title = Gira cap a la dreta +pdfjs-page-rotate-cw-button-label = Gira cap a la dreta +pdfjs-page-rotate-ccw-button = + .title = Gira cap a l'esquerra +pdfjs-page-rotate-ccw-button-label = Gira cap a l'esquerra +pdfjs-cursor-text-select-tool-button = + .title = Habilita l'eina de selecció de text +pdfjs-cursor-text-select-tool-button-label = Eina de selecció de text +pdfjs-cursor-hand-tool-button = + .title = Habilita l'eina de mà +pdfjs-cursor-hand-tool-button-label = Eina de mà +pdfjs-scroll-page-button = + .title = Usa el desplaçament de pàgina +pdfjs-scroll-page-button-label = Desplaçament de pàgina +pdfjs-scroll-vertical-button = + .title = Utilitza el desplaçament vertical +pdfjs-scroll-vertical-button-label = Desplaçament vertical +pdfjs-scroll-horizontal-button = + .title = Utilitza el desplaçament horitzontal +pdfjs-scroll-horizontal-button-label = Desplaçament horitzontal +pdfjs-scroll-wrapped-button = + .title = Activa el desplaçament continu +pdfjs-scroll-wrapped-button-label = Desplaçament continu +pdfjs-spread-none-button = + .title = No agrupis les pàgines de dues en dues +pdfjs-spread-none-button-label = Una sola pàgina +pdfjs-spread-odd-button = + .title = Mostra dues pàgines començant per les pàgines de numeració senar +pdfjs-spread-odd-button-label = Doble pàgina (senar) +pdfjs-spread-even-button = + .title = Mostra dues pàgines començant per les pàgines de numeració parell +pdfjs-spread-even-button-label = Doble pàgina (parell) + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propietats del document… +pdfjs-document-properties-button-label = Propietats del document… +pdfjs-document-properties-file-name = Nom del fitxer: +pdfjs-document-properties-file-size = Mida del fitxer: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Títol: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Assumpte: +pdfjs-document-properties-keywords = Paraules clau: +pdfjs-document-properties-creation-date = Data de creació: +pdfjs-document-properties-modification-date = Data de modificació: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creador: +pdfjs-document-properties-producer = Generador de PDF: +pdfjs-document-properties-version = Versió de PDF: +pdfjs-document-properties-page-count = Nombre de pàgines: +pdfjs-document-properties-page-size = Mida de la pàgina: +pdfjs-document-properties-page-size-unit-inches = polzades +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = apaïsat +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web ràpida: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Tanca + +## Print + +pdfjs-print-progress-message = S'està preparant la impressió del document… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancel·la +pdfjs-printing-not-supported = Avís: la impressió no és plenament funcional en aquest navegador. +pdfjs-printing-not-ready = Atenció: el PDF no s'ha acabat de carregar per imprimir-lo. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Mostra/amaga la barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Mostra/amaga la barra lateral (el document conté un esquema, adjuncions o capes) +pdfjs-toggle-sidebar-button-label = Mostra/amaga la barra lateral +pdfjs-document-outline-button = + .title = Mostra l'esquema del document (doble clic per ampliar/reduir tots els elements) +pdfjs-document-outline-button-label = Esquema del document +pdfjs-attachments-button = + .title = Mostra les adjuncions +pdfjs-attachments-button-label = Adjuncions +pdfjs-layers-button = + .title = Mostra les capes (doble clic per restablir totes les capes al seu estat per defecte) +pdfjs-layers-button-label = Capes +pdfjs-thumbs-button = + .title = Mostra les miniatures +pdfjs-thumbs-button-label = Miniatures +pdfjs-current-outline-item-button = + .title = Cerca l'element d'esquema actual +pdfjs-current-outline-item-button-label = Element d'esquema actual +pdfjs-findbar-button = + .title = Cerca al document +pdfjs-findbar-button-label = Cerca +pdfjs-additional-layers = Capes addicionals + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pàgina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de la pàgina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Cerca + .placeholder = Cerca al document… +pdfjs-find-previous-button = + .title = Cerca l'anterior coincidència de l'expressió +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Cerca la següent coincidència de l'expressió +pdfjs-find-next-button-label = Següent +pdfjs-find-highlight-checkbox = Ressalta-ho tot +pdfjs-find-match-case-checkbox-label = Distingeix entre majúscules i minúscules +pdfjs-find-match-diacritics-checkbox-label = Respecta els diacrítics +pdfjs-find-entire-word-checkbox-label = Paraules senceres +pdfjs-find-reached-top = S'ha arribat al principi del document, es continua pel final +pdfjs-find-reached-bottom = S'ha arribat al final del document, es continua pel principi +pdfjs-find-not-found = No s'ha trobat l'expressió + +## Predefined zoom values + +pdfjs-page-scale-width = Amplada de la pàgina +pdfjs-page-scale-fit = Ajusta la pàgina +pdfjs-page-scale-auto = Zoom automàtic +pdfjs-page-scale-actual = Mida real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pàgina { $page } + +## Loading indicator messages + +pdfjs-loading-error = S'ha produït un error en carregar el PDF. +pdfjs-invalid-file-error = El fitxer PDF no és vàlid o està malmès. +pdfjs-missing-file-error = Falta el fitxer PDF. +pdfjs-unexpected-response-error = Resposta inesperada del servidor. +pdfjs-rendering-error = S'ha produït un error mentre es renderitzava la pàgina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotació { $type }] + +## Password + +pdfjs-password-label = Introduïu la contrasenya per obrir aquest fitxer PDF. +pdfjs-password-invalid = La contrasenya no és vàlida. Torneu-ho a provar. +pdfjs-password-ok-button = D'acord +pdfjs-password-cancel-button = Cancel·la +pdfjs-web-fonts-disabled = Els tipus de lletra web estan desactivats: no es poden utilitzar els tipus de lletra incrustats al PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Dibuixa +pdfjs-editor-ink-button-label = Dibuixa + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Mida +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Gruix +pdfjs-editor-ink-opacity-input = Opacitat +pdfjs-free-text = + .aria-label = Editor de text +pdfjs-free-text-default-content = Escriviu… +pdfjs-ink = + .aria-label = Editor de dibuix +pdfjs-ink-canvas = + .aria-label = Imatge creada per l'usuari + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/cak/viewer.ftl b/public/assets/pdfjs/locale/cak/viewer.ftl new file mode 100644 index 0000000..f40c1e9 --- /dev/null +++ b/public/assets/pdfjs/locale/cak/viewer.ftl @@ -0,0 +1,291 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Jun kan ruxaq +pdfjs-previous-button-label = Jun kan +pdfjs-next-button = + .title = Jun chik ruxaq +pdfjs-next-button-label = Jun chik +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Ruxaq +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = richin { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } richin { $pagesCount }) +pdfjs-zoom-out-button = + .title = Tich'utinirisäx +pdfjs-zoom-out-button-label = Tich'utinirisäx +pdfjs-zoom-in-button = + .title = Tinimirisäx +pdfjs-zoom-in-button-label = Tinimirisäx +pdfjs-zoom-select = + .title = Sum +pdfjs-presentation-mode-button = + .title = Tijal ri rub'anikil niwachin +pdfjs-presentation-mode-button-label = Pa rub'eyal niwachin +pdfjs-open-file-button = + .title = Tijaq Yakb'äl +pdfjs-open-file-button-label = Tijaq +pdfjs-print-button = + .title = Titz'ajb'äx +pdfjs-print-button-label = Titz'ajb'äx +pdfjs-save-button = + .title = Tiyak +pdfjs-save-button-label = Tiyak +pdfjs-bookmark-button-label = Ruxaq k'o wakami + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Samajib'äl +pdfjs-tools-button-label = Samajib'äl +pdfjs-first-page-button = + .title = Tib'e pa nab'ey ruxaq +pdfjs-first-page-button-label = Tib'e pa nab'ey ruxaq +pdfjs-last-page-button = + .title = Tib'e pa ruk'isib'äl ruxaq +pdfjs-last-page-button-label = Tib'e pa ruk'isib'äl ruxaq +pdfjs-page-rotate-cw-button = + .title = Tisutïx pan ajkiq'a' +pdfjs-page-rotate-cw-button-label = Tisutïx pan ajkiq'a' +pdfjs-page-rotate-ccw-button = + .title = Tisutïx pan ajxokon +pdfjs-page-rotate-ccw-button-label = Tisutïx pan ajxokon +pdfjs-cursor-text-select-tool-button = + .title = Titzij ri rusamajib'al Rucha'ik Rucholajem Tzij +pdfjs-cursor-text-select-tool-button-label = Rusamajib'al Rucha'ik Rucholajem Tzij +pdfjs-cursor-hand-tool-button = + .title = Titzij ri q'ab'aj samajib'äl +pdfjs-cursor-hand-tool-button-label = Q'ab'aj Samajib'äl +pdfjs-scroll-page-button = + .title = Tokisäx Ruxaq Q'axanem +pdfjs-scroll-page-button-label = Ruxaq Q'axanem +pdfjs-scroll-vertical-button = + .title = Tokisäx Pa'äl Q'axanem +pdfjs-scroll-vertical-button-label = Pa'äl Q'axanem +pdfjs-scroll-horizontal-button = + .title = Tokisäx Kotz'öl Q'axanem +pdfjs-scroll-horizontal-button-label = Kotz'öl Q'axanem +pdfjs-scroll-wrapped-button = + .title = Tokisäx Tzub'aj Q'axanem +pdfjs-scroll-wrapped-button-label = Tzub'aj Q'axanem +pdfjs-spread-none-button = + .title = Man ketun taq ruxaq pa rub'eyal wuj +pdfjs-spread-none-button-label = Majun Rub'eyal +pdfjs-spread-odd-button = + .title = Ke'atunu' ri taq ruxaq rik'in natikirisaj rik'in jun man k'ulaj ta rajilab'al +pdfjs-spread-odd-button-label = Man K'ulaj Ta Rub'eyal +pdfjs-spread-even-button = + .title = Ke'atunu' ri taq ruxaq rik'in natikirisaj rik'in jun k'ulaj rajilab'al +pdfjs-spread-even-button-label = K'ulaj Rub'eyal + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Taq richinil wuj… +pdfjs-document-properties-button-label = Taq richinil wuj… +pdfjs-document-properties-file-name = Rub'i' yakb'äl: +pdfjs-document-properties-file-size = Runimilem yakb'äl: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = B'i'aj: +pdfjs-document-properties-author = B'anel: +pdfjs-document-properties-subject = Taqikil: +pdfjs-document-properties-keywords = Kixe'el taq tzij: +pdfjs-document-properties-creation-date = Ruq'ijul xtz'uk: +pdfjs-document-properties-modification-date = Ruq'ijul xjalwachïx: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Q'inonel: +pdfjs-document-properties-producer = PDF b'anöy: +pdfjs-document-properties-version = PDF ruwäch: +pdfjs-document-properties-page-count = Jarupe' ruxaq: +pdfjs-document-properties-page-size = Runimilem ri Ruxaq: +pdfjs-document-properties-page-size-unit-inches = pa +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = rupalem +pdfjs-document-properties-page-size-orientation-landscape = rukotz'olem +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Loman wuj +pdfjs-document-properties-page-size-name-legal = Taqanel tzijol + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Anin Rutz'etik Ajk'amaya'l: +pdfjs-document-properties-linearized-yes = Ja' +pdfjs-document-properties-linearized-no = Mani +pdfjs-document-properties-close-button = Titz'apïx + +## Print + +pdfjs-print-progress-message = Ruchojmirisaxik wuj richin nitz'ajb'äx… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Tiq'at +pdfjs-printing-not-supported = Rutzijol k'ayewal: Ri rutz'ajb'axik man koch'el ta ronojel pa re okik'amaya'l re'. +pdfjs-printing-not-ready = Rutzijol k'ayewal: Ri PDF man xusamajij ta ronojel richin nitz'ajb'äx. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Tijal ri ajxikin kajtz'ik +pdfjs-toggle-sidebar-notification-button = + .title = Tik'ex ri ajxikin yuqkajtz'ik (ri wuj eruk'wan taq ruchi'/taqo/kuchuj) +pdfjs-toggle-sidebar-button-label = Tijal ri ajxikin kajtz'ik +pdfjs-document-outline-button = + .title = Tik'ut pe ruch'akulal wuj (kamul-pitz'oj richin nirik'/nich'utinirisäx ronojel ruch'akulal) +pdfjs-document-outline-button-label = Ruch'akulal wuj +pdfjs-attachments-button = + .title = Kek'ut pe ri taq taqoj +pdfjs-attachments-button-label = Taq taqoj +pdfjs-layers-button = + .title = Kek'ut taq Kuchuj (ka'i'-pitz' richin yetzolïx ronojel ri taq kuchuj e k'o wi) +pdfjs-layers-button-label = Taq kuchuj +pdfjs-thumbs-button = + .title = Kek'ut pe taq ch'utiq +pdfjs-thumbs-button-label = Koköj +pdfjs-current-outline-item-button = + .title = Kekanöx Taq Ch'akulal Kik'wan Chib'äl +pdfjs-current-outline-item-button-label = Taq Ch'akulal Kik'wan Chib'äl +pdfjs-findbar-button = + .title = Tikanöx chupam ri wuj +pdfjs-findbar-button-label = Tikanöx +pdfjs-additional-layers = Tz'aqat ta Kuchuj + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Ruxaq { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Ruch'utinirisaxik ruxaq { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Tikanöx + .placeholder = Tikanöx pa wuj… +pdfjs-find-previous-button = + .title = Tib'an b'enam pa ri jun kan q'aptzij xilitäj +pdfjs-find-previous-button-label = Jun kan +pdfjs-find-next-button = + .title = Tib'e pa ri jun chik pajtzij xilitäj +pdfjs-find-next-button-label = Jun chik +pdfjs-find-highlight-checkbox = Tiya' retal ronojel +pdfjs-find-match-case-checkbox-label = Tuk'äm ri' kik'in taq nimatz'ib' chuqa' taq ch'utitz'ib' +pdfjs-find-match-diacritics-checkbox-label = Tiya' Kikojol Tz'aqat taq Tz'ib' +pdfjs-find-entire-word-checkbox-label = Tz'aqät taq tzij +pdfjs-find-reached-top = Xb'eq'i' ri rutikirib'al wuj, xtikanöx k'a pa ruk'isib'äl +pdfjs-find-reached-bottom = Xb'eq'i' ri ruk'isib'äl wuj, xtikanöx pa rutikirib'al +pdfjs-find-not-found = Man xilitäj ta ri pajtzij + +## Predefined zoom values + +pdfjs-page-scale-width = Ruwa ruxaq +pdfjs-page-scale-fit = Tinuk' ruxaq +pdfjs-page-scale-auto = Yonil chi nimilem +pdfjs-page-scale-actual = Runimilem Wakami +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Ruxaq { $page } + +## Loading indicator messages + +pdfjs-loading-error = Xk'ulwachitäj jun sach'oj toq xnuk'ux ri PDF . +pdfjs-invalid-file-error = Man oke ta o yujtajinäq ri PDF yakb'äl. +pdfjs-missing-file-error = Man xilitäj ta ri PDF yakb'äl. +pdfjs-unexpected-response-error = Man oyob'en ta tz'olin rutzij ruk'u'x samaj. +pdfjs-rendering-error = Xk'ulwachitäj jun sachoj toq ninuk'wachij ri ruxaq. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Tz'ib'anïk] + +## Password + +pdfjs-password-label = Tatz'ib'aj ri ewan tzij richin najäq re yakb'äl re' pa PDF. +pdfjs-password-invalid = Man okel ta ri ewan tzij: Tatojtob'ej chik. +pdfjs-password-ok-button = Ütz +pdfjs-password-cancel-button = Tiq'at +pdfjs-web-fonts-disabled = E chupül ri taq ajk'amaya'l tz'ib': man tikirel ta nokisäx ri taq tz'ib' PDF pa ch'ikenïk + +## Editing + +pdfjs-editor-free-text-button = + .title = Rucholajem tz'ib' +pdfjs-editor-free-text-button-label = Rucholajem tz'ib' +pdfjs-editor-ink-button = + .title = Tiwachib'ëx +pdfjs-editor-ink-button-label = Tiwachib'ëx +# Editor Parameters +pdfjs-editor-free-text-color-input = B'onil +pdfjs-editor-free-text-size-input = Nimilem +pdfjs-editor-ink-color-input = B'onil +pdfjs-editor-ink-thickness-input = Rupimil +pdfjs-editor-ink-opacity-input = Q'equmal +pdfjs-free-text = + .aria-label = Nuk'unel tz'ib'atzij +pdfjs-free-text-default-content = Titikitisäx rutz'ib'axik… +pdfjs-ink = + .aria-label = Nuk'unel wachib'äl +pdfjs-ink-canvas = + .aria-label = Wachib'äl nuk'un ruma okisaxel + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ckb/viewer.ftl b/public/assets/pdfjs/locale/ckb/viewer.ftl new file mode 100644 index 0000000..ae87335 --- /dev/null +++ b/public/assets/pdfjs/locale/ckb/viewer.ftl @@ -0,0 +1,242 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = پەڕەی پێشوو +pdfjs-previous-button-label = پێشوو +pdfjs-next-button = + .title = پەڕەی دوواتر +pdfjs-next-button-label = دوواتر +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = پەرە +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = لە { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } لە { $pagesCount }) +pdfjs-zoom-out-button = + .title = ڕۆچوونی +pdfjs-zoom-out-button-label = ڕۆچوونی +pdfjs-zoom-in-button = + .title = هێنانەپێش +pdfjs-zoom-in-button-label = هێنانەپێش +pdfjs-zoom-select = + .title = زووم +pdfjs-presentation-mode-button = + .title = گۆڕین بۆ دۆخی پێشکەشکردن +pdfjs-presentation-mode-button-label = دۆخی پێشکەشکردن +pdfjs-open-file-button = + .title = پەڕگە بکەرەوە +pdfjs-open-file-button-label = کردنەوە +pdfjs-print-button = + .title = چاپکردن +pdfjs-print-button-label = چاپکردن + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ئامرازەکان +pdfjs-tools-button-label = ئامرازەکان +pdfjs-first-page-button = + .title = برۆ بۆ یەکەم پەڕە +pdfjs-first-page-button-label = بڕۆ بۆ یەکەم پەڕە +pdfjs-last-page-button = + .title = بڕۆ بۆ کۆتا پەڕە +pdfjs-last-page-button-label = بڕۆ بۆ کۆتا پەڕە +pdfjs-page-rotate-cw-button = + .title = ئاڕاستەی میلی کاتژمێر +pdfjs-page-rotate-cw-button-label = ئاڕاستەی میلی کاتژمێر +pdfjs-page-rotate-ccw-button = + .title = پێچەوانەی میلی کاتژمێر +pdfjs-page-rotate-ccw-button-label = پێچەوانەی میلی کاتژمێر +pdfjs-cursor-text-select-tool-button = + .title = توڵامرازی نیشانکەری دەق چالاک بکە +pdfjs-cursor-text-select-tool-button-label = توڵامرازی نیشانکەری دەق +pdfjs-cursor-hand-tool-button = + .title = توڵامرازی دەستی چالاک بکە +pdfjs-cursor-hand-tool-button-label = توڵامرازی دەستی +pdfjs-scroll-vertical-button = + .title = ناردنی ئەستوونی بەکاربێنە +pdfjs-scroll-vertical-button-label = ناردنی ئەستوونی +pdfjs-scroll-horizontal-button = + .title = ناردنی ئاسۆیی بەکاربێنە +pdfjs-scroll-horizontal-button-label = ناردنی ئاسۆیی +pdfjs-scroll-wrapped-button = + .title = ناردنی لوولکراو بەکاربێنە +pdfjs-scroll-wrapped-button-label = ناردنی لوولکراو + +## Document properties dialog + +pdfjs-document-properties-button = + .title = تایبەتمەندییەکانی بەڵگەنامە... +pdfjs-document-properties-button-label = تایبەتمەندییەکانی بەڵگەنامە... +pdfjs-document-properties-file-name = ناوی پەڕگە: +pdfjs-document-properties-file-size = قەبارەی پەڕگە: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } کب ({ $size_b } بایت) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } مب ({ $size_b } بایت) +pdfjs-document-properties-title = سەردێڕ: +pdfjs-document-properties-author = نووسەر +pdfjs-document-properties-subject = بابەت: +pdfjs-document-properties-keywords = کلیلەوشە: +pdfjs-document-properties-creation-date = بەرواری درووستکردن: +pdfjs-document-properties-modification-date = بەرواری دەستکاریکردن: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = درووستکەر: +pdfjs-document-properties-producer = بەرهەمهێنەری PDF: +pdfjs-document-properties-version = وەشانی PDF: +pdfjs-document-properties-page-count = ژمارەی پەرەکان: +pdfjs-document-properties-page-size = قەبارەی پەڕە: +pdfjs-document-properties-page-size-unit-inches = ئینچ +pdfjs-document-properties-page-size-unit-millimeters = ملم +pdfjs-document-properties-page-size-orientation-portrait = پۆرترەیت(درێژ) +pdfjs-document-properties-page-size-orientation-landscape = پانیی +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = نامە +pdfjs-document-properties-page-size-name-legal = یاسایی + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = پیشاندانی وێبی خێرا: +pdfjs-document-properties-linearized-yes = بەڵێ +pdfjs-document-properties-linearized-no = نەخێر +pdfjs-document-properties-close-button = داخستن + +## Print + +pdfjs-print-progress-message = بەڵگەنامە ئامادەدەکرێت بۆ چاپکردن... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = پاشگەزبوونەوە +pdfjs-printing-not-supported = ئاگاداربە: چاپکردن بە تەواوی پشتگیر ناکرێت لەم وێبگەڕە. +pdfjs-printing-not-ready = ئاگاداربە: PDF بە تەواوی بارنەبووە بۆ چاپکردن. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = لاتەنیشت پیشاندان/شاردنەوە +pdfjs-toggle-sidebar-button-label = لاتەنیشت پیشاندان/شاردنەوە +pdfjs-document-outline-button-label = سنووری چوارچێوە +pdfjs-attachments-button = + .title = پاشکۆکان پیشان بدە +pdfjs-attachments-button-label = پاشکۆکان +pdfjs-layers-button-label = چینەکان +pdfjs-thumbs-button = + .title = وێنۆچکە پیشان بدە +pdfjs-thumbs-button-label = وێنۆچکە +pdfjs-findbar-button = + .title = لە بەڵگەنامە بگەرێ +pdfjs-findbar-button-label = دۆزینەوە +pdfjs-additional-layers = چینی زیاتر + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = پەڕەی { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = وێنۆچکەی پەڕەی { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = دۆزینەوە + .placeholder = لە بەڵگەنامە بگەرێ... +pdfjs-find-previous-button = + .title = هەبوونی پێشوو بدۆزرەوە لە ڕستەکەدا +pdfjs-find-previous-button-label = پێشوو +pdfjs-find-next-button = + .title = هەبوونی داهاتوو بدۆزەرەوە لە ڕستەکەدا +pdfjs-find-next-button-label = دوواتر +pdfjs-find-highlight-checkbox = هەمووی نیشانە بکە +pdfjs-find-match-case-checkbox-label = دۆخی لەیەکچوون +pdfjs-find-entire-word-checkbox-label = هەموو وشەکان +pdfjs-find-reached-top = گەشتیتە سەرەوەی بەڵگەنامە، لە خوارەوە دەستت پێکرد +pdfjs-find-reached-bottom = گەشتیتە کۆتایی بەڵگەنامە. لەسەرەوە دەستت پێکرد +pdfjs-find-not-found = نووسین نەدۆزرایەوە + +## Predefined zoom values + +pdfjs-page-scale-width = پانی پەڕە +pdfjs-page-scale-fit = پڕبوونی پەڕە +pdfjs-page-scale-auto = زوومی خۆکار +pdfjs-page-scale-actual = قەبارەی ڕاستی +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = هەڵەیەک ڕوویدا لە کاتی بارکردنی PDF. +pdfjs-invalid-file-error = پەڕگەی pdf تێکچووە یان نەگونجاوە. +pdfjs-missing-file-error = پەڕگەی pdf بوونی نیە. +pdfjs-unexpected-response-error = وەڵامی ڕاژەخوازی نەخوازراو. +pdfjs-rendering-error = هەڵەیەک ڕوویدا لە کاتی پوختەکردنی (ڕێندەر) پەڕە. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } سەرنج] + +## Password + +pdfjs-password-label = وشەی تێپەڕ بنووسە بۆ کردنەوەی پەڕگەی pdf. +pdfjs-password-invalid = وشەی تێپەڕ هەڵەیە. تکایە دووبارە هەوڵ بدەرەوە. +pdfjs-password-ok-button = باشە +pdfjs-password-cancel-button = پاشگەزبوونەوە +pdfjs-web-fonts-disabled = جۆرەپیتی وێب ناچالاکە: نەتوانی جۆرەپیتی تێخراوی ناو pdfـەکە بەکاربێت. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/cs/viewer.ftl b/public/assets/pdfjs/locale/cs/viewer.ftl new file mode 100644 index 0000000..696fe30 --- /dev/null +++ b/public/assets/pdfjs/locale/cs/viewer.ftl @@ -0,0 +1,521 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Přejde na předchozí stránku +pdfjs-previous-button-label = Předchozí +pdfjs-next-button = + .title = Přejde na následující stránku +pdfjs-next-button-label = Další +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Stránka +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = z { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } z { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zmenší velikost +pdfjs-zoom-out-button-label = Zmenšit +pdfjs-zoom-in-button = + .title = Zvětší velikost +pdfjs-zoom-in-button-label = Zvětšit +pdfjs-zoom-select = + .title = Nastaví velikost +pdfjs-presentation-mode-button = + .title = Přepne do režimu prezentace +pdfjs-presentation-mode-button-label = Režim prezentace +pdfjs-open-file-button = + .title = Otevře soubor +pdfjs-open-file-button-label = Otevřít +pdfjs-print-button = + .title = Vytiskne dokument +pdfjs-print-button-label = Vytisknout +pdfjs-save-button = + .title = Uložit +pdfjs-save-button-label = Uložit +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Stáhnout +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Stáhnout +pdfjs-bookmark-button = + .title = Aktuální stránka (zobrazit URL od aktuální stránky) +pdfjs-bookmark-button-label = Aktuální stránka + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Nástroje +pdfjs-tools-button-label = Nástroje +pdfjs-first-page-button = + .title = Přejde na první stránku +pdfjs-first-page-button-label = Přejít na první stránku +pdfjs-last-page-button = + .title = Přejde na poslední stránku +pdfjs-last-page-button-label = Přejít na poslední stránku +pdfjs-page-rotate-cw-button = + .title = Otočí po směru hodin +pdfjs-page-rotate-cw-button-label = Otočit po směru hodin +pdfjs-page-rotate-ccw-button = + .title = Otočí proti směru hodin +pdfjs-page-rotate-ccw-button-label = Otočit proti směru hodin +pdfjs-cursor-text-select-tool-button = + .title = Povolí výběr textu +pdfjs-cursor-text-select-tool-button-label = Výběr textu +pdfjs-cursor-hand-tool-button = + .title = Povolí nástroj ručička +pdfjs-cursor-hand-tool-button-label = Nástroj ručička +pdfjs-scroll-page-button = + .title = Posouvat po stránkách +pdfjs-scroll-page-button-label = Posouvání po stránkách +pdfjs-scroll-vertical-button = + .title = Použít svislé posouvání +pdfjs-scroll-vertical-button-label = Svislé posouvání +pdfjs-scroll-horizontal-button = + .title = Použít vodorovné posouvání +pdfjs-scroll-horizontal-button-label = Vodorovné posouvání +pdfjs-scroll-wrapped-button = + .title = Použít postupné posouvání +pdfjs-scroll-wrapped-button-label = Postupné posouvání +pdfjs-spread-none-button = + .title = Nesdružovat stránky +pdfjs-spread-none-button-label = Žádné sdružení +pdfjs-spread-odd-button = + .title = Sdruží stránky s umístěním lichých vlevo +pdfjs-spread-odd-button-label = Sdružení stránek (liché vlevo) +pdfjs-spread-even-button = + .title = Sdruží stránky s umístěním sudých vlevo +pdfjs-spread-even-button-label = Sdružení stránek (sudé vlevo) + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Vlastnosti dokumentu… +pdfjs-document-properties-button-label = Vlastnosti dokumentu… +pdfjs-document-properties-file-name = Název souboru: +pdfjs-document-properties-file-size = Velikost souboru: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bajtů) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtů) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajtů) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtů) +pdfjs-document-properties-title = Název stránky: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Předmět: +pdfjs-document-properties-keywords = Klíčová slova: +pdfjs-document-properties-creation-date = Datum vytvoření: +pdfjs-document-properties-modification-date = Datum úpravy: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Vytvořil: +pdfjs-document-properties-producer = Tvůrce PDF: +pdfjs-document-properties-version = Verze PDF: +pdfjs-document-properties-page-count = Počet stránek: +pdfjs-document-properties-page-size = Velikost stránky: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = na výšku +pdfjs-document-properties-page-size-orientation-landscape = na šířku +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Dopis +pdfjs-document-properties-page-size-name-legal = Právní dokument + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Rychlé zobrazování z webu: +pdfjs-document-properties-linearized-yes = Ano +pdfjs-document-properties-linearized-no = Ne +pdfjs-document-properties-close-button = Zavřít + +## Print + +pdfjs-print-progress-message = Příprava dokumentu pro tisk… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Zrušit +pdfjs-printing-not-supported = Upozornění: Tisk není v tomto prohlížeči plně podporován. +pdfjs-printing-not-ready = Upozornění: Dokument PDF není kompletně načten. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Postranní lišta +pdfjs-toggle-sidebar-notification-button = + .title = Přepnout postranní lištu (dokument obsahuje osnovu/přílohy/vrstvy) +pdfjs-toggle-sidebar-button-label = Postranní lišta +pdfjs-document-outline-button = + .title = Zobrazí osnovu dokumentu (poklepání přepne zobrazení všech položek) +pdfjs-document-outline-button-label = Osnova dokumentu +pdfjs-attachments-button = + .title = Zobrazí přílohy +pdfjs-attachments-button-label = Přílohy +pdfjs-layers-button = + .title = Zobrazit vrstvy (poklepáním obnovíte všechny vrstvy do výchozího stavu) +pdfjs-layers-button-label = Vrstvy +pdfjs-thumbs-button = + .title = Zobrazí náhledy +pdfjs-thumbs-button-label = Náhledy +pdfjs-current-outline-item-button = + .title = Najít aktuální položku v osnově +pdfjs-current-outline-item-button-label = Aktuální položka v osnově +pdfjs-findbar-button = + .title = Najde v dokumentu +pdfjs-findbar-button-label = Najít +pdfjs-additional-layers = Další vrstvy + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Strana { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Náhled strany { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Najít + .placeholder = Najít v dokumentu… +pdfjs-find-previous-button = + .title = Najde předchozí výskyt hledaného textu +pdfjs-find-previous-button-label = Předchozí +pdfjs-find-next-button = + .title = Najde další výskyt hledaného textu +pdfjs-find-next-button-label = Další +pdfjs-find-highlight-checkbox = Zvýraznit +pdfjs-find-match-case-checkbox-label = Rozlišovat velikost +pdfjs-find-match-diacritics-checkbox-label = Rozlišovat diakritiku +pdfjs-find-entire-word-checkbox-label = Celá slova +pdfjs-find-reached-top = Dosažen začátek dokumentu, pokračuje se od konce +pdfjs-find-reached-bottom = Dosažen konec dokumentu, pokračuje se od začátku +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current }. z { $total } výskytu + [few] { $current }. z { $total } výskytů + [many] { $current }. z { $total } výskytů + *[other] { $current }. z { $total } výskytů + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Více než { $limit } výskyt + [few] Více než { $limit } výskyty + [many] Více než { $limit } výskytů + *[other] Více než { $limit } výskytů + } +pdfjs-find-not-found = Hledaný text nenalezen + +## Predefined zoom values + +pdfjs-page-scale-width = Podle šířky +pdfjs-page-scale-fit = Podle výšky +pdfjs-page-scale-auto = Automatická velikost +pdfjs-page-scale-actual = Skutečná velikost +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Strana { $page } + +## Loading indicator messages + +pdfjs-loading-error = Při nahrávání PDF nastala chyba. +pdfjs-invalid-file-error = Neplatný nebo chybný soubor PDF. +pdfjs-missing-file-error = Chybí soubor PDF. +pdfjs-unexpected-response-error = Neočekávaná odpověď serveru. +pdfjs-rendering-error = Při vykreslování stránky nastala chyba. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotace typu { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Pro otevření PDF souboru vložte heslo. +pdfjs-password-invalid = Neplatné heslo. Zkuste to znovu. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Zrušit +pdfjs-web-fonts-disabled = Webová písma jsou zakázána, proto není možné použít vložená písma PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Kreslení +pdfjs-editor-ink-button-label = Kreslení +pdfjs-editor-stamp-button = + .title = Přidání či úprava obrázků +pdfjs-editor-stamp-button-label = Přidání či úprava obrázků +pdfjs-editor-highlight-button = + .title = Zvýraznění +pdfjs-editor-highlight-button-label = Zvýraznění +pdfjs-highlight-floating-button1 = + .title = Zvýraznit + .aria-label = Zvýraznit +pdfjs-highlight-floating-button-label = Zvýraznit + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Odebrat kresbu +pdfjs-editor-remove-freetext-button = + .title = Odebrat text +pdfjs-editor-remove-stamp-button = + .title = Odebrat obrázek +pdfjs-editor-remove-highlight-button = + .title = Odebrat zvýraznění + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Barva +pdfjs-editor-free-text-size-input = Velikost +pdfjs-editor-ink-color-input = Barva +pdfjs-editor-ink-thickness-input = Tloušťka +pdfjs-editor-ink-opacity-input = Průhlednost +pdfjs-editor-stamp-add-image-button = + .title = Přidat obrázek +pdfjs-editor-stamp-add-image-button-label = Přidat obrázek +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tloušťka +pdfjs-editor-free-highlight-thickness-title = + .title = Změna tloušťky při zvýrazňování jiných položek než textu +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Textový editor + .default-content = Začněte psát... +pdfjs-free-text = + .aria-label = Textový editor +pdfjs-free-text-default-content = Začněte psát… +pdfjs-ink = + .aria-label = Editor kreslení +pdfjs-ink-canvas = + .aria-label = Uživatelem vytvořený obrázek + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Náhradní popis +pdfjs-editor-alt-text-edit-button = + .aria-label = Upravit alternativní text +pdfjs-editor-alt-text-edit-button-label = Upravit náhradní popis +pdfjs-editor-alt-text-dialog-label = Vyberte možnost +pdfjs-editor-alt-text-dialog-description = Náhradní popis pomáhá, když lidé obrázek nevidí nebo když se nenačítá. +pdfjs-editor-alt-text-add-description-label = Přidat popis +pdfjs-editor-alt-text-add-description-description = Snažte se o 1-2 věty, které popisují předmět, prostředí nebo činnosti. +pdfjs-editor-alt-text-mark-decorative-label = Označit jako dekorativní +pdfjs-editor-alt-text-mark-decorative-description = Používá se pro okrasné obrázky, jako jsou rámečky nebo vodoznaky. +pdfjs-editor-alt-text-cancel-button = Zrušit +pdfjs-editor-alt-text-save-button = Uložit +pdfjs-editor-alt-text-decorative-tooltip = Označen jako dekorativní +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Například: “Mladý muž si sedá ke stolu, aby se najedl.” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternativní text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Levý horní roh — změna velikosti +pdfjs-editor-resizer-label-top-middle = Horní střed — změna velikosti +pdfjs-editor-resizer-label-top-right = Pravý horní roh — změna velikosti +pdfjs-editor-resizer-label-middle-right = Vpravo uprostřed — změna velikosti +pdfjs-editor-resizer-label-bottom-right = Pravý dolní roh — změna velikosti +pdfjs-editor-resizer-label-bottom-middle = Střed dole — změna velikosti +pdfjs-editor-resizer-label-bottom-left = Levý dolní roh — změna velikosti +pdfjs-editor-resizer-label-middle-left = Vlevo uprostřed — změna velikosti +pdfjs-editor-resizer-top-left = + .aria-label = Levý horní roh — změna velikosti +pdfjs-editor-resizer-top-middle = + .aria-label = Horní střed — změna velikosti +pdfjs-editor-resizer-top-right = + .aria-label = Pravý horní roh — změna velikosti +pdfjs-editor-resizer-middle-right = + .aria-label = Vpravo uprostřed — změna velikosti +pdfjs-editor-resizer-bottom-right = + .aria-label = Pravý dolní roh — změna velikosti +pdfjs-editor-resizer-bottom-middle = + .aria-label = Střed dole — změna velikosti +pdfjs-editor-resizer-bottom-left = + .aria-label = Levý dolní roh — změna velikosti +pdfjs-editor-resizer-middle-left = + .aria-label = Vlevo uprostřed — změna velikosti + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Barva zvýraznění +pdfjs-editor-colorpicker-button = + .title = Změna barvy +pdfjs-editor-colorpicker-dropdown = + .aria-label = Výběr barev +pdfjs-editor-colorpicker-yellow = + .title = Žlutá +pdfjs-editor-colorpicker-green = + .title = Zelená +pdfjs-editor-colorpicker-blue = + .title = Modrá +pdfjs-editor-colorpicker-pink = + .title = Růžová +pdfjs-editor-colorpicker-red = + .title = Červená + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Zobrazit vše +pdfjs-editor-highlight-show-all-button = + .title = Zobrazit vše + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Upravit alternativní text (popis obrázku) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Přidat alternativní text (popis obrázku) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Sem napište svůj popis… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Krátký popis pro lidi, kteří neuvidí obrázek nebo když se obrázek nenačítá. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Tento alternativní text byl vytvořen automaticky a může být nepřesný. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Více informací +pdfjs-editor-new-alt-text-create-automatically-button-label = Vytvořit alternativní text automaticky +pdfjs-editor-new-alt-text-not-now-button = Teď ne +pdfjs-editor-new-alt-text-error-title = Nepodařilo se automaticky vytvořit alternativní text +pdfjs-editor-new-alt-text-error-description = Napište prosím vlastní alternativní text nebo to zkuste znovu později. +pdfjs-editor-new-alt-text-error-close-button = Zavřít +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Stahuje se model AI pro alternativní texty ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Stahuje se model AI pro alternativní texty ({ $downloadedSize } z { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativní text byl přidán +pdfjs-editor-new-alt-text-added-button-label = Alternativní text byl přidán +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Chybí alternativní text +pdfjs-editor-new-alt-text-missing-button-label = Chybí alternativní text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Zkontrolovat alternativní text +pdfjs-editor-new-alt-text-to-review-button-label = Zkontrolovat alternativní text +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Vytvořeno automaticky: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Nastavení alternativního textu obrázku +pdfjs-image-alt-text-settings-button-label = Nastavení alternativního textu obrázku +pdfjs-editor-alt-text-settings-dialog-label = Nastavení alternativního textu obrázku +pdfjs-editor-alt-text-settings-automatic-title = Automatický alternativní text +pdfjs-editor-alt-text-settings-create-model-button-label = Vytvořit alternativní text automaticky +pdfjs-editor-alt-text-settings-create-model-description = Navrhuje popisy, které pomohou lidem, kteří nevidí obrázek nebo když se obrázek nenačte. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model AI pro alternativní text ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Běží lokálně na vašem zařízení, takže vaše data zůstávají v bezpečí. Vyžadováno pro automatický alternativní text. +pdfjs-editor-alt-text-settings-delete-model-button = Smazat +pdfjs-editor-alt-text-settings-download-model-button = Stáhnout +pdfjs-editor-alt-text-settings-downloading-model-button = Probíhá stahování... +pdfjs-editor-alt-text-settings-editor-title = Editor alternativního textu +pdfjs-editor-alt-text-settings-show-dialog-button-label = Při přidávání obrázku hned zobrazit editor alternativního textu +pdfjs-editor-alt-text-settings-show-dialog-description = Pomůže vám zajistit, aby všechny vaše obrázky obsahovaly alternativní text. +pdfjs-editor-alt-text-settings-close-button = Zavřít + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Zvýraznění odebráno +pdfjs-editor-undo-bar-message-freetext = Text odstraněn +pdfjs-editor-undo-bar-message-ink = Kresba odstraněna +pdfjs-editor-undo-bar-message-stamp = Obrázek odebrán +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotace odebrána + [few] { $count } anotace odebrány + [many] { $count } anotací odebráno + *[other] { $count } anotací odebráno + } +pdfjs-editor-undo-bar-undo-button = + .title = Zpět +pdfjs-editor-undo-bar-undo-button-label = Zpět +pdfjs-editor-undo-bar-close-button = + .title = Zavřít +pdfjs-editor-undo-bar-close-button-label = Zavřít diff --git a/public/assets/pdfjs/locale/cy/viewer.ftl b/public/assets/pdfjs/locale/cy/viewer.ftl new file mode 100644 index 0000000..8363835 --- /dev/null +++ b/public/assets/pdfjs/locale/cy/viewer.ftl @@ -0,0 +1,527 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Tudalen Flaenorol +pdfjs-previous-button-label = Blaenorol +pdfjs-next-button = + .title = Tudalen Nesaf +pdfjs-next-button-label = Nesaf +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Tudalen +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = o { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } o { $pagesCount }) +pdfjs-zoom-out-button = + .title = Lleihau +pdfjs-zoom-out-button-label = Lleihau +pdfjs-zoom-in-button = + .title = Cynyddu +pdfjs-zoom-in-button-label = Cynyddu +pdfjs-zoom-select = + .title = Chwyddo +pdfjs-presentation-mode-button = + .title = Newid i'r Modd Cyflwyno +pdfjs-presentation-mode-button-label = Modd Cyflwyno +pdfjs-open-file-button = + .title = Agor Ffeil +pdfjs-open-file-button-label = Agor +pdfjs-print-button = + .title = Argraffu +pdfjs-print-button-label = Argraffu +pdfjs-save-button = + .title = Cadw +pdfjs-save-button-label = Cadw +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Llwytho i lawr +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Llwytho i lawr +pdfjs-bookmark-button = + .title = Tudalen Gyfredol (Gweld URL o'r Dudalen Gyfredol) +pdfjs-bookmark-button-label = Tudalen Gyfredol + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Offer +pdfjs-tools-button-label = Offer +pdfjs-first-page-button = + .title = Mynd i'r Dudalen Gyntaf +pdfjs-first-page-button-label = Mynd i'r Dudalen Gyntaf +pdfjs-last-page-button = + .title = Mynd i'r Dudalen Olaf +pdfjs-last-page-button-label = Mynd i'r Dudalen Olaf +pdfjs-page-rotate-cw-button = + .title = Cylchdroi Clocwedd +pdfjs-page-rotate-cw-button-label = Cylchdroi Clocwedd +pdfjs-page-rotate-ccw-button = + .title = Cylchdroi Gwrthglocwedd +pdfjs-page-rotate-ccw-button-label = Cylchdroi Gwrthglocwedd +pdfjs-cursor-text-select-tool-button = + .title = Galluogi Dewis Offeryn Testun +pdfjs-cursor-text-select-tool-button-label = Offeryn Dewis Testun +pdfjs-cursor-hand-tool-button = + .title = Galluogi Offeryn Llaw +pdfjs-cursor-hand-tool-button-label = Offeryn Llaw +pdfjs-scroll-page-button = + .title = Defnyddio Sgrolio Tudalen +pdfjs-scroll-page-button-label = Sgrolio Tudalen +pdfjs-scroll-vertical-button = + .title = Defnyddio Sgrolio Fertigol +pdfjs-scroll-vertical-button-label = Sgrolio Fertigol +pdfjs-scroll-horizontal-button = + .title = Defnyddio Sgrolio Llorweddol +pdfjs-scroll-horizontal-button-label = Sgrolio Llorweddol +pdfjs-scroll-wrapped-button = + .title = Defnyddio Sgrolio Amlapio +pdfjs-scroll-wrapped-button-label = Sgrolio Amlapio +pdfjs-spread-none-button = + .title = Peidio uno trawsdaleniadau +pdfjs-spread-none-button-label = Dim Trawsdaleniadau +pdfjs-spread-odd-button = + .title = Uno trawsdaleniadau gan gychwyn gyda thudalennau odrif +pdfjs-spread-odd-button-label = Trawsdaleniadau Odrif +pdfjs-spread-even-button = + .title = Uno trawsdaleniadau gan gychwyn gyda thudalennau eilrif +pdfjs-spread-even-button-label = Trawsdaleniadau Eilrif + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Priodweddau Dogfen… +pdfjs-document-properties-button-label = Priodweddau Dogfen… +pdfjs-document-properties-file-name = Enw ffeil: +pdfjs-document-properties-file-size = Maint ffeil: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } beit) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } beit) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } beit) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } beit) +pdfjs-document-properties-title = Teitl: +pdfjs-document-properties-author = Awdur: +pdfjs-document-properties-subject = Pwnc: +pdfjs-document-properties-keywords = Allweddair: +pdfjs-document-properties-creation-date = Dyddiad Creu: +pdfjs-document-properties-modification-date = Dyddiad Addasu: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Crewr: +pdfjs-document-properties-producer = Cynhyrchydd PDF: +pdfjs-document-properties-version = Fersiwn PDF: +pdfjs-document-properties-page-count = Cyfrif Tudalen: +pdfjs-document-properties-page-size = Maint Tudalen: +pdfjs-document-properties-page-size-unit-inches = o fewn +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portread +pdfjs-document-properties-page-size-orientation-landscape = tirlun +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Llythyr +pdfjs-document-properties-page-size-name-legal = Cyfreithiol + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Golwg Gwe Cyflym: +pdfjs-document-properties-linearized-yes = Iawn +pdfjs-document-properties-linearized-no = Na +pdfjs-document-properties-close-button = Cau + +## Print + +pdfjs-print-progress-message = Paratoi dogfen ar gyfer ei hargraffu… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Diddymu +pdfjs-printing-not-supported = Rhybudd: Nid yw argraffu yn cael ei gynnal yn llawn gan y porwr. +pdfjs-printing-not-ready = Rhybudd: Nid yw'r PDF wedi ei lwytho'n llawn ar gyfer argraffu. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toglo'r Bar Ochr +pdfjs-toggle-sidebar-notification-button = + .title = Toglo'r Bar Ochr (mae'r ddogfen yn cynnwys amlinelliadau/atodiadau/haenau) +pdfjs-toggle-sidebar-button-label = Toglo'r Bar Ochr +pdfjs-document-outline-button = + .title = Dangos Amlinell Dogfen (clic dwbl i ymestyn/cau pob eitem) +pdfjs-document-outline-button-label = Amlinelliad Dogfen +pdfjs-attachments-button = + .title = Dangos Atodiadau +pdfjs-attachments-button-label = Atodiadau +pdfjs-layers-button = + .title = Dangos Haenau (cliciwch ddwywaith i ailosod yr holl haenau i'r cyflwr rhagosodedig) +pdfjs-layers-button-label = Haenau +pdfjs-thumbs-button = + .title = Dangos Lluniau Bach +pdfjs-thumbs-button-label = Lluniau Bach +pdfjs-current-outline-item-button = + .title = Canfod yr Eitem Amlinellol Gyfredol +pdfjs-current-outline-item-button-label = Yr Eitem Amlinellol Gyfredol +pdfjs-findbar-button = + .title = Canfod yn y Ddogfen +pdfjs-findbar-button-label = Canfod +pdfjs-additional-layers = Haenau Ychwanegol + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Tudalen { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Llun Bach Tudalen { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Canfod + .placeholder = Canfod yn y ddogfen… +pdfjs-find-previous-button = + .title = Canfod enghraifft flaenorol o'r ymadrodd +pdfjs-find-previous-button-label = Blaenorol +pdfjs-find-next-button = + .title = Canfod enghraifft nesaf yr ymadrodd +pdfjs-find-next-button-label = Nesaf +pdfjs-find-highlight-checkbox = Amlygu Popeth +pdfjs-find-match-case-checkbox-label = Cydweddu Maint +pdfjs-find-match-diacritics-checkbox-label = Diacritigau Cyfatebol +pdfjs-find-entire-word-checkbox-label = Geiriau Cyfan +pdfjs-find-reached-top = Wedi cyrraedd brig y dudalen, parhau o'r gwaelod +pdfjs-find-reached-bottom = Wedi cyrraedd diwedd y dudalen, parhau o'r brig +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [zero] { $current } o { $total } cydweddiadau + [one] { $current } o { $total } cydweddiad + [two] { $current } o { $total } gydweddiad + [few] { $current } o { $total } cydweddiad + [many] { $current } o { $total } chydweddiad + *[other] { $current } o { $total } cydweddiad + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [zero] Mwy nag { $limit } cydweddiadau + [one] Mwy nag { $limit } cydweddiad + [two] Mwy nag { $limit } gydweddiad + [few] Mwy nag { $limit } cydweddiad + [many] Mwy nag { $limit } chydweddiad + *[other] Mwy nag { $limit } cydweddiad + } +pdfjs-find-not-found = Heb ganfod ymadrodd + +## Predefined zoom values + +pdfjs-page-scale-width = Lled Tudalen +pdfjs-page-scale-fit = Ffit Tudalen +pdfjs-page-scale-auto = Chwyddo Awtomatig +pdfjs-page-scale-actual = Maint Gwirioneddol +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Tudalen { $page } + +## Loading indicator messages + +pdfjs-loading-error = Digwyddodd gwall wrth lwytho'r PDF. +pdfjs-invalid-file-error = Ffeil PDF annilys neu llwgr. +pdfjs-missing-file-error = Ffeil PDF coll. +pdfjs-unexpected-response-error = Ymateb annisgwyl gan y gweinydd. +pdfjs-rendering-error = Digwyddodd gwall wrth adeiladu'r dudalen. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anodiad { $type } ] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Rhowch gyfrinair i agor y PDF. +pdfjs-password-invalid = Cyfrinair annilys. Ceisiwch eto. +pdfjs-password-ok-button = Iawn +pdfjs-password-cancel-button = Diddymu +pdfjs-web-fonts-disabled = Ffontiau gwe wedi eu hanalluogi: methu defnyddio ffontiau PDF mewnblanedig. + +## Editing + +pdfjs-editor-free-text-button = + .title = Testun +pdfjs-editor-free-text-button-label = Testun +pdfjs-editor-ink-button = + .title = Lluniadu +pdfjs-editor-ink-button-label = Lluniadu +pdfjs-editor-stamp-button = + .title = Ychwanegu neu olygu delweddau +pdfjs-editor-stamp-button-label = Ychwanegu neu olygu delweddau +pdfjs-editor-highlight-button = + .title = Amlygu +pdfjs-editor-highlight-button-label = Amlygu +pdfjs-highlight-floating-button1 = + .title = Amlygu + .aria-label = Amlygu +pdfjs-highlight-floating-button-label = Amlygu + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Dileu lluniad +pdfjs-editor-remove-freetext-button = + .title = Dileu testun +pdfjs-editor-remove-stamp-button = + .title = Dileu delwedd +pdfjs-editor-remove-highlight-button = + .title = Tynnu amlygiad + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Lliw +pdfjs-editor-free-text-size-input = Maint +pdfjs-editor-ink-color-input = Lliw +pdfjs-editor-ink-thickness-input = Trwch +pdfjs-editor-ink-opacity-input = Didreiddedd +pdfjs-editor-stamp-add-image-button = + .title = Ychwanegu delwedd +pdfjs-editor-stamp-add-image-button-label = Ychwanegu delwedd +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Trwch +pdfjs-editor-free-highlight-thickness-title = + .title = Newid trwch wrth amlygu eitemau heblaw testun +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Golygydd Testun + .default-content = Cychwyn teipio… +pdfjs-free-text = + .aria-label = Golygydd Testun +pdfjs-free-text-default-content = Cychwyn teipio… +pdfjs-ink = + .aria-label = Golygydd Lluniadu +pdfjs-ink-canvas = + .aria-label = Delwedd wedi'i chreu gan ddefnyddwyr + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Testun amgen (alt) +pdfjs-editor-alt-text-edit-button = + .aria-label = Golygu testun amgen +pdfjs-editor-alt-text-edit-button-label = Golygu testun amgen +pdfjs-editor-alt-text-dialog-label = Dewisiadau +pdfjs-editor-alt-text-dialog-description = Mae testun amgen (testun alt) yn helpu pan na all pobl weld y ddelwedd neu pan nad yw'n llwytho. +pdfjs-editor-alt-text-add-description-label = Ychwanegu disgrifiad +pdfjs-editor-alt-text-add-description-description = Anelwch at 1-2 frawddeg sy'n disgrifio'r pwnc, y cefndir neu'r gweithredoedd. +pdfjs-editor-alt-text-mark-decorative-label = Marcio fel addurniadol +pdfjs-editor-alt-text-mark-decorative-description = Mae'n cael ei ddefnyddio ar gyfer delweddau addurniadol, fel borderi neu farciau dŵr. +pdfjs-editor-alt-text-cancel-button = Diddymu +pdfjs-editor-alt-text-save-button = Cadw +pdfjs-editor-alt-text-decorative-tooltip = Marcio fel addurniadol +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Er enghraifft, “Mae dyn ifanc yn eistedd wrth fwrdd i fwyta pryd bwyd” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Testun amgen (alt) + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Y gornel chwith uchaf — newid maint +pdfjs-editor-resizer-label-top-middle = Canol uchaf - newid maint +pdfjs-editor-resizer-label-top-right = Y gornel dde uchaf - newid maint +pdfjs-editor-resizer-label-middle-right = De canol - newid maint +pdfjs-editor-resizer-label-bottom-right = Y gornel dde isaf — newid maint +pdfjs-editor-resizer-label-bottom-middle = Canol gwaelod — newid maint +pdfjs-editor-resizer-label-bottom-left = Y gornel chwith isaf — newid maint +pdfjs-editor-resizer-label-middle-left = Chwith canol — newid maint +pdfjs-editor-resizer-top-left = + .aria-label = Y gornel chwith uchaf — newid maint +pdfjs-editor-resizer-top-middle = + .aria-label = Canol uchaf - newid maint +pdfjs-editor-resizer-top-right = + .aria-label = Y gornel dde uchaf - newid maint +pdfjs-editor-resizer-middle-right = + .aria-label = De canol - newid maint +pdfjs-editor-resizer-bottom-right = + .aria-label = Y gornel dde isaf — newid maint +pdfjs-editor-resizer-bottom-middle = + .aria-label = Canol gwaelod — newid maint +pdfjs-editor-resizer-bottom-left = + .aria-label = Y gornel chwith isaf — newid maint +pdfjs-editor-resizer-middle-left = + .aria-label = Chwith canol — newid maint + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Lliw amlygu +pdfjs-editor-colorpicker-button = + .title = Newid lliw +pdfjs-editor-colorpicker-dropdown = + .aria-label = Dewisiadau lliw +pdfjs-editor-colorpicker-yellow = + .title = Melyn +pdfjs-editor-colorpicker-green = + .title = Gwyrdd +pdfjs-editor-colorpicker-blue = + .title = Glas +pdfjs-editor-colorpicker-pink = + .title = Pinc +pdfjs-editor-colorpicker-red = + .title = Coch + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Dangos y cyfan +pdfjs-editor-highlight-show-all-button = + .title = Dangos y cyfan + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Golygu testun amgen (disgrifiad o ddelwedd) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Ychwanegwch destun amgen (disgrifiad delwedd) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Ysgrifennwch eich disgrifiad yma… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Disgrifiad byr ar gyfer pobl sydd ddim yn gallu gweld y ddelwedd neu pan nad yw'r ddelwedd yn llwytho. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Cafodd y testun amgen hwn ei greu'n awtomatig a gall fod yn anghywir. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Rhagor +pdfjs-editor-new-alt-text-create-automatically-button-label = Creu testun amgen yn awtomatig +pdfjs-editor-new-alt-text-not-now-button = Nid nawr +pdfjs-editor-new-alt-text-error-title = Methu â chreu testun amgen yn awtomatig +pdfjs-editor-new-alt-text-error-description = Ysgrifennwch eich testun amgen eich hun neu ceisiwch eto yn nes ymlaen. +pdfjs-editor-new-alt-text-error-close-button = Cau +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Wrthi'n llwytho i lawr model AI testun amgen ( { $downloadedSize } o { $totalSize } MB) + .aria-valuetext = Wrthi'n llwytho i lawr model AI testun amgen ( { $downloadedSize } o { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Ychwanegwyd testun amgen +pdfjs-editor-new-alt-text-added-button-label = Ychwanegwyd testun amgen +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Testun amgen coll +pdfjs-editor-new-alt-text-missing-button-label = Testun amgen coll +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Adolygu'r testun amgen +pdfjs-editor-new-alt-text-to-review-button-label = Adolygu'r testun amgen +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Crëwyd yn awtomatig: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Gosodiadau testun amgen delwedd +pdfjs-image-alt-text-settings-button-label = Gosodiadau testun amgen delwedd +pdfjs-editor-alt-text-settings-dialog-label = Gosodiadau testun amgen delwedd +pdfjs-editor-alt-text-settings-automatic-title = Testun amgen awtomatig +pdfjs-editor-alt-text-settings-create-model-button-label = Creu testun amgen yn awtomatig +pdfjs-editor-alt-text-settings-create-model-description = Yn awgrymu disgrifiadau i helpu pobl sydd ddim yn gallu gweld y ddelwedd neu pan nad yw'r ddelwedd yn llwytho. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model AI testun amgen ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Yn rhedeg yn lleol ar eich dyfais fel bod eich data'n aros yn breifat. Yn ofynnol ar gyfer testun amgen awtomatig. +pdfjs-editor-alt-text-settings-delete-model-button = Dileu +pdfjs-editor-alt-text-settings-download-model-button = Llwytho i Lawr +pdfjs-editor-alt-text-settings-downloading-model-button = Wrthi'n llwytho i lawr… +pdfjs-editor-alt-text-settings-editor-title = Golygydd testun amgen +pdfjs-editor-alt-text-settings-show-dialog-button-label = Dangoswch y golygydd testun amgen yn syth wrth ychwanegu delwedd +pdfjs-editor-alt-text-settings-show-dialog-description = Yn eich helpu i wneud yn siŵr bod gan eich holl ddelweddau destun amgen. +pdfjs-editor-alt-text-settings-close-button = Cau + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Tynnwyd yr amlygu +pdfjs-editor-undo-bar-message-freetext = Tynnwyd y testun +pdfjs-editor-undo-bar-message-ink = Tynnwyd y lluniad +pdfjs-editor-undo-bar-message-stamp = Tynnwyd y ddelwedd +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [zero] { $count } anodiad wedi'u tynnu + [one] { $count } anodiad wedi'i dynnu + [two] { $count } anodiad wedi'u tynnu + [few] { $count } anodiad wedi'u tynnu + [many] { $count } anodiad wedi'u tynnu + *[other] { $count } anodiad wedi'u tynnu + } +pdfjs-editor-undo-bar-undo-button = + .title = Dadwneud +pdfjs-editor-undo-bar-undo-button-label = Dadwneud +pdfjs-editor-undo-bar-close-button = + .title = Cau +pdfjs-editor-undo-bar-close-button-label = Cau diff --git a/public/assets/pdfjs/locale/da/viewer.ftl b/public/assets/pdfjs/locale/da/viewer.ftl new file mode 100644 index 0000000..224eac1 --- /dev/null +++ b/public/assets/pdfjs/locale/da/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Forrige side +pdfjs-previous-button-label = Forrige +pdfjs-next-button = + .title = Næste side +pdfjs-next-button-label = Næste +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Side +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = af { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } af { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom ud +pdfjs-zoom-out-button-label = Zoom ud +pdfjs-zoom-in-button = + .title = Zoom ind +pdfjs-zoom-in-button-label = Zoom ind +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Skift til fuldskærmsvisning +pdfjs-presentation-mode-button-label = Fuldskærmsvisning +pdfjs-open-file-button = + .title = Åbn fil +pdfjs-open-file-button-label = Åbn +pdfjs-print-button = + .title = Udskriv +pdfjs-print-button-label = Udskriv +pdfjs-save-button = + .title = Gem +pdfjs-save-button-label = Gem +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Hent +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Hent +pdfjs-bookmark-button = + .title = Aktuel side (vis URL fra den aktuelle side) +pdfjs-bookmark-button-label = Aktuel side + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Funktioner +pdfjs-tools-button-label = Funktioner +pdfjs-first-page-button = + .title = Gå til første side +pdfjs-first-page-button-label = Gå til første side +pdfjs-last-page-button = + .title = Gå til sidste side +pdfjs-last-page-button-label = Gå til sidste side +pdfjs-page-rotate-cw-button = + .title = Roter med uret +pdfjs-page-rotate-cw-button-label = Roter med uret +pdfjs-page-rotate-ccw-button = + .title = Roter mod uret +pdfjs-page-rotate-ccw-button-label = Roter mod uret +pdfjs-cursor-text-select-tool-button = + .title = Aktiver markeringsværktøj +pdfjs-cursor-text-select-tool-button-label = Markeringsværktøj +pdfjs-cursor-hand-tool-button = + .title = Aktiver håndværktøj +pdfjs-cursor-hand-tool-button-label = Håndværktøj +pdfjs-scroll-page-button = + .title = Brug sidescrolling +pdfjs-scroll-page-button-label = Sidescrolling +pdfjs-scroll-vertical-button = + .title = Brug vertikal scrolling +pdfjs-scroll-vertical-button-label = Vertikal scrolling +pdfjs-scroll-horizontal-button = + .title = Brug horisontal scrolling +pdfjs-scroll-horizontal-button-label = Horisontal scrolling +pdfjs-scroll-wrapped-button = + .title = Brug ombrudt scrolling +pdfjs-scroll-wrapped-button-label = Ombrudt scrolling +pdfjs-spread-none-button = + .title = Vis enkeltsider +pdfjs-spread-none-button-label = Enkeltsider +pdfjs-spread-odd-button = + .title = Vis opslag med ulige sidenumre til venstre +pdfjs-spread-odd-button-label = Opslag med forside +pdfjs-spread-even-button = + .title = Vis opslag med lige sidenumre til venstre +pdfjs-spread-even-button-label = Opslag uden forside + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentegenskaber… +pdfjs-document-properties-button-label = Dokumentegenskaber… +pdfjs-document-properties-file-name = Filnavn: +pdfjs-document-properties-file-size = Filstørrelse: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Forfatter: +pdfjs-document-properties-subject = Emne: +pdfjs-document-properties-keywords = Nøgleord: +pdfjs-document-properties-creation-date = Oprettet: +pdfjs-document-properties-modification-date = Redigeret: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Program: +pdfjs-document-properties-producer = PDF-producent: +pdfjs-document-properties-version = PDF-version: +pdfjs-document-properties-page-count = Antal sider: +pdfjs-document-properties-page-size = Sidestørrelse: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = stående +pdfjs-document-properties-page-size-orientation-landscape = liggende +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Hurtig web-visning: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nej +pdfjs-document-properties-close-button = Luk + +## Print + +pdfjs-print-progress-message = Forbereder dokument til udskrivning… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Annuller +pdfjs-printing-not-supported = Advarsel: Udskrivning er ikke fuldt understøttet af browseren. +pdfjs-printing-not-ready = Advarsel: PDF-filen er ikke fuldt indlæst til udskrivning. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Slå sidepanel til eller fra +pdfjs-toggle-sidebar-notification-button = + .title = Slå sidepanel til eller fra (dokumentet indeholder disposition/vedhæftede filer/lag) +pdfjs-toggle-sidebar-button-label = Slå sidepanel til eller fra +pdfjs-document-outline-button = + .title = Vis dokumentets disposition (dobbeltklik for at udvide/sammenfolde alle elementer) +pdfjs-document-outline-button-label = Dokument-disposition +pdfjs-attachments-button = + .title = Vis vedhæftede filer +pdfjs-attachments-button-label = Vedhæftede filer +pdfjs-layers-button = + .title = Vis lag (dobbeltklik for at nulstille alle lag til standard-tilstanden) +pdfjs-layers-button-label = Lag +pdfjs-thumbs-button = + .title = Vis miniaturer +pdfjs-thumbs-button-label = Miniaturer +pdfjs-current-outline-item-button = + .title = Find det aktuelle dispositions-element +pdfjs-current-outline-item-button-label = Aktuelt dispositions-element +pdfjs-findbar-button = + .title = Find i dokument +pdfjs-findbar-button-label = Find +pdfjs-additional-layers = Yderligere lag + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Side { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniature af side { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find i dokument… +pdfjs-find-previous-button = + .title = Find den forrige forekomst +pdfjs-find-previous-button-label = Forrige +pdfjs-find-next-button = + .title = Find den næste forekomst +pdfjs-find-next-button-label = Næste +pdfjs-find-highlight-checkbox = Fremhæv alle +pdfjs-find-match-case-checkbox-label = Forskel på store og små bogstaver +pdfjs-find-match-diacritics-checkbox-label = Diakritiske tegn +pdfjs-find-entire-word-checkbox-label = Hele ord +pdfjs-find-reached-top = Toppen af siden blev nået, fortsatte fra bunden +pdfjs-find-reached-bottom = Bunden af siden blev nået, fortsatte fra toppen +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } af { $total } forekomst + *[other] { $current } af { $total } forekomster + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mere end { $limit } forekomst + *[other] Mere end { $limit } forekomster + } +pdfjs-find-not-found = Der blev ikke fundet noget + +## Predefined zoom values + +pdfjs-page-scale-width = Sidebredde +pdfjs-page-scale-fit = Tilpas til side +pdfjs-page-scale-auto = Automatisk zoom +pdfjs-page-scale-actual = Faktisk størrelse +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Side { $page } + +## Loading indicator messages + +pdfjs-loading-error = Der opstod en fejl ved indlæsning af PDF-filen. +pdfjs-invalid-file-error = PDF-filen er ugyldig eller ødelagt. +pdfjs-missing-file-error = Manglende PDF-fil. +pdfjs-unexpected-response-error = Uventet svar fra serveren. +pdfjs-rendering-error = Der opstod en fejl ved generering af siden. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }kommentar] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Angiv adgangskode til at åbne denne PDF-fil. +pdfjs-password-invalid = Ugyldig adgangskode. Prøv igen. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Fortryd +pdfjs-web-fonts-disabled = Webskrifttyper er deaktiverede. De indlejrede skrifttyper i PDF-filen kan ikke anvendes. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Tegn +pdfjs-editor-ink-button-label = Tegn +pdfjs-editor-stamp-button = + .title = Tilføj eller rediger billeder +pdfjs-editor-stamp-button-label = Tilføj eller rediger billeder +pdfjs-editor-highlight-button = + .title = Fremhæv +pdfjs-editor-highlight-button-label = Fremhæv +pdfjs-highlight-floating-button1 = + .title = Fremhæv + .aria-label = Fremhæv +pdfjs-highlight-floating-button-label = Fremhæv + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Fjern tegning +pdfjs-editor-remove-freetext-button = + .title = Fjern tekst +pdfjs-editor-remove-stamp-button = + .title = Fjern billede +pdfjs-editor-remove-highlight-button = + .title = Fjern fremhævning + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Farve +pdfjs-editor-free-text-size-input = Størrelse +pdfjs-editor-ink-color-input = Farve +pdfjs-editor-ink-thickness-input = Tykkelse +pdfjs-editor-ink-opacity-input = Uigennemsigtighed +pdfjs-editor-stamp-add-image-button = + .title = Tilføj billede +pdfjs-editor-stamp-add-image-button-label = Tilføj billede +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tykkelse +pdfjs-editor-free-highlight-thickness-title = + .title = Ændr tykkelse, når andre elementer end tekst fremhæves +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Teksteditor + .default-content = Begynd at skrive… +pdfjs-free-text = + .aria-label = Teksteditor +pdfjs-free-text-default-content = Begynd at skrive… +pdfjs-ink = + .aria-label = Tegnings-editor +pdfjs-ink-canvas = + .aria-label = Brugeroprettet billede + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternativ tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Rediger alternativ tekst +pdfjs-editor-alt-text-edit-button-label = Rediger alternativ tekst +pdfjs-editor-alt-text-dialog-label = Vælg en indstilling +pdfjs-editor-alt-text-dialog-description = Alternativ tekst hjælper folk, som ikke kan se billedet eller når det ikke indlæses. +pdfjs-editor-alt-text-add-description-label = Tilføj en beskrivelse +pdfjs-editor-alt-text-add-description-description = Sigt efter en eller to sætninger, der beskriver emnet, omgivelserne eller handlinger. +pdfjs-editor-alt-text-mark-decorative-label = Marker som dekorativ +pdfjs-editor-alt-text-mark-decorative-description = Dette bruges for dekorative billeder som rammer eller vandmærker. +pdfjs-editor-alt-text-cancel-button = Annuller +pdfjs-editor-alt-text-save-button = Gem +pdfjs-editor-alt-text-decorative-tooltip = Markeret som dekorativ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = For eksempel: "En ung mand sætter sig ved et bord for at spise et måltid mad" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternativ tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Øverste venstre hjørne — tilpas størrelse +pdfjs-editor-resizer-label-top-middle = Øverste i midten — tilpas størrelse +pdfjs-editor-resizer-label-top-right = Øverste højre hjørne — tilpas størrelse +pdfjs-editor-resizer-label-middle-right = Midten til højre — tilpas størrelse +pdfjs-editor-resizer-label-bottom-right = Nederste højre hjørne - tilpas størrelse +pdfjs-editor-resizer-label-bottom-middle = Nederst i midten - tilpas størrelse +pdfjs-editor-resizer-label-bottom-left = Nederste venstre hjørne - tilpas størrelse +pdfjs-editor-resizer-label-middle-left = Midten til venstre — tilpas størrelse +pdfjs-editor-resizer-top-left = + .aria-label = Øverste venstre hjørne — tilpas størrelse +pdfjs-editor-resizer-top-middle = + .aria-label = Øverste i midten — tilpas størrelse +pdfjs-editor-resizer-top-right = + .aria-label = Øverste højre hjørne — tilpas størrelse +pdfjs-editor-resizer-middle-right = + .aria-label = Midten til højre — tilpas størrelse +pdfjs-editor-resizer-bottom-right = + .aria-label = Nederste højre hjørne - tilpas størrelse +pdfjs-editor-resizer-bottom-middle = + .aria-label = Nederst i midten - tilpas størrelse +pdfjs-editor-resizer-bottom-left = + .aria-label = Nederste venstre hjørne - tilpas størrelse +pdfjs-editor-resizer-middle-left = + .aria-label = Midten til venstre — tilpas størrelse + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Fremhævningsfarve +pdfjs-editor-colorpicker-button = + .title = Skift farve +pdfjs-editor-colorpicker-dropdown = + .aria-label = Farvevalg +pdfjs-editor-colorpicker-yellow = + .title = Gul +pdfjs-editor-colorpicker-green = + .title = Grøn +pdfjs-editor-colorpicker-blue = + .title = Blå +pdfjs-editor-colorpicker-pink = + .title = Lyserød +pdfjs-editor-colorpicker-red = + .title = Rød + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Vis alle +pdfjs-editor-highlight-show-all-button = + .title = Vis alle + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Rediger alternativ tekst (billedbeskrivelse) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Tilføj alternativ tekst (billedbeskrivelse) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skriv din beskrivelse her... +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kort beskrivelse til personer, der ikke kan se billedet, eller når billedet ikke indlæses. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Denne alternative tekst blev oprettet automatisk og kan være upræcis. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Læs mere +pdfjs-editor-new-alt-text-create-automatically-button-label = Opret alternativ tekst automatisk +pdfjs-editor-new-alt-text-not-now-button = Ikke nu +pdfjs-editor-new-alt-text-error-title = Kunne ikke oprette alternativ tekst automatisk +pdfjs-editor-new-alt-text-error-description = Skriv din egen alternative tekst, eller prøv igen senere. +pdfjs-editor-new-alt-text-error-close-button = Luk +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Henter alternativ tekst AI-model ({ $downloadedSize } af { $totalSize } MB) + .aria-valuetext = Henter alternativ tekst AI-model ({ $downloadedSize } af { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativ tekst tilføjet +pdfjs-editor-new-alt-text-added-button-label = Alternativ tekst tilføjet +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Mangler alternativ tekst +pdfjs-editor-new-alt-text-missing-button-label = Mangler alternativ tekst +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Gennemgå alternativ tekst +pdfjs-editor-new-alt-text-to-review-button-label = Gennemgå alternativ tekst +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Oprettet automatisk: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Indstillinger for alternativ tekst til billeder +pdfjs-image-alt-text-settings-button-label = Indstillinger for alternativ tekst til billeder +pdfjs-editor-alt-text-settings-dialog-label = Indstillinger for alternativ tekst til billeder +pdfjs-editor-alt-text-settings-automatic-title = Automatisk alternativ tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Opret alternativ tekst automatisk +pdfjs-editor-alt-text-settings-create-model-description = Foreslår beskrivelser for at hjælpe folk, der ikke kan se billedet, eller når billedet ikke indlæses. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = AI-model til at oprette alternative tekster ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Kører lokalt på din enhed, så dine data forbliver private. Påkrævet for at anvende automatisk alternativ tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Slet +pdfjs-editor-alt-text-settings-download-model-button = Hent +pdfjs-editor-alt-text-settings-downloading-model-button = Henter… +pdfjs-editor-alt-text-settings-editor-title = Redigering af alternativ tekst +pdfjs-editor-alt-text-settings-show-dialog-button-label = Vis redigering af alternativ tekst med det samme, når et billede tilføjes +pdfjs-editor-alt-text-settings-show-dialog-description = Hjælper dig med at sikre, at alle dine billeder har alternativ tekst. +pdfjs-editor-alt-text-settings-close-button = Luk + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Fremhævning fjernet +pdfjs-editor-undo-bar-message-freetext = Tekst fjernet +pdfjs-editor-undo-bar-message-ink = Tegning fjernet +pdfjs-editor-undo-bar-message-stamp = Billede fjernet +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } kommentar fjernet + *[other] { $count } kommentarer fjernet + } +pdfjs-editor-undo-bar-undo-button = + .title = Fortryd +pdfjs-editor-undo-bar-undo-button-label = Fortryd +pdfjs-editor-undo-bar-close-button = + .title = Luk +pdfjs-editor-undo-bar-close-button-label = Luk diff --git a/public/assets/pdfjs/locale/de/viewer.ftl b/public/assets/pdfjs/locale/de/viewer.ftl new file mode 100644 index 0000000..ee26455 --- /dev/null +++ b/public/assets/pdfjs/locale/de/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Eine Seite zurück +pdfjs-previous-button-label = Zurück +pdfjs-next-button = + .title = Eine Seite vor +pdfjs-next-button-label = Vor +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Seite +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = von { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } von { $pagesCount }) +pdfjs-zoom-out-button = + .title = Verkleinern +pdfjs-zoom-out-button-label = Verkleinern +pdfjs-zoom-in-button = + .title = Vergrößern +pdfjs-zoom-in-button-label = Vergrößern +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = In Präsentationsmodus wechseln +pdfjs-presentation-mode-button-label = Präsentationsmodus +pdfjs-open-file-button = + .title = Datei öffnen +pdfjs-open-file-button-label = Öffnen +pdfjs-print-button = + .title = Drucken +pdfjs-print-button-label = Drucken +pdfjs-save-button = + .title = Speichern +pdfjs-save-button-label = Speichern +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Herunterladen +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Herunterladen +pdfjs-bookmark-button = + .title = Aktuelle Seite (URL von aktueller Seite anzeigen) +pdfjs-bookmark-button-label = Aktuelle Seite + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Werkzeuge +pdfjs-tools-button-label = Werkzeuge +pdfjs-first-page-button = + .title = Erste Seite anzeigen +pdfjs-first-page-button-label = Erste Seite anzeigen +pdfjs-last-page-button = + .title = Letzte Seite anzeigen +pdfjs-last-page-button-label = Letzte Seite anzeigen +pdfjs-page-rotate-cw-button = + .title = Im Uhrzeigersinn drehen +pdfjs-page-rotate-cw-button-label = Im Uhrzeigersinn drehen +pdfjs-page-rotate-ccw-button = + .title = Gegen Uhrzeigersinn drehen +pdfjs-page-rotate-ccw-button-label = Gegen Uhrzeigersinn drehen +pdfjs-cursor-text-select-tool-button = + .title = Textauswahl-Werkzeug aktivieren +pdfjs-cursor-text-select-tool-button-label = Textauswahl-Werkzeug +pdfjs-cursor-hand-tool-button = + .title = Hand-Werkzeug aktivieren +pdfjs-cursor-hand-tool-button-label = Hand-Werkzeug +pdfjs-scroll-page-button = + .title = Seiten einzeln anordnen +pdfjs-scroll-page-button-label = Einzelseitenanordnung +pdfjs-scroll-vertical-button = + .title = Seiten übereinander anordnen +pdfjs-scroll-vertical-button-label = Vertikale Seitenanordnung +pdfjs-scroll-horizontal-button = + .title = Seiten nebeneinander anordnen +pdfjs-scroll-horizontal-button-label = Horizontale Seitenanordnung +pdfjs-scroll-wrapped-button = + .title = Seiten neben- und übereinander anordnen, abhängig vom Platz +pdfjs-scroll-wrapped-button-label = Kombinierte Seitenanordnung +pdfjs-spread-none-button = + .title = Seiten nicht nebeneinander anzeigen +pdfjs-spread-none-button-label = Einzelne Seiten +pdfjs-spread-odd-button = + .title = Jeweils eine ungerade und eine gerade Seite nebeneinander anzeigen +pdfjs-spread-odd-button-label = Ungerade + gerade Seite +pdfjs-spread-even-button = + .title = Jeweils eine gerade und eine ungerade Seite nebeneinander anzeigen +pdfjs-spread-even-button-label = Gerade + ungerade Seite + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumenteigenschaften +pdfjs-document-properties-button-label = Dokumenteigenschaften… +pdfjs-document-properties-file-name = Dateiname: +pdfjs-document-properties-file-size = Dateigröße: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } Bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } Bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } Bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } Bytes) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Thema: +pdfjs-document-properties-keywords = Stichwörter: +pdfjs-document-properties-creation-date = Erstelldatum: +pdfjs-document-properties-modification-date = Bearbeitungsdatum: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } { $time } +pdfjs-document-properties-creator = Anwendung: +pdfjs-document-properties-producer = PDF erstellt mit: +pdfjs-document-properties-version = PDF-Version: +pdfjs-document-properties-page-count = Seitenzahl: +pdfjs-document-properties-page-size = Seitengröße: +pdfjs-document-properties-page-size-unit-inches = Zoll +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = Hochformat +pdfjs-document-properties-page-size-orientation-landscape = Querformat +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Schnelle Webanzeige: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nein +pdfjs-document-properties-close-button = Schließen + +## Print + +pdfjs-print-progress-message = Dokument wird für Drucken vorbereitet… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Abbrechen +pdfjs-printing-not-supported = Warnung: Die Drucken-Funktion wird durch diesen Browser nicht vollständig unterstützt. +pdfjs-printing-not-ready = Warnung: Die PDF-Datei ist nicht vollständig geladen, dies ist für das Drucken aber empfohlen. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Sidebar umschalten +pdfjs-toggle-sidebar-notification-button = + .title = Sidebar umschalten (Dokument enthält Dokumentstruktur/Anhänge/Ebenen) +pdfjs-toggle-sidebar-button-label = Sidebar umschalten +pdfjs-document-outline-button = + .title = Dokumentstruktur anzeigen (Doppelklicken, um alle Einträge aus- bzw. einzuklappen) +pdfjs-document-outline-button-label = Dokumentstruktur +pdfjs-attachments-button = + .title = Anhänge anzeigen +pdfjs-attachments-button-label = Anhänge +pdfjs-layers-button = + .title = Ebenen anzeigen (Doppelklicken, um alle Ebenen auf den Standardzustand zurückzusetzen) +pdfjs-layers-button-label = Ebenen +pdfjs-thumbs-button = + .title = Miniaturansichten anzeigen +pdfjs-thumbs-button-label = Miniaturansichten +pdfjs-current-outline-item-button = + .title = Aktuelles Struktur-Element finden +pdfjs-current-outline-item-button-label = Aktuelles Struktur-Element +pdfjs-findbar-button = + .title = Dokument durchsuchen +pdfjs-findbar-button-label = Suchen +pdfjs-additional-layers = Zusätzliche Ebenen + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Seite { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniaturansicht von Seite { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Suchen + .placeholder = Dokument durchsuchen… +pdfjs-find-previous-button = + .title = Vorheriges Vorkommen des Suchbegriffs finden +pdfjs-find-previous-button-label = Zurück +pdfjs-find-next-button = + .title = Nächstes Vorkommen des Suchbegriffs finden +pdfjs-find-next-button-label = Weiter +pdfjs-find-highlight-checkbox = Alle hervorheben +pdfjs-find-match-case-checkbox-label = Groß-/Kleinschreibung beachten +pdfjs-find-match-diacritics-checkbox-label = Akzente +pdfjs-find-entire-word-checkbox-label = Ganze Wörter +pdfjs-find-reached-top = Anfang des Dokuments erreicht, fahre am Ende fort +pdfjs-find-reached-bottom = Ende des Dokuments erreicht, fahre am Anfang fort +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } von { $total } Übereinstimmung + *[other] { $current } von { $total } Übereinstimmungen + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mehr als { $limit } Übereinstimmung + *[other] Mehr als { $limit } Übereinstimmungen + } +pdfjs-find-not-found = Suchbegriff nicht gefunden + +## Predefined zoom values + +pdfjs-page-scale-width = Seitenbreite +pdfjs-page-scale-fit = Seitengröße +pdfjs-page-scale-auto = Automatischer Zoom +pdfjs-page-scale-actual = Originalgröße +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Seite { $page } + +## Loading indicator messages + +pdfjs-loading-error = Beim Laden der PDF-Datei trat ein Fehler auf. +pdfjs-invalid-file-error = Ungültige oder beschädigte PDF-Datei +pdfjs-missing-file-error = Fehlende PDF-Datei +pdfjs-unexpected-response-error = Unerwartete Antwort des Servers +pdfjs-rendering-error = Beim Darstellen der Seite trat ein Fehler auf. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anlage: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Geben Sie zum Öffnen der PDF-Datei deren Passwort ein. +pdfjs-password-invalid = Falsches Passwort. Bitte versuchen Sie es erneut. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Abbrechen +pdfjs-web-fonts-disabled = Web-Schriftarten sind deaktiviert: Eingebettete PDF-Schriftarten konnten nicht geladen werden. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Zeichnen +pdfjs-editor-ink-button-label = Zeichnen +pdfjs-editor-stamp-button = + .title = Grafiken hinzufügen oder bearbeiten +pdfjs-editor-stamp-button-label = Grafiken hinzufügen oder bearbeiten +pdfjs-editor-highlight-button = + .title = Hervorheben +pdfjs-editor-highlight-button-label = Hervorheben +pdfjs-highlight-floating-button1 = + .title = Hervorheben + .aria-label = Hervorheben +pdfjs-highlight-floating-button-label = Hervorheben + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Zeichnung entfernen +pdfjs-editor-remove-freetext-button = + .title = Text entfernen +pdfjs-editor-remove-stamp-button = + .title = Grafik entfernen +pdfjs-editor-remove-highlight-button = + .title = Hervorhebung entfernen + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Farbe +pdfjs-editor-free-text-size-input = Größe +pdfjs-editor-ink-color-input = Farbe +pdfjs-editor-ink-thickness-input = Linienstärke +pdfjs-editor-ink-opacity-input = Deckkraft +pdfjs-editor-stamp-add-image-button = + .title = Grafik hinzufügen +pdfjs-editor-stamp-add-image-button-label = Grafik hinzufügen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Linienstärke +pdfjs-editor-free-highlight-thickness-title = + .title = Linienstärke beim Hervorheben anderer Elemente als Text ändern +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Texteditor + .default-content = Schreiben beginnen… +pdfjs-free-text = + .aria-label = Texteditor +pdfjs-free-text-default-content = Schreiben beginnen… +pdfjs-ink = + .aria-label = Zeichnungseditor +pdfjs-ink-canvas = + .aria-label = Vom Benutzer erstelltes Bild + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternativ-Text +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternativ-Text bearbeiten +pdfjs-editor-alt-text-edit-button-label = Alternativ-Text bearbeiten +pdfjs-editor-alt-text-dialog-label = Option wählen +pdfjs-editor-alt-text-dialog-description = Alt-Text (Alternativtext) hilft, wenn Personen die Grafik nicht sehen können oder wenn sie nicht geladen wird. +pdfjs-editor-alt-text-add-description-label = Beschreibung hinzufügen +pdfjs-editor-alt-text-add-description-description = Ziel sind 1-2 Sätze, die das Thema, das Szenario oder Aktionen beschreiben. +pdfjs-editor-alt-text-mark-decorative-label = Als dekorativ markieren +pdfjs-editor-alt-text-mark-decorative-description = Dies wird für Ziergrafiken wie Ränder oder Wasserzeichen verwendet. +pdfjs-editor-alt-text-cancel-button = Abbrechen +pdfjs-editor-alt-text-save-button = Speichern +pdfjs-editor-alt-text-decorative-tooltip = Als dekorativ markiert +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Zum Beispiel: "Ein junger Mann setzt sich an einen Tisch, um zu essen." +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternativ-Text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Linke obere Ecke - Größe ändern +pdfjs-editor-resizer-label-top-middle = Oben mittig - Größe ändern +pdfjs-editor-resizer-label-top-right = Rechts oben - Größe ändern +pdfjs-editor-resizer-label-middle-right = Mitte rechts - Größe ändern +pdfjs-editor-resizer-label-bottom-right = Rechte untere Ecke - Größe ändern +pdfjs-editor-resizer-label-bottom-middle = Unten mittig - Größe ändern +pdfjs-editor-resizer-label-bottom-left = Linke untere Ecke - Größe ändern +pdfjs-editor-resizer-label-middle-left = Mitte links - Größe ändern +pdfjs-editor-resizer-top-left = + .aria-label = Linke obere Ecke - Größe ändern +pdfjs-editor-resizer-top-middle = + .aria-label = Oben mittig - Größe ändern +pdfjs-editor-resizer-top-right = + .aria-label = Rechts oben - Größe ändern +pdfjs-editor-resizer-middle-right = + .aria-label = Mitte rechts - Größe ändern +pdfjs-editor-resizer-bottom-right = + .aria-label = Rechte untere Ecke - Größe ändern +pdfjs-editor-resizer-bottom-middle = + .aria-label = Unten mittig - Größe ändern +pdfjs-editor-resizer-bottom-left = + .aria-label = Linke untere Ecke - Größe ändern +pdfjs-editor-resizer-middle-left = + .aria-label = Mitte links - Größe ändern + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Hervorhebungsfarbe +pdfjs-editor-colorpicker-button = + .title = Farbe ändern +pdfjs-editor-colorpicker-dropdown = + .aria-label = Farbauswahl +pdfjs-editor-colorpicker-yellow = + .title = Gelb +pdfjs-editor-colorpicker-green = + .title = Grün +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Rot + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Alle anzeigen +pdfjs-editor-highlight-show-all-button = + .title = Alle anzeigen + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternativ-Text (Grafikbeschreibung) bearbeiten +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternativ-Text (Grafikbeschreibung) hinzufügen +pdfjs-editor-new-alt-text-textarea = + .placeholder = Schreiben Sie Ihre Beschreibung hier… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kurze Beschreibung für Personen, die die Grafik nicht sehen können, oder wenn die Grafik nicht geladen wird. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Dieser Alternativ-Text wurde automatisch erstellt und könnte ungenau sein. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Weitere Informationen +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternativ-Text automatisch erstellen +pdfjs-editor-new-alt-text-not-now-button = Nicht jetzt +pdfjs-editor-new-alt-text-error-title = Alternativ-Text konnte nicht automatisch erstellt werden +pdfjs-editor-new-alt-text-error-description = Bitte schreiben Sie Ihren eigenen Alternativ-Text oder versuchen Sie es später erneut. +pdfjs-editor-new-alt-text-error-close-button = Schließen +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Alternativ-Text-KI-Modell wird heruntergeladen ({ $downloadedSize } von { $totalSize } MB) + .aria-valuetext = Alternativ-Text-KI-Modell wird heruntergeladen ({ $downloadedSize } von { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativ-Text hinzugefügt +pdfjs-editor-new-alt-text-added-button-label = Alternativ-Text hinzugefügt +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Fehlender Alternativ-Text +pdfjs-editor-new-alt-text-missing-button-label = Fehlender Alternativ-Text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternativ-Text überprüfen +pdfjs-editor-new-alt-text-to-review-button-label = Alternativ-Text überprüfen +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatisch erstellt: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Alternativ-Text-Einstellungen für Grafiken +pdfjs-image-alt-text-settings-button-label = Alternativ-Text-Einstellungen für Grafiken +pdfjs-editor-alt-text-settings-dialog-label = Alternativ-Text-Einstellungen für Grafiken +pdfjs-editor-alt-text-settings-automatic-title = Automatischer Alternativ-Text +pdfjs-editor-alt-text-settings-create-model-button-label = Alternativ-Text automatisch erstellen +pdfjs-editor-alt-text-settings-create-model-description = Schlägt Beschreibungen vor, um Personen zu helfen, die die Grafik nicht sehen können, oder wenn die Grafik nicht geladen wird. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alternativ-Text-KI-Modell ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Wird lokal auf Ihrem Gerät ausgeführt, sodass Ihre Daten privat bleiben. Erforderlich für automatischen Alternativ-Text. +pdfjs-editor-alt-text-settings-delete-model-button = Löschen +pdfjs-editor-alt-text-settings-download-model-button = Herunterladen +pdfjs-editor-alt-text-settings-downloading-model-button = Wird heruntergeladen… +pdfjs-editor-alt-text-settings-editor-title = Alternativ-Texteditor +pdfjs-editor-alt-text-settings-show-dialog-button-label = Alternativ-Texteditor beim Hinzufügen einer Grafik anzeigen +pdfjs-editor-alt-text-settings-show-dialog-description = Hilft Ihnen, sicherzustellen, dass alle Ihre Grafiken Alternativ-Text haben. +pdfjs-editor-alt-text-settings-close-button = Schließen + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Hervorhebung entfernt +pdfjs-editor-undo-bar-message-freetext = Text entfernt +pdfjs-editor-undo-bar-message-ink = Zeichnung entfernt +pdfjs-editor-undo-bar-message-stamp = Grafik entfernt +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } Anmerkung entfernt + *[other] { $count } Anmerkungen entfernt + } +pdfjs-editor-undo-bar-undo-button = + .title = Rückgängig +pdfjs-editor-undo-bar-undo-button-label = Rückgängig +pdfjs-editor-undo-bar-close-button = + .title = Schließen +pdfjs-editor-undo-bar-close-button-label = Schließen diff --git a/public/assets/pdfjs/locale/dsb/viewer.ftl b/public/assets/pdfjs/locale/dsb/viewer.ftl new file mode 100644 index 0000000..24ac94f --- /dev/null +++ b/public/assets/pdfjs/locale/dsb/viewer.ftl @@ -0,0 +1,521 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pjerwjejšny bok +pdfjs-previous-button-label = Slědk +pdfjs-next-button = + .title = Pśiducy bok +pdfjs-next-button-label = Dalej +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Bok +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = z { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } z { $pagesCount }) +pdfjs-zoom-out-button = + .title = Pómjeńšyś +pdfjs-zoom-out-button-label = Pómjeńšyś +pdfjs-zoom-in-button = + .title = Pówětšyś +pdfjs-zoom-in-button-label = Pówětšyś +pdfjs-zoom-select = + .title = Skalěrowanje +pdfjs-presentation-mode-button = + .title = Do prezentaciskego modusa pśejś +pdfjs-presentation-mode-button-label = Prezentaciski modus +pdfjs-open-file-button = + .title = Dataju wócyniś +pdfjs-open-file-button-label = Wócyniś +pdfjs-print-button = + .title = Śišćaś +pdfjs-print-button-label = Śišćaś +pdfjs-save-button = + .title = Składowaś +pdfjs-save-button-label = Składowaś +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Ześěgnuś +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Ześěgnuś +pdfjs-bookmark-button = + .title = Aktualny bok (URL z aktualnego boka pokazaś) +pdfjs-bookmark-button-label = Aktualny bok + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Rědy +pdfjs-tools-button-label = Rědy +pdfjs-first-page-button = + .title = K prědnemu bokoju +pdfjs-first-page-button-label = K prědnemu bokoju +pdfjs-last-page-button = + .title = K slědnemu bokoju +pdfjs-last-page-button-label = K slědnemu bokoju +pdfjs-page-rotate-cw-button = + .title = Wobwjertnuś ako špěra źo +pdfjs-page-rotate-cw-button-label = Wobwjertnuś ako špěra źo +pdfjs-page-rotate-ccw-button = + .title = Wobwjertnuś nawopaki ako špěra źo +pdfjs-page-rotate-ccw-button-label = Wobwjertnuś nawopaki ako špěra źo +pdfjs-cursor-text-select-tool-button = + .title = Rěd za wuběranje teksta zmóžniś +pdfjs-cursor-text-select-tool-button-label = Rěd za wuběranje teksta +pdfjs-cursor-hand-tool-button = + .title = Rucny rěd zmóžniś +pdfjs-cursor-hand-tool-button-label = Rucny rěd +pdfjs-scroll-page-button = + .title = Kulanje boka wužywaś +pdfjs-scroll-page-button-label = Kulanje boka +pdfjs-scroll-vertical-button = + .title = Wertikalne suwanje wužywaś +pdfjs-scroll-vertical-button-label = Wertikalne suwanje +pdfjs-scroll-horizontal-button = + .title = Horicontalne suwanje wužywaś +pdfjs-scroll-horizontal-button-label = Horicontalne suwanje +pdfjs-scroll-wrapped-button = + .title = Pózlažke suwanje wužywaś +pdfjs-scroll-wrapped-button-label = Pózlažke suwanje +pdfjs-spread-none-button = + .title = Boki njezwězaś +pdfjs-spread-none-button-label = Žeden dwójny bok +pdfjs-spread-odd-button = + .title = Boki zachopinajucy z njerownymi bokami zwězaś +pdfjs-spread-odd-button-label = Njerowne boki +pdfjs-spread-even-button = + .title = Boki zachopinajucy z rownymi bokami zwězaś +pdfjs-spread-even-button-label = Rowne boki + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentowe kakosći… +pdfjs-document-properties-button-label = Dokumentowe kakosći… +pdfjs-document-properties-file-name = Mě dataje: +pdfjs-document-properties-file-size = Wjelikosć dataje: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bajtow) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtow) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajtow) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtow) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Awtor: +pdfjs-document-properties-subject = Tema: +pdfjs-document-properties-keywords = Klucowe słowa: +pdfjs-document-properties-creation-date = Datum napóranja: +pdfjs-document-properties-modification-date = Datum změny: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Awtor: +pdfjs-document-properties-producer = PDF-gótowaŕ: +pdfjs-document-properties-version = PDF-wersija: +pdfjs-document-properties-page-count = Licba bokow: +pdfjs-document-properties-page-size = Wjelikosć boka: +pdfjs-document-properties-page-size-unit-inches = col +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = wusoki format +pdfjs-document-properties-page-size-orientation-landscape = prěcny format +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Jo +pdfjs-document-properties-linearized-no = Ně +pdfjs-document-properties-close-button = Zacyniś + +## Print + +pdfjs-print-progress-message = Dokument pśigótujo se za śišćanje… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Pśetergnuś +pdfjs-printing-not-supported = Warnowanje: Śišćanje njepódpěra se połnje pśez toś ten wobglědowak. +pdfjs-printing-not-ready = Warnowanje: PDF njejo se za śišćanje dopołnje zacytał. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Bócnicu pokazaś/schowaś +pdfjs-toggle-sidebar-notification-button = + .title = Bocnicu pśešaltowaś (dokument rozrědowanje/pśipiski/warstwy wopśimujo) +pdfjs-toggle-sidebar-button-label = Bócnicu pokazaś/schowaś +pdfjs-document-outline-button = + .title = Dokumentowe naraźenje pokazaś (dwójne kliknjenje, aby se wšykne zapiski pokazali/schowali) +pdfjs-document-outline-button-label = Dokumentowa struktura +pdfjs-attachments-button = + .title = Pśidanki pokazaś +pdfjs-attachments-button-label = Pśidanki +pdfjs-layers-button = + .title = Warstwy pokazaś (klikniśo dwójcy, aby wšykne warstwy na standardny staw slědk stajił) +pdfjs-layers-button-label = Warstwy +pdfjs-thumbs-button = + .title = Miniatury pokazaś +pdfjs-thumbs-button-label = Miniatury +pdfjs-current-outline-item-button = + .title = Aktualny rozrědowański zapisk pytaś +pdfjs-current-outline-item-button-label = Aktualny rozrědowański zapisk +pdfjs-findbar-button = + .title = W dokumenśe pytaś +pdfjs-findbar-button-label = Pytaś +pdfjs-additional-layers = Dalšne warstwy + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Bok { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura boka { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Pytaś + .placeholder = W dokumenśe pytaś… +pdfjs-find-previous-button = + .title = Pjerwjejšne wustupowanje pytańskego wuraza pytaś +pdfjs-find-previous-button-label = Slědk +pdfjs-find-next-button = + .title = Pśidujuce wustupowanje pytańskego wuraza pytaś +pdfjs-find-next-button-label = Dalej +pdfjs-find-highlight-checkbox = Wšykne wuzwignuś +pdfjs-find-match-case-checkbox-label = Na wjelikopisanje źiwaś +pdfjs-find-match-diacritics-checkbox-label = Diakritiske znamuška wužywaś +pdfjs-find-entire-word-checkbox-label = Cełe słowa +pdfjs-find-reached-top = Zachopjeńk dokumenta dostany, pókšacujo se z kóńcom +pdfjs-find-reached-bottom = Kóńc dokumenta dostany, pókšacujo se ze zachopjeńkom +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } z { $total } wótpowědnika + [two] { $current } z { $total } wótpowědnikowu + [few] { $current } z { $total } wótpowědnikow + *[other] { $current } z { $total } wótpowědnikow + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Wušej { $limit } wótpowědnik + [two] Wušej { $limit } wótpowědnika + [few] Wušej { $limit } wótpowědniki + *[other] Wušej { $limit } wótpowědniki + } +pdfjs-find-not-found = Pytański wuraz njejo se namakał + +## Predefined zoom values + +pdfjs-page-scale-width = Šyrokosć boka +pdfjs-page-scale-fit = Wjelikosć boka +pdfjs-page-scale-auto = Awtomatiske skalěrowanje +pdfjs-page-scale-actual = Aktualna wjelikosć +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Bok { $page } + +## Loading indicator messages + +pdfjs-loading-error = Pśi zacytowanju PDF jo zmólka nastała. +pdfjs-invalid-file-error = Njepłaśiwa abo wobškóźona PDF-dataja. +pdfjs-missing-file-error = Felujuca PDF-dataja. +pdfjs-unexpected-response-error = Njewócakane serwerowe wótegrono. +pdfjs-rendering-error = Pśi zwobraznjanju boka jo zmólka nastała. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Typ pśipiskow: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Zapódajśo gronidło, aby PDF-dataju wócynił. +pdfjs-password-invalid = Njepłaśiwe gronidło. Pšosym wopytajśo hyšći raz. +pdfjs-password-ok-button = W pórěźe +pdfjs-password-cancel-button = Pśetergnuś +pdfjs-web-fonts-disabled = Webpisma su znjemóžnjone: njejo móžno, zasajźone PDF-pisma wužywaś. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Kresliś +pdfjs-editor-ink-button-label = Kresliś +pdfjs-editor-stamp-button = + .title = Wobraze pśidaś abo wobźěłaś +pdfjs-editor-stamp-button-label = Wobraze pśidaś abo wobźěłaś +pdfjs-editor-highlight-button = + .title = Wuzwignuś +pdfjs-editor-highlight-button-label = Wuzwignuś +pdfjs-highlight-floating-button1 = + .title = Wuzwignuś + .aria-label = Wuzwignuś +pdfjs-highlight-floating-button-label = Wuzwignuś + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Kreslanku wótwónoźeś +pdfjs-editor-remove-freetext-button = + .title = Tekst wótwónoźeś +pdfjs-editor-remove-stamp-button = + .title = Wobraz wótwónoźeś +pdfjs-editor-remove-highlight-button = + .title = Wuzwignjenje wótpóraś + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Barwa +pdfjs-editor-free-text-size-input = Wjelikosć +pdfjs-editor-ink-color-input = Barwa +pdfjs-editor-ink-thickness-input = Tłustosć +pdfjs-editor-ink-opacity-input = Opacita +pdfjs-editor-stamp-add-image-button = + .title = Wobraz pśidaś +pdfjs-editor-stamp-add-image-button-label = Wobraz pśidaś +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tłustosć +pdfjs-editor-free-highlight-thickness-title = + .title = Tłustosć změniś, gaž se zapiski wuzwiguju, kótarež tekst njejsu +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstowy editor + .default-content = Zachopśo pisaś … +pdfjs-free-text = + .aria-label = Tekstowy editor +pdfjs-free-text-default-content = Zachopśo pisaś… +pdfjs-ink = + .aria-label = Kresleński editor +pdfjs-ink-canvas = + .aria-label = Wobraz napórany wót wužywarja + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatiwny tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternatiwny tekst wobźěłaś +pdfjs-editor-alt-text-edit-button-label = Alternatiwny tekst wobźěłaś +pdfjs-editor-alt-text-dialog-label = Nastajenje wubraś +pdfjs-editor-alt-text-dialog-description = Alternatiwny tekst pomaga, gaž luźe njamógu wobraz wiźeś abo gaž se wobraz njezacytajo. +pdfjs-editor-alt-text-add-description-label = Wopisanje pśidaś +pdfjs-editor-alt-text-add-description-description = Pišćo 1 sadu abo 2 saźe, kótarejž temu, nastajenje abo akcije wopisujotej. +pdfjs-editor-alt-text-mark-decorative-label = Ako dekoratiwny markěrowaś +pdfjs-editor-alt-text-mark-decorative-description = To se za pyšnjece wobraze wužywa, na pśikład ramiki abo wódowe znamjenja. +pdfjs-editor-alt-text-cancel-button = Pśetergnuś +pdfjs-editor-alt-text-save-button = Składowaś +pdfjs-editor-alt-text-decorative-tooltip = Ako dekoratiwny markěrowany +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Na pśikład, „Młody muski za blidom sejźi, aby jěź jědł“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatiwny tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Górjejce nalěwo – wjelikosć změniś +pdfjs-editor-resizer-label-top-middle = Górjejce wesrjejź – wjelikosć změniś +pdfjs-editor-resizer-label-top-right = Górjejce napšawo – wjelikosć změniś +pdfjs-editor-resizer-label-middle-right = Wesrjejź napšawo – wjelikosć změniś +pdfjs-editor-resizer-label-bottom-right = Dołojce napšawo – wjelikosć změniś +pdfjs-editor-resizer-label-bottom-middle = Dołojce wesrjejź – wjelikosć změniś +pdfjs-editor-resizer-label-bottom-left = Dołojce nalěwo – wjelikosć změniś +pdfjs-editor-resizer-label-middle-left = Wesrjejź nalěwo – wjelikosć změniś +pdfjs-editor-resizer-top-left = + .aria-label = Górjejce nalěwo – wjelikosć změniś +pdfjs-editor-resizer-top-middle = + .aria-label = Górjejce wesrjejź – wjelikosć změniś +pdfjs-editor-resizer-top-right = + .aria-label = Górjejce napšawo – wjelikosć změniś +pdfjs-editor-resizer-middle-right = + .aria-label = Wesrjejź napšawo – wjelikosć změniś +pdfjs-editor-resizer-bottom-right = + .aria-label = Dołojce napšawo – wjelikosć změniś +pdfjs-editor-resizer-bottom-middle = + .aria-label = Dołojce wesrjejź – wjelikosć změniś +pdfjs-editor-resizer-bottom-left = + .aria-label = Dołojce nalěwo – wjelikosć změniś +pdfjs-editor-resizer-middle-left = + .aria-label = Wesrjejź nalěwo – wjelikosć změniś + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Barwa wuzwignjenja +pdfjs-editor-colorpicker-button = + .title = Barwu změniś +pdfjs-editor-colorpicker-dropdown = + .aria-label = Wuběrk barwow +pdfjs-editor-colorpicker-yellow = + .title = Žołty +pdfjs-editor-colorpicker-green = + .title = Zeleny +pdfjs-editor-colorpicker-blue = + .title = Módry +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Cerwjeny + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Wšykne pokazaś +pdfjs-editor-highlight-show-all-button = + .title = Wšykne pokazaś + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternatiwny tekst wobźěłaś (wobrazowe wopisanje) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternatiwny tekst pśidaś (wobrazowe wopisanje) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Pišćo how swójo wopisanje… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Krotke wopisanje za luźe, kótarež njamóžośo wobraz wiźeś abo gaž se wobraz njezacytajo. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Toś ten alternatiwny tekst jo se awtomatiski napórał a jo snaź njedokradny. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Dalšne informacije +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternatiwny tekst awtomatiski napóraś +pdfjs-editor-new-alt-text-not-now-button = Nic něnto +pdfjs-editor-new-alt-text-error-title = Alternatiwny tekst njedajo se awtomatiski napóraś +pdfjs-editor-new-alt-text-error-description = Pšosym pišćo swój alternatiwny tekst abo wopytajśo pózdźej hyšći raz. +pdfjs-editor-new-alt-text-error-close-button = Zacyniś +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Model KI za alternatiwny tekst se ześěgujo ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Model KI za alternatiwny tekst se ześěgujo ({ $downloadedSize } z { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatiwny tekst jo se pśidał +pdfjs-editor-new-alt-text-added-button-label = Alternatiwny tekst jo se pśidał +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Alternatiwny tekst felujo +pdfjs-editor-new-alt-text-missing-button-label = Alternatiwny tekst felujo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternatiwny tekst pśeglědowaś +pdfjs-editor-new-alt-text-to-review-button-label = Alternatiwny tekst pśeglědowaś +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Awtomatiski napórany: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Nastajenja alternatiwnego wobrazowego teksta +pdfjs-image-alt-text-settings-button-label = Nastajenja alternatiwnego wobrazowego teksta +pdfjs-editor-alt-text-settings-dialog-label = Nastajenja alternatiwnego wobrazowego teksta +pdfjs-editor-alt-text-settings-automatic-title = Awtomatiski alternatiwny tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Alternatiwny tekst awtomatiski napóraś +pdfjs-editor-alt-text-settings-create-model-description = Naraźujo wopisanja, aby pomagał ludam, kótarež njamóžośo wobraz wiźeś abo gaž se wobraz njezacytajo. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model KI alternatiwnego teksta ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Běžy lokalnje na wašom rěźe, aby waše daty priwatne wóstali. Za awtomatiski alternatiwny tekst trjebny. +pdfjs-editor-alt-text-settings-delete-model-button = Lašowaś +pdfjs-editor-alt-text-settings-download-model-button = Ześěgnuś +pdfjs-editor-alt-text-settings-downloading-model-button = Ześěgujo se… +pdfjs-editor-alt-text-settings-editor-title = Editor za alternatiwny tekst +pdfjs-editor-alt-text-settings-show-dialog-button-label = Editor alternatiwnego teksta ned pokazaś, gaž se wobraz pśidawa +pdfjs-editor-alt-text-settings-show-dialog-description = Pomaga, wam wšym swójim wobrazam alternatiwny tekst pśidaś. +pdfjs-editor-alt-text-settings-close-button = Zacyniś + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Wótwónoźone wuzwignuś +pdfjs-editor-undo-bar-message-freetext = Tekst jo se wótwónoźeł +pdfjs-editor-undo-bar-message-ink = Kreslanka jo se wótwónoźeła +pdfjs-editor-undo-bar-message-stamp = Wobraz jo se wótwónoźeł +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } pśipisk jo se wótwónoźeł + [two] { $count } pśipiska stej se wótwónoźełej + [few] { $count } pśipiski su se wótwónoźeli + *[other] { $count } pśipiskow jo se wótwónoźeło + } +pdfjs-editor-undo-bar-undo-button = + .title = Anulěrowaś +pdfjs-editor-undo-bar-undo-button-label = Anulěrowaś +pdfjs-editor-undo-bar-close-button = + .title = Zacyniś +pdfjs-editor-undo-bar-close-button-label = Zacyniś diff --git a/public/assets/pdfjs/locale/el/viewer.ftl b/public/assets/pdfjs/locale/el/viewer.ftl new file mode 100644 index 0000000..5a04bd8 --- /dev/null +++ b/public/assets/pdfjs/locale/el/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Προηγούμενη σελίδα +pdfjs-previous-button-label = Προηγούμενη +pdfjs-next-button = + .title = Επόμενη σελίδα +pdfjs-next-button-label = Επόμενη +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Σελίδα +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = από { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } από { $pagesCount }) +pdfjs-zoom-out-button = + .title = Σμίκρυνση +pdfjs-zoom-out-button-label = Σμίκρυνση +pdfjs-zoom-in-button = + .title = Μεγέθυνση +pdfjs-zoom-in-button-label = Μεγέθυνση +pdfjs-zoom-select = + .title = Ζουμ +pdfjs-presentation-mode-button = + .title = Εναλλαγή σε λειτουργία παρουσίασης +pdfjs-presentation-mode-button-label = Λειτουργία παρουσίασης +pdfjs-open-file-button = + .title = Άνοιγμα αρχείου +pdfjs-open-file-button-label = Άνοιγμα +pdfjs-print-button = + .title = Εκτύπωση +pdfjs-print-button-label = Εκτύπωση +pdfjs-save-button = + .title = Αποθήκευση +pdfjs-save-button-label = Αποθήκευση +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Λήψη +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Λήψη +pdfjs-bookmark-button = + .title = Τρέχουσα σελίδα (Προβολή URL από τρέχουσα σελίδα) +pdfjs-bookmark-button-label = Τρέχουσα σελίδα + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Εργαλεία +pdfjs-tools-button-label = Εργαλεία +pdfjs-first-page-button = + .title = Μετάβαση στην πρώτη σελίδα +pdfjs-first-page-button-label = Μετάβαση στην πρώτη σελίδα +pdfjs-last-page-button = + .title = Μετάβαση στην τελευταία σελίδα +pdfjs-last-page-button-label = Μετάβαση στην τελευταία σελίδα +pdfjs-page-rotate-cw-button = + .title = Δεξιόστροφη περιστροφή +pdfjs-page-rotate-cw-button-label = Δεξιόστροφη περιστροφή +pdfjs-page-rotate-ccw-button = + .title = Αριστερόστροφη περιστροφή +pdfjs-page-rotate-ccw-button-label = Αριστερόστροφη περιστροφή +pdfjs-cursor-text-select-tool-button = + .title = Ενεργοποίηση εργαλείου επιλογής κειμένου +pdfjs-cursor-text-select-tool-button-label = Εργαλείο επιλογής κειμένου +pdfjs-cursor-hand-tool-button = + .title = Ενεργοποίηση εργαλείου χεριού +pdfjs-cursor-hand-tool-button-label = Εργαλείο χεριού +pdfjs-scroll-page-button = + .title = Χρήση κύλισης σελίδας +pdfjs-scroll-page-button-label = Κύλιση σελίδας +pdfjs-scroll-vertical-button = + .title = Χρήση κάθετης κύλισης +pdfjs-scroll-vertical-button-label = Κάθετη κύλιση +pdfjs-scroll-horizontal-button = + .title = Χρήση οριζόντιας κύλισης +pdfjs-scroll-horizontal-button-label = Οριζόντια κύλιση +pdfjs-scroll-wrapped-button = + .title = Χρήση κυκλικής κύλισης +pdfjs-scroll-wrapped-button-label = Κυκλική κύλιση +pdfjs-spread-none-button = + .title = Να μη γίνει σύνδεση επεκτάσεων σελίδων +pdfjs-spread-none-button-label = Χωρίς επεκτάσεις +pdfjs-spread-odd-button = + .title = Σύνδεση επεκτάσεων σελίδων ξεκινώντας από τις μονές σελίδες +pdfjs-spread-odd-button-label = Μονές επεκτάσεις +pdfjs-spread-even-button = + .title = Σύνδεση επεκτάσεων σελίδων ξεκινώντας από τις ζυγές σελίδες +pdfjs-spread-even-button-label = Ζυγές επεκτάσεις + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Ιδιότητες εγγράφου… +pdfjs-document-properties-button-label = Ιδιότητες εγγράφου… +pdfjs-document-properties-file-name = Όνομα αρχείου: +pdfjs-document-properties-file-size = Μέγεθος αρχείου: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Τίτλος: +pdfjs-document-properties-author = Συγγραφέας: +pdfjs-document-properties-subject = Θέμα: +pdfjs-document-properties-keywords = Λέξεις-κλειδιά: +pdfjs-document-properties-creation-date = Ημερομηνία δημιουργίας: +pdfjs-document-properties-modification-date = Ημερομηνία τροποποίησης: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Δημιουργός: +pdfjs-document-properties-producer = Παραγωγός PDF: +pdfjs-document-properties-version = Έκδοση PDF: +pdfjs-document-properties-page-count = Αριθμός σελίδων: +pdfjs-document-properties-page-size = Μέγεθος σελίδας: +pdfjs-document-properties-page-size-unit-inches = ίντσες +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = κατακόρυφα +pdfjs-document-properties-page-size-orientation-landscape = οριζόντια +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Επιστολή +pdfjs-document-properties-page-size-name-legal = Τύπου Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Ταχεία προβολή ιστού: +pdfjs-document-properties-linearized-yes = Ναι +pdfjs-document-properties-linearized-no = Όχι +pdfjs-document-properties-close-button = Κλείσιμο + +## Print + +pdfjs-print-progress-message = Προετοιμασία του εγγράφου για εκτύπωση… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Ακύρωση +pdfjs-printing-not-supported = Προειδοποίηση: Η εκτύπωση δεν υποστηρίζεται πλήρως από το πρόγραμμα περιήγησης. +pdfjs-printing-not-ready = Προειδοποίηση: Το PDF δεν φορτώθηκε πλήρως για εκτύπωση. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = (Απ)ενεργοποίηση πλαϊνής γραμμής +pdfjs-toggle-sidebar-notification-button = + .title = (Απ)ενεργοποίηση πλαϊνής γραμμής (το έγγραφο περιέχει περίγραμμα/συνημμένα/επίπεδα) +pdfjs-toggle-sidebar-button-label = (Απ)ενεργοποίηση πλαϊνής γραμμής +pdfjs-document-outline-button = + .title = Εμφάνιση διάρθρωσης εγγράφου (διπλό κλικ για ανάπτυξη/σύμπτυξη όλων των στοιχείων) +pdfjs-document-outline-button-label = Διάρθρωση εγγράφου +pdfjs-attachments-button = + .title = Εμφάνιση συνημμένων +pdfjs-attachments-button-label = Συνημμένα +pdfjs-layers-button = + .title = Εμφάνιση επιπέδων (διπλό κλικ για επαναφορά όλων των επιπέδων στην προεπιλεγμένη κατάσταση) +pdfjs-layers-button-label = Επίπεδα +pdfjs-thumbs-button = + .title = Εμφάνιση μικρογραφιών +pdfjs-thumbs-button-label = Μικρογραφίες +pdfjs-current-outline-item-button = + .title = Εύρεση τρέχοντος στοιχείου διάρθρωσης +pdfjs-current-outline-item-button-label = Τρέχον στοιχείο διάρθρωσης +pdfjs-findbar-button = + .title = Εύρεση στο έγγραφο +pdfjs-findbar-button-label = Εύρεση +pdfjs-additional-layers = Επιπρόσθετα επίπεδα + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Σελίδα { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Μικρογραφία σελίδας { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Εύρεση + .placeholder = Εύρεση στο έγγραφο… +pdfjs-find-previous-button = + .title = Εύρεση της προηγούμενης εμφάνισης της φράσης +pdfjs-find-previous-button-label = Προηγούμενο +pdfjs-find-next-button = + .title = Εύρεση της επόμενης εμφάνισης της φράσης +pdfjs-find-next-button-label = Επόμενο +pdfjs-find-highlight-checkbox = Επισήμανση όλων +pdfjs-find-match-case-checkbox-label = Συμφωνία πεζών/κεφαλαίων +pdfjs-find-match-diacritics-checkbox-label = Αντιστοίχιση διακριτικών +pdfjs-find-entire-word-checkbox-label = Ολόκληρες λέξεις +pdfjs-find-reached-top = Φτάσατε στην αρχή του εγγράφου, συνέχεια από το τέλος +pdfjs-find-reached-bottom = Φτάσατε στο τέλος του εγγράφου, συνέχεια από την αρχή +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } από { $total } αντιστοιχία + *[other] { $current } από { $total } αντιστοιχίες + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Περισσότερες από { $limit } αντιστοιχία + *[other] Περισσότερες από { $limit } αντιστοιχίες + } +pdfjs-find-not-found = Η φράση δεν βρέθηκε + +## Predefined zoom values + +pdfjs-page-scale-width = Πλάτος σελίδας +pdfjs-page-scale-fit = Μέγεθος σελίδας +pdfjs-page-scale-auto = Αυτόματο ζουμ +pdfjs-page-scale-actual = Πραγματικό μέγεθος +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Σελίδα { $page } + +## Loading indicator messages + +pdfjs-loading-error = Προέκυψε σφάλμα κατά τη φόρτωση του PDF. +pdfjs-invalid-file-error = Μη έγκυρο ή κατεστραμμένο αρχείο PDF. +pdfjs-missing-file-error = Λείπει αρχείο PDF. +pdfjs-unexpected-response-error = Μη αναμενόμενη απόκριση από το διακομιστή. +pdfjs-rendering-error = Προέκυψε σφάλμα κατά την εμφάνιση της σελίδας. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Σχόλιο «{ $type }»] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Εισαγάγετε τον κωδικό πρόσβασης για να ανοίξετε αυτό το αρχείο PDF. +pdfjs-password-invalid = Μη έγκυρος κωδικός πρόσβασης. Παρακαλώ δοκιμάστε ξανά. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Ακύρωση +pdfjs-web-fonts-disabled = Οι γραμματοσειρές ιστού είναι ανενεργές: δεν είναι δυνατή η χρήση των ενσωματωμένων γραμματοσειρών PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Κείμενο +pdfjs-editor-free-text-button-label = Κείμενο +pdfjs-editor-ink-button = + .title = Σχέδιο +pdfjs-editor-ink-button-label = Σχέδιο +pdfjs-editor-stamp-button = + .title = Προσθήκη ή επεξεργασία εικόνων +pdfjs-editor-stamp-button-label = Προσθήκη ή επεξεργασία εικόνων +pdfjs-editor-highlight-button = + .title = Επισήμανση +pdfjs-editor-highlight-button-label = Επισήμανση +pdfjs-highlight-floating-button1 = + .title = Επισήμανση + .aria-label = Επισήμανση +pdfjs-highlight-floating-button-label = Επισήμανση + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Αφαίρεση σχεδίου +pdfjs-editor-remove-freetext-button = + .title = Αφαίρεση κειμένου +pdfjs-editor-remove-stamp-button = + .title = Αφαίρεση εικόνας +pdfjs-editor-remove-highlight-button = + .title = Αφαίρεση επισήμανσης + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Χρώμα +pdfjs-editor-free-text-size-input = Μέγεθος +pdfjs-editor-ink-color-input = Χρώμα +pdfjs-editor-ink-thickness-input = Πάχος +pdfjs-editor-ink-opacity-input = Αδιαφάνεια +pdfjs-editor-stamp-add-image-button = + .title = Προσθήκη εικόνας +pdfjs-editor-stamp-add-image-button-label = Προσθήκη εικόνας +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Πάχος +pdfjs-editor-free-highlight-thickness-title = + .title = Αλλαγή πάχους κατά την επισήμανση στοιχείων εκτός κειμένου +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Επεξεργασία κειμένου + .default-content = Ξεκινήστε να πληκτρολογείτε… +pdfjs-free-text = + .aria-label = Επεξεργασία κειμένου +pdfjs-free-text-default-content = Ξεκινήστε να πληκτρολογείτε… +pdfjs-ink = + .aria-label = Επεξεργασία σχεδίων +pdfjs-ink-canvas = + .aria-label = Εικόνα από τον χρήστη + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Εναλλακτικό κείμενο +pdfjs-editor-alt-text-edit-button = + .aria-label = Επεξεργασία εναλλακτικού κειμένου +pdfjs-editor-alt-text-edit-button-label = Επεξεργασία εναλλακτικού κειμένου +pdfjs-editor-alt-text-dialog-label = Διαλέξτε μια επιλογή +pdfjs-editor-alt-text-dialog-description = Το εναλλακτικό κείμενο είναι χρήσιμο όταν οι άνθρωποι δεν μπορούν να δουν την εικόνα ή όταν αυτή δεν φορτώνεται. +pdfjs-editor-alt-text-add-description-label = Προσθήκη περιγραφής +pdfjs-editor-alt-text-add-description-description = Στοχεύστε σε μία ή δύο προτάσεις που περιγράφουν το θέμα, τη ρύθμιση ή τις ενέργειες. +pdfjs-editor-alt-text-mark-decorative-label = Επισήμανση ως διακοσμητικό +pdfjs-editor-alt-text-mark-decorative-description = Χρησιμοποιείται για διακοσμητικές εικόνες, όπως περιγράμματα ή υδατογραφήματα. +pdfjs-editor-alt-text-cancel-button = Ακύρωση +pdfjs-editor-alt-text-save-button = Αποθήκευση +pdfjs-editor-alt-text-decorative-tooltip = Επισημασμένο ως διακοσμητικό +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Για παράδειγμα, «Ένας νεαρός άνδρας κάθεται σε ένα τραπέζι για να φάει ένα γεύμα» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Εναλλακτικό κείμενο + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Επάνω αριστερή γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-label-top-middle = Μέσο επάνω πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-label-top-right = Επάνω δεξιά γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-label-middle-right = Μέσο δεξιάς πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-label-bottom-right = Κάτω δεξιά γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-label-bottom-middle = Μέσο κάτω πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-label-bottom-left = Κάτω αριστερή γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-label-middle-left = Μέσο αριστερής πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-top-left = + .aria-label = Επάνω αριστερή γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-top-middle = + .aria-label = Μέσο επάνω πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-top-right = + .aria-label = Επάνω δεξιά γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-middle-right = + .aria-label = Μέσο δεξιάς πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-bottom-right = + .aria-label = Κάτω δεξιά γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-bottom-middle = + .aria-label = Μέσο κάτω πλευράς — αλλαγή μεγέθους +pdfjs-editor-resizer-bottom-left = + .aria-label = Κάτω αριστερή γωνία — αλλαγή μεγέθους +pdfjs-editor-resizer-middle-left = + .aria-label = Μέσο αριστερής πλευράς — αλλαγή μεγέθους + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Χρώμα επισήμανσης +pdfjs-editor-colorpicker-button = + .title = Αλλαγή χρώματος +pdfjs-editor-colorpicker-dropdown = + .aria-label = Επιλογές χρωμάτων +pdfjs-editor-colorpicker-yellow = + .title = Κίτρινο +pdfjs-editor-colorpicker-green = + .title = Πράσινο +pdfjs-editor-colorpicker-blue = + .title = Μπλε +pdfjs-editor-colorpicker-pink = + .title = Ροζ +pdfjs-editor-colorpicker-red = + .title = Κόκκινο + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Εμφάνιση όλων +pdfjs-editor-highlight-show-all-button = + .title = Εμφάνιση όλων + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Επεξεργασία εναλλακτικού κειμένου (περιγραφή εικόνας) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Προσθήκη εναλλακτικού κειμένου (περιγραφή εικόνας) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Γράψτε την περιγραφή σας εδώ… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Σύντομη περιγραφή για άτομα που δεν μπορούν να δουν την εικόνα ή όταν η εικόνα δεν φορτώνεται. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Αυτό το εναλλακτικό κείμενο δημιουργήθηκε αυτόματα και ενδέχεται να είναι ανακριβές. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Μάθετε περισσότερα +pdfjs-editor-new-alt-text-create-automatically-button-label = Αυτόματη δημιουργία εναλλακτικού κειμένου +pdfjs-editor-new-alt-text-not-now-button = Όχι τώρα +pdfjs-editor-new-alt-text-error-title = Δεν ήταν δυνατή η αυτόματη δημιουργία εναλλακτικού κειμένου +pdfjs-editor-new-alt-text-error-description = Γράψτε το δικό σας εναλλακτικό κείμενο ή δοκιμάστε ξανά αργότερα. +pdfjs-editor-new-alt-text-error-close-button = Κλείσιμο +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Λήψη μοντέλου AI εναλλακτικού κειμένου ({ $downloadedSize } από { $totalSize } MB) + .aria-valuetext = Λήψη μοντέλου AI εναλλακτικού κειμένου ({ $downloadedSize } από { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Προστέθηκε εναλλακτικό κείμενο +pdfjs-editor-new-alt-text-added-button-label = Προστέθηκε εναλλακτικό κείμενο +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Απουσία εναλλακτικού κειμένου +pdfjs-editor-new-alt-text-missing-button-label = Απουσία εναλλακτικού κειμένου +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Έλεγχος εναλλακτικού κειμένου +pdfjs-editor-new-alt-text-to-review-button-label = Έλεγχος εναλλακτικού κειμένου +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Αυτόματη δημιουργία: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ρυθμίσεις εναλλακτικού κειμένου εικόνας +pdfjs-image-alt-text-settings-button-label = Ρυθμίσεις εναλλακτικού κειμένου εικόνας +pdfjs-editor-alt-text-settings-dialog-label = Ρυθμίσεις εναλλακτικού κειμένου εικόνας +pdfjs-editor-alt-text-settings-automatic-title = Αυτόματο εναλλακτικό κείμενο +pdfjs-editor-alt-text-settings-create-model-button-label = Αυτόματη δημιουργία εναλλακτικού κειμένου +pdfjs-editor-alt-text-settings-create-model-description = Προτείνει περιγραφές για άτομα που δεν μπορούν να δουν την εικόνα ή όταν η εικόνα δεν φορτώνεται. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Μοντέλο AI εναλλακτικού κειμένου ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Εκτελείται τοπικά στη συσκευή σας, ώστε τα δεδομένα σας να παραμένουν ιδιωτικά. Απαιτείται για τη δημιουργία του αυτόματου εναλλακτικού κειμένου. +pdfjs-editor-alt-text-settings-delete-model-button = Διαγραφή +pdfjs-editor-alt-text-settings-download-model-button = Λήψη +pdfjs-editor-alt-text-settings-downloading-model-button = Λήψη… +pdfjs-editor-alt-text-settings-editor-title = Επεξεργασία εναλλακτικού κειμένου +pdfjs-editor-alt-text-settings-show-dialog-button-label = Άμεση εμφάνιση της επεξεργασίας εναλλακτικού κειμένου κατά την προσθήκη εικόνας +pdfjs-editor-alt-text-settings-show-dialog-description = Σας βοηθά να βεβαιωθείτε ότι όλες οι εικόνες σας έχουν εναλλακτικό κείμενο. +pdfjs-editor-alt-text-settings-close-button = Κλείσιμο + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Η επισήμανση αφαιρέθηκε +pdfjs-editor-undo-bar-message-freetext = Το κείμενο αφαιρέθηκε +pdfjs-editor-undo-bar-message-ink = Το σχέδιο αφαιρέθηκε +pdfjs-editor-undo-bar-message-stamp = Η εικόνα αφαιρέθηκε +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] Αφαιρέθηκε { $count } σχολιασμός + *[other] Αφαιρέθηκαν { $count } σχολιασμοί + } +pdfjs-editor-undo-bar-undo-button = + .title = Αναίρεση +pdfjs-editor-undo-bar-undo-button-label = Αναίρεση +pdfjs-editor-undo-bar-close-button = + .title = Κλείσιμο +pdfjs-editor-undo-bar-close-button-label = Κλείσιμο diff --git a/public/assets/pdfjs/locale/en-CA/viewer.ftl b/public/assets/pdfjs/locale/en-CA/viewer.ftl new file mode 100644 index 0000000..346e6e8 --- /dev/null +++ b/public/assets/pdfjs/locale/en-CA/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Previous Page +pdfjs-previous-button-label = Previous +pdfjs-next-button = + .title = Next Page +pdfjs-next-button-label = Next +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Page +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = of { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom Out +pdfjs-zoom-out-button-label = Zoom Out +pdfjs-zoom-in-button = + .title = Zoom In +pdfjs-zoom-in-button-label = Zoom In +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Switch to Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Open File +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Print +pdfjs-print-button-label = Print +pdfjs-save-button = + .title = Save +pdfjs-save-button-label = Save +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Download +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Download +pdfjs-bookmark-button = + .title = Current Page (View URL from Current Page) +pdfjs-bookmark-button-label = Current Page + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Go to First Page +pdfjs-first-page-button-label = Go to First Page +pdfjs-last-page-button = + .title = Go to Last Page +pdfjs-last-page-button-label = Go to Last Page +pdfjs-page-rotate-cw-button = + .title = Rotate Clockwise +pdfjs-page-rotate-cw-button-label = Rotate Clockwise +pdfjs-page-rotate-ccw-button = + .title = Rotate Counterclockwise +pdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise +pdfjs-cursor-text-select-tool-button = + .title = Enable Text Selection Tool +pdfjs-cursor-text-select-tool-button-label = Text Selection Tool +pdfjs-cursor-hand-tool-button = + .title = Enable Hand Tool +pdfjs-cursor-hand-tool-button-label = Hand Tool +pdfjs-scroll-page-button = + .title = Use Page Scrolling +pdfjs-scroll-page-button-label = Page Scrolling +pdfjs-scroll-vertical-button = + .title = Use Vertical Scrolling +pdfjs-scroll-vertical-button-label = Vertical Scrolling +pdfjs-scroll-horizontal-button = + .title = Use Horizontal Scrolling +pdfjs-scroll-horizontal-button-label = Horizontal Scrolling +pdfjs-scroll-wrapped-button = + .title = Use Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = Do not join page spreads +pdfjs-spread-none-button-label = No Spreads +pdfjs-spread-odd-button = + .title = Join page spreads starting with odd-numbered pages +pdfjs-spread-odd-button-label = Odd Spreads +pdfjs-spread-even-button = + .title = Join page spreads starting with even-numbered pages +pdfjs-spread-even-button-label = Even Spreads + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Document Properties… +pdfjs-document-properties-button-label = Document Properties… +pdfjs-document-properties-file-name = File name: +pdfjs-document-properties-file-size = File size: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Title: +pdfjs-document-properties-author = Author: +pdfjs-document-properties-subject = Subject: +pdfjs-document-properties-keywords = Keywords: +pdfjs-document-properties-creation-date = Creation Date: +pdfjs-document-properties-modification-date = Modification Date: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creator: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Page Count: +pdfjs-document-properties-page-size = Page Size: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = landscape +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Yes +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Close + +## Print + +pdfjs-print-progress-message = Preparing document for printing… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancel +pdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser. +pdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggle Sidebar +pdfjs-toggle-sidebar-notification-button = + .title = Toggle Sidebar (document contains outline/attachments/layers) +pdfjs-toggle-sidebar-button-label = Toggle Sidebar +pdfjs-document-outline-button = + .title = Show Document Outline (double-click to expand/collapse all items) +pdfjs-document-outline-button-label = Document Outline +pdfjs-attachments-button = + .title = Show Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-layers-button = + .title = Show Layers (double-click to reset all layers to the default state) +pdfjs-layers-button-label = Layers +pdfjs-thumbs-button = + .title = Show Thumbnails +pdfjs-thumbs-button-label = Thumbnails +pdfjs-current-outline-item-button = + .title = Find Current Outline Item +pdfjs-current-outline-item-button-label = Current Outline Item +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = Find +pdfjs-additional-layers = Additional Layers + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail of Page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find in document… +pdfjs-find-previous-button = + .title = Find the previous occurrence of the phrase +pdfjs-find-previous-button-label = Previous +pdfjs-find-next-button = + .title = Find the next occurrence of the phrase +pdfjs-find-next-button-label = Next +pdfjs-find-highlight-checkbox = Highlight All +pdfjs-find-match-case-checkbox-label = Match Case +pdfjs-find-match-diacritics-checkbox-label = Match Diacritics +pdfjs-find-entire-word-checkbox-label = Whole Words +pdfjs-find-reached-top = Reached top of document, continued from bottom +pdfjs-find-reached-bottom = Reached end of document, continued from top +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } of { $total } match + *[other] { $current } of { $total } matches + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] More than { $limit } match + *[other] More than { $limit } matches + } +pdfjs-find-not-found = Phrase not found + +## Predefined zoom values + +pdfjs-page-scale-width = Page Width +pdfjs-page-scale-fit = Page Fit +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Actual Size +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Page { $page } + +## Loading indicator messages + +pdfjs-loading-error = An error occurred while loading the PDF. +pdfjs-invalid-file-error = Invalid or corrupted PDF file. +pdfjs-missing-file-error = Missing PDF file. +pdfjs-unexpected-response-error = Unexpected server response. +pdfjs-rendering-error = An error occurred while rendering the page. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Enter the password to open this PDF file. +pdfjs-password-invalid = Invalid password. Please try again. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancel +pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Draw +pdfjs-editor-ink-button-label = Draw +pdfjs-editor-stamp-button = + .title = Add or edit images +pdfjs-editor-stamp-button-label = Add or edit images +pdfjs-editor-highlight-button = + .title = Highlight +pdfjs-editor-highlight-button-label = Highlight +pdfjs-highlight-floating-button1 = + .title = Highlight + .aria-label = Highlight +pdfjs-highlight-floating-button-label = Highlight + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remove drawing +pdfjs-editor-remove-freetext-button = + .title = Remove text +pdfjs-editor-remove-stamp-button = + .title = Remove image +pdfjs-editor-remove-highlight-button = + .title = Remove highlight + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colour +pdfjs-editor-free-text-size-input = Size +pdfjs-editor-ink-color-input = Colour +pdfjs-editor-ink-thickness-input = Thickness +pdfjs-editor-ink-opacity-input = Opacity +pdfjs-editor-stamp-add-image-button = + .title = Add image +pdfjs-editor-stamp-add-image-button-label = Add image +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Thickness +pdfjs-editor-free-highlight-thickness-title = + .title = Change thickness when highlighting items other than text +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Text Editor + .default-content = Start typing… +pdfjs-free-text = + .aria-label = Text Editor +pdfjs-free-text-default-content = Start typing… +pdfjs-ink = + .aria-label = Draw Editor +pdfjs-ink-canvas = + .aria-label = User-created image + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt text +pdfjs-editor-alt-text-edit-button = + .aria-label = Edit alt text +pdfjs-editor-alt-text-edit-button-label = Edit alt text +pdfjs-editor-alt-text-dialog-label = Choose an option +pdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can’t see the image or when it doesn’t load. +pdfjs-editor-alt-text-add-description-label = Add a description +pdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions. +pdfjs-editor-alt-text-mark-decorative-label = Mark as decorative +pdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks. +pdfjs-editor-alt-text-cancel-button = Cancel +pdfjs-editor-alt-text-save-button = Save +pdfjs-editor-alt-text-decorative-tooltip = Marked as decorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = For example, “A young man sits down at a table to eat a meal” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Top left corner — resize +pdfjs-editor-resizer-label-top-middle = Top middle — resize +pdfjs-editor-resizer-label-top-right = Top right corner — resize +pdfjs-editor-resizer-label-middle-right = Middle right — resize +pdfjs-editor-resizer-label-bottom-right = Bottom right corner — resize +pdfjs-editor-resizer-label-bottom-middle = Bottom middle — resize +pdfjs-editor-resizer-label-bottom-left = Bottom left corner — resize +pdfjs-editor-resizer-label-middle-left = Middle left — resize +pdfjs-editor-resizer-top-left = + .aria-label = Top left corner — resize +pdfjs-editor-resizer-top-middle = + .aria-label = Top middle — resize +pdfjs-editor-resizer-top-right = + .aria-label = Top right corner — resize +pdfjs-editor-resizer-middle-right = + .aria-label = Middle right — resize +pdfjs-editor-resizer-bottom-right = + .aria-label = Bottom right corner — resize +pdfjs-editor-resizer-bottom-middle = + .aria-label = Bottom middle — resize +pdfjs-editor-resizer-bottom-left = + .aria-label = Bottom left corner — resize +pdfjs-editor-resizer-middle-left = + .aria-label = Middle left — resize + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Highlight colour +pdfjs-editor-colorpicker-button = + .title = Change colour +pdfjs-editor-colorpicker-dropdown = + .aria-label = Colour choices +pdfjs-editor-colorpicker-yellow = + .title = Yellow +pdfjs-editor-colorpicker-green = + .title = Green +pdfjs-editor-colorpicker-blue = + .title = Blue +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Red + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Show all +pdfjs-editor-highlight-show-all-button = + .title = Show all + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Write your description here… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Short description for people who can’t see the image or when the image doesn’t load. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more +pdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically +pdfjs-editor-new-alt-text-not-now-button = Not now +pdfjs-editor-new-alt-text-error-title = Couldn’t create alt text automatically +pdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later. +pdfjs-editor-new-alt-text-error-close-button = Close +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) + .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alt text added +pdfjs-editor-new-alt-text-added-button-label = Alt text added +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Missing alt text +pdfjs-editor-new-alt-text-missing-button-label = Missing alt text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Review alt text +pdfjs-editor-new-alt-text-to-review-button-label = Review alt text +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Image alt text settings +pdfjs-image-alt-text-settings-button-label = Image alt text settings +pdfjs-editor-alt-text-settings-dialog-label = Image alt text settings +pdfjs-editor-alt-text-settings-automatic-title = Automatic alt text +pdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically +pdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can’t see the image or when the image doesn’t load. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text. +pdfjs-editor-alt-text-settings-delete-model-button = Delete +pdfjs-editor-alt-text-settings-download-model-button = Download +pdfjs-editor-alt-text-settings-downloading-model-button = Downloading… +pdfjs-editor-alt-text-settings-editor-title = Alt text editor +pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image +pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. +pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/public/assets/pdfjs/locale/en-GB/viewer.ftl b/public/assets/pdfjs/locale/en-GB/viewer.ftl new file mode 100644 index 0000000..4222f6f --- /dev/null +++ b/public/assets/pdfjs/locale/en-GB/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Previous Page +pdfjs-previous-button-label = Previous +pdfjs-next-button = + .title = Next Page +pdfjs-next-button-label = Next +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Page +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = of { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom Out +pdfjs-zoom-out-button-label = Zoom Out +pdfjs-zoom-in-button = + .title = Zoom In +pdfjs-zoom-in-button-label = Zoom In +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Switch to Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Open File +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Print +pdfjs-print-button-label = Print +pdfjs-save-button = + .title = Save +pdfjs-save-button-label = Save +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Download +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Download +pdfjs-bookmark-button = + .title = Current Page (View URL from Current Page) +pdfjs-bookmark-button-label = Current Page + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Go to First Page +pdfjs-first-page-button-label = Go to First Page +pdfjs-last-page-button = + .title = Go to Last Page +pdfjs-last-page-button-label = Go to Last Page +pdfjs-page-rotate-cw-button = + .title = Rotate Clockwise +pdfjs-page-rotate-cw-button-label = Rotate Clockwise +pdfjs-page-rotate-ccw-button = + .title = Rotate Anti-Clockwise +pdfjs-page-rotate-ccw-button-label = Rotate Anti-Clockwise +pdfjs-cursor-text-select-tool-button = + .title = Enable Text Selection Tool +pdfjs-cursor-text-select-tool-button-label = Text Selection Tool +pdfjs-cursor-hand-tool-button = + .title = Enable Hand Tool +pdfjs-cursor-hand-tool-button-label = Hand Tool +pdfjs-scroll-page-button = + .title = Use Page Scrolling +pdfjs-scroll-page-button-label = Page Scrolling +pdfjs-scroll-vertical-button = + .title = Use Vertical Scrolling +pdfjs-scroll-vertical-button-label = Vertical Scrolling +pdfjs-scroll-horizontal-button = + .title = Use Horizontal Scrolling +pdfjs-scroll-horizontal-button-label = Horizontal Scrolling +pdfjs-scroll-wrapped-button = + .title = Use Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = Do not join page spreads +pdfjs-spread-none-button-label = No Spreads +pdfjs-spread-odd-button = + .title = Join page spreads starting with odd-numbered pages +pdfjs-spread-odd-button-label = Odd Spreads +pdfjs-spread-even-button = + .title = Join page spreads starting with even-numbered pages +pdfjs-spread-even-button-label = Even Spreads + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Document Properties… +pdfjs-document-properties-button-label = Document Properties… +pdfjs-document-properties-file-name = File name: +pdfjs-document-properties-file-size = File size: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Title: +pdfjs-document-properties-author = Author: +pdfjs-document-properties-subject = Subject: +pdfjs-document-properties-keywords = Keywords: +pdfjs-document-properties-creation-date = Creation Date: +pdfjs-document-properties-modification-date = Modification Date: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creator: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Page Count: +pdfjs-document-properties-page-size = Page Size: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = landscape +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Yes +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Close + +## Print + +pdfjs-print-progress-message = Preparing document for printing… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancel +pdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser. +pdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggle Sidebar +pdfjs-toggle-sidebar-notification-button = + .title = Toggle Sidebar (document contains outline/attachments/layers) +pdfjs-toggle-sidebar-button-label = Toggle Sidebar +pdfjs-document-outline-button = + .title = Show Document Outline (double-click to expand/collapse all items) +pdfjs-document-outline-button-label = Document Outline +pdfjs-attachments-button = + .title = Show Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-layers-button = + .title = Show Layers (double-click to reset all layers to the default state) +pdfjs-layers-button-label = Layers +pdfjs-thumbs-button = + .title = Show Thumbnails +pdfjs-thumbs-button-label = Thumbnails +pdfjs-current-outline-item-button = + .title = Find Current Outline Item +pdfjs-current-outline-item-button-label = Current Outline Item +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = Find +pdfjs-additional-layers = Additional Layers + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail of Page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find in document… +pdfjs-find-previous-button = + .title = Find the previous occurrence of the phrase +pdfjs-find-previous-button-label = Previous +pdfjs-find-next-button = + .title = Find the next occurrence of the phrase +pdfjs-find-next-button-label = Next +pdfjs-find-highlight-checkbox = Highlight All +pdfjs-find-match-case-checkbox-label = Match Case +pdfjs-find-match-diacritics-checkbox-label = Match Diacritics +pdfjs-find-entire-word-checkbox-label = Whole Words +pdfjs-find-reached-top = Reached top of document, continued from bottom +pdfjs-find-reached-bottom = Reached end of document, continued from top +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } of { $total } match + *[other] { $current } of { $total } matches + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] More than { $limit } match + *[other] More than { $limit } matches + } +pdfjs-find-not-found = Phrase not found + +## Predefined zoom values + +pdfjs-page-scale-width = Page Width +pdfjs-page-scale-fit = Page Fit +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Actual Size +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Page { $page } + +## Loading indicator messages + +pdfjs-loading-error = An error occurred while loading the PDF. +pdfjs-invalid-file-error = Invalid or corrupted PDF file. +pdfjs-missing-file-error = Missing PDF file. +pdfjs-unexpected-response-error = Unexpected server response. +pdfjs-rendering-error = An error occurred while rendering the page. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Enter the password to open this PDF file. +pdfjs-password-invalid = Invalid password. Please try again. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancel +pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Draw +pdfjs-editor-ink-button-label = Draw +pdfjs-editor-stamp-button = + .title = Add or edit images +pdfjs-editor-stamp-button-label = Add or edit images +pdfjs-editor-highlight-button = + .title = Highlight +pdfjs-editor-highlight-button-label = Highlight +pdfjs-highlight-floating-button1 = + .title = Highlight + .aria-label = Highlight +pdfjs-highlight-floating-button-label = Highlight + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remove drawing +pdfjs-editor-remove-freetext-button = + .title = Remove text +pdfjs-editor-remove-stamp-button = + .title = Remove image +pdfjs-editor-remove-highlight-button = + .title = Remove highlight + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colour +pdfjs-editor-free-text-size-input = Size +pdfjs-editor-ink-color-input = Colour +pdfjs-editor-ink-thickness-input = Thickness +pdfjs-editor-ink-opacity-input = Opacity +pdfjs-editor-stamp-add-image-button = + .title = Add image +pdfjs-editor-stamp-add-image-button-label = Add image +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Thickness +pdfjs-editor-free-highlight-thickness-title = + .title = Change thickness when highlighting items other than text +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Text Editor + .default-content = Start typing… +pdfjs-free-text = + .aria-label = Text Editor +pdfjs-free-text-default-content = Start typing… +pdfjs-ink = + .aria-label = Draw Editor +pdfjs-ink-canvas = + .aria-label = User-created image + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt text +pdfjs-editor-alt-text-edit-button = + .aria-label = Edit alt text +pdfjs-editor-alt-text-edit-button-label = Edit alt text +pdfjs-editor-alt-text-dialog-label = Choose an option +pdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can’t see the image or when it doesn’t load. +pdfjs-editor-alt-text-add-description-label = Add a description +pdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions. +pdfjs-editor-alt-text-mark-decorative-label = Mark as decorative +pdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks. +pdfjs-editor-alt-text-cancel-button = Cancel +pdfjs-editor-alt-text-save-button = Save +pdfjs-editor-alt-text-decorative-tooltip = Marked as decorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = For example, “A young man sits down at a table to eat a meal” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Top left corner — resize +pdfjs-editor-resizer-label-top-middle = Top middle — resize +pdfjs-editor-resizer-label-top-right = Top right corner — resize +pdfjs-editor-resizer-label-middle-right = Middle right — resize +pdfjs-editor-resizer-label-bottom-right = Bottom right corner — resize +pdfjs-editor-resizer-label-bottom-middle = Bottom middle — resize +pdfjs-editor-resizer-label-bottom-left = Bottom left corner — resize +pdfjs-editor-resizer-label-middle-left = Middle left — resize +pdfjs-editor-resizer-top-left = + .aria-label = Top left corner — resize +pdfjs-editor-resizer-top-middle = + .aria-label = Top middle — resize +pdfjs-editor-resizer-top-right = + .aria-label = Top right corner — resize +pdfjs-editor-resizer-middle-right = + .aria-label = Middle right — resize +pdfjs-editor-resizer-bottom-right = + .aria-label = Bottom right corner — resize +pdfjs-editor-resizer-bottom-middle = + .aria-label = Bottom middle — resize +pdfjs-editor-resizer-bottom-left = + .aria-label = Bottom left corner — resize +pdfjs-editor-resizer-middle-left = + .aria-label = Middle left — resize + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Highlight colour +pdfjs-editor-colorpicker-button = + .title = Change colour +pdfjs-editor-colorpicker-dropdown = + .aria-label = Colour choices +pdfjs-editor-colorpicker-yellow = + .title = Yellow +pdfjs-editor-colorpicker-green = + .title = Green +pdfjs-editor-colorpicker-blue = + .title = Blue +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Red + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Show all +pdfjs-editor-highlight-show-all-button = + .title = Show all + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Write your description here… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Short description for people who can’t see the image or when the image doesn’t load. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more +pdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically +pdfjs-editor-new-alt-text-not-now-button = Not now +pdfjs-editor-new-alt-text-error-title = Couldn’t create alt text automatically +pdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later. +pdfjs-editor-new-alt-text-error-close-button = Close +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) + .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alt text added +pdfjs-editor-new-alt-text-added-button-label = Alt text added +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Missing alt text +pdfjs-editor-new-alt-text-missing-button-label = Missing alt text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Review alt text +pdfjs-editor-new-alt-text-to-review-button-label = Review alt text +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Image alt text settings +pdfjs-image-alt-text-settings-button-label = Image alt text settings +pdfjs-editor-alt-text-settings-dialog-label = Image alt text settings +pdfjs-editor-alt-text-settings-automatic-title = Automatic alt text +pdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically +pdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can’t see the image or when the image doesn’t load. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text. +pdfjs-editor-alt-text-settings-delete-model-button = Delete +pdfjs-editor-alt-text-settings-download-model-button = Download +pdfjs-editor-alt-text-settings-downloading-model-button = Downloading… +pdfjs-editor-alt-text-settings-editor-title = Alt text editor +pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image +pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. +pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/public/assets/pdfjs/locale/en-US/viewer.ftl b/public/assets/pdfjs/locale/en-US/viewer.ftl new file mode 100644 index 0000000..3e4a351 --- /dev/null +++ b/public/assets/pdfjs/locale/en-US/viewer.ftl @@ -0,0 +1,526 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Previous Page +pdfjs-previous-button-label = Previous +pdfjs-next-button = + .title = Next Page +pdfjs-next-button-label = Next + +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Page + +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = of { $pagesCount } + +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) + +pdfjs-zoom-out-button = + .title = Zoom Out +pdfjs-zoom-out-button-label = Zoom Out +pdfjs-zoom-in-button = + .title = Zoom In +pdfjs-zoom-in-button-label = Zoom In +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Switch to Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Open File +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Print +pdfjs-print-button-label = Print +pdfjs-save-button = + .title = Save +pdfjs-save-button-label = Save + +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Download + +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Download + +pdfjs-bookmark-button = + .title = Current Page (View URL from Current Page) +pdfjs-bookmark-button-label = Current Page + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools + +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Go to First Page +pdfjs-first-page-button-label = Go to First Page +pdfjs-last-page-button = + .title = Go to Last Page +pdfjs-last-page-button-label = Go to Last Page +pdfjs-page-rotate-cw-button = + .title = Rotate Clockwise +pdfjs-page-rotate-cw-button-label = Rotate Clockwise +pdfjs-page-rotate-ccw-button = + .title = Rotate Counterclockwise +pdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise +pdfjs-cursor-text-select-tool-button = + .title = Enable Text Selection Tool +pdfjs-cursor-text-select-tool-button-label = Text Selection Tool +pdfjs-cursor-hand-tool-button = + .title = Enable Hand Tool +pdfjs-cursor-hand-tool-button-label = Hand Tool +pdfjs-scroll-page-button = + .title = Use Page Scrolling +pdfjs-scroll-page-button-label = Page Scrolling +pdfjs-scroll-vertical-button = + .title = Use Vertical Scrolling +pdfjs-scroll-vertical-button-label = Vertical Scrolling +pdfjs-scroll-horizontal-button = + .title = Use Horizontal Scrolling +pdfjs-scroll-horizontal-button-label = Horizontal Scrolling +pdfjs-scroll-wrapped-button = + .title = Use Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = Do not join page spreads +pdfjs-spread-none-button-label = No Spreads +pdfjs-spread-odd-button = + .title = Join page spreads starting with odd-numbered pages +pdfjs-spread-odd-button-label = Odd Spreads +pdfjs-spread-even-button = + .title = Join page spreads starting with even-numbered pages +pdfjs-spread-even-button-label = Even Spreads + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Document Properties… +pdfjs-document-properties-button-label = Document Properties… +pdfjs-document-properties-file-name = File name: +pdfjs-document-properties-file-size = File size: + +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) + +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) + +pdfjs-document-properties-title = Title: +pdfjs-document-properties-author = Author: +pdfjs-document-properties-subject = Subject: +pdfjs-document-properties-keywords = Keywords: +pdfjs-document-properties-creation-date = Creation Date: +pdfjs-document-properties-modification-date = Modification Date: + +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +pdfjs-document-properties-creator = Creator: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Page Count: +pdfjs-document-properties-page-size = Page Size: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = landscape +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Yes +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Close + +## Print + +pdfjs-print-progress-message = Preparing document for printing… + +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% + +pdfjs-print-progress-close-button = Cancel +pdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser. +pdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggle Sidebar +pdfjs-toggle-sidebar-notification-button = + .title = Toggle Sidebar (document contains outline/attachments/layers) +pdfjs-toggle-sidebar-button-label = Toggle Sidebar +pdfjs-document-outline-button = + .title = Show Document Outline (double-click to expand/collapse all items) +pdfjs-document-outline-button-label = Document Outline +pdfjs-attachments-button = + .title = Show Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-layers-button = + .title = Show Layers (double-click to reset all layers to the default state) +pdfjs-layers-button-label = Layers +pdfjs-thumbs-button = + .title = Show Thumbnails +pdfjs-thumbs-button-label = Thumbnails +pdfjs-current-outline-item-button = + .title = Find Current Outline Item +pdfjs-current-outline-item-button-label = Current Outline Item +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = Find +pdfjs-additional-layers = Additional Layers + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail of Page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find in document… +pdfjs-find-previous-button = + .title = Find the previous occurrence of the phrase +pdfjs-find-previous-button-label = Previous +pdfjs-find-next-button = + .title = Find the next occurrence of the phrase +pdfjs-find-next-button-label = Next +pdfjs-find-highlight-checkbox = Highlight All +pdfjs-find-match-case-checkbox-label = Match Case +pdfjs-find-match-diacritics-checkbox-label = Match Diacritics +pdfjs-find-entire-word-checkbox-label = Whole Words +pdfjs-find-reached-top = Reached top of document, continued from bottom +pdfjs-find-reached-bottom = Reached end of document, continued from top + +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } of { $total } match + *[other] { $current } of { $total } matches + } + +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] More than { $limit } match + *[other] More than { $limit } matches + } + +pdfjs-find-not-found = Phrase not found + +## Predefined zoom values + +pdfjs-page-scale-width = Page Width +pdfjs-page-scale-fit = Page Fit +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Actual Size + +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Page { $page } + +## Loading indicator messages + +pdfjs-loading-error = An error occurred while loading the PDF. +pdfjs-invalid-file-error = Invalid or corrupted PDF file. +pdfjs-missing-file-error = Missing PDF file. +pdfjs-unexpected-response-error = Unexpected server response. +pdfjs-rendering-error = An error occurred while rendering the page. + +## Annotations + +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = Enter the password to open this PDF file. +pdfjs-password-invalid = Invalid password. Please try again. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancel +pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Draw +pdfjs-editor-ink-button-label = Draw +pdfjs-editor-stamp-button = + .title = Add or edit images +pdfjs-editor-stamp-button-label = Add or edit images +pdfjs-editor-highlight-button = + .title = Highlight +pdfjs-editor-highlight-button-label = Highlight +pdfjs-highlight-floating-button1 = + .title = Highlight + .aria-label = Highlight +pdfjs-highlight-floating-button-label = Highlight + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remove drawing +pdfjs-editor-remove-freetext-button = + .title = Remove text +pdfjs-editor-remove-stamp-button = + .title = Remove image +pdfjs-editor-remove-highlight-button = + .title = Remove highlight + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Size +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Thickness +pdfjs-editor-ink-opacity-input = Opacity +pdfjs-editor-stamp-add-image-button = + .title = Add image +pdfjs-editor-stamp-add-image-button-label = Add image +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Thickness +pdfjs-editor-free-highlight-thickness-title = + .title = Change thickness when highlighting items other than text + +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Text Editor + .default-content = Start typing… +pdfjs-ink = + .aria-label = Draw Editor +pdfjs-ink-canvas = + .aria-label = User-created image + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt text +pdfjs-editor-alt-text-button-label = Alt text + +pdfjs-editor-alt-text-edit-button = + .aria-label = Edit alt text +pdfjs-editor-alt-text-dialog-label = Choose an option +pdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can’t see the image or when it doesn’t load. +pdfjs-editor-alt-text-add-description-label = Add a description +pdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions. +pdfjs-editor-alt-text-mark-decorative-label = Mark as decorative +pdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks. +pdfjs-editor-alt-text-cancel-button = Cancel +pdfjs-editor-alt-text-save-button = Save +pdfjs-editor-alt-text-decorative-tooltip = Marked as decorative + +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = For example, “A young man sits down at a table to eat a meal” + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-top-left = + .aria-label = Top left corner — resize +pdfjs-editor-resizer-top-middle = + .aria-label = Top middle — resize +pdfjs-editor-resizer-top-right = + .aria-label = Top right corner — resize +pdfjs-editor-resizer-middle-right = + .aria-label = Middle right — resize +pdfjs-editor-resizer-bottom-right = + .aria-label = Bottom right corner — resize +pdfjs-editor-resizer-bottom-middle = + .aria-label = Bottom middle — resize +pdfjs-editor-resizer-bottom-left = + .aria-label = Bottom left corner — resize +pdfjs-editor-resizer-middle-left = + .aria-label = Middle left — resize + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Highlight color + +pdfjs-editor-colorpicker-button = + .title = Change color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Color choices +pdfjs-editor-colorpicker-yellow = + .title = Yellow +pdfjs-editor-colorpicker-green = + .title = Green +pdfjs-editor-colorpicker-blue = + .title = Blue +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Red + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Show all +pdfjs-editor-highlight-show-all-button = + .title = Show all + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description) + +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description) + +pdfjs-editor-new-alt-text-textarea = + .placeholder = Write your description here… + +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Short description for people who can’t see the image or when the image doesn’t load. + +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more + +pdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically +pdfjs-editor-new-alt-text-not-now-button = Not now +pdfjs-editor-new-alt-text-error-title = Couldn’t create alt text automatically +pdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later. +pdfjs-editor-new-alt-text-error-close-button = Close + +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) + .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB) + +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alt text added +pdfjs-editor-new-alt-text-added-button-label = Alt text added + +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Missing alt text +pdfjs-editor-new-alt-text-missing-button-label = Missing alt text + +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Review alt text +pdfjs-editor-new-alt-text-to-review-button-label = Review alt text + +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Image alt text settings +pdfjs-image-alt-text-settings-button-label = Image alt text settings + +pdfjs-editor-alt-text-settings-dialog-label = Image alt text settings +pdfjs-editor-alt-text-settings-automatic-title = Automatic alt text +pdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically +pdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can’t see the image or when the image doesn’t load. + +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB) + +pdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text. +pdfjs-editor-alt-text-settings-delete-model-button = Delete +pdfjs-editor-alt-text-settings-download-model-button = Download +pdfjs-editor-alt-text-settings-downloading-model-button = Downloading… + +pdfjs-editor-alt-text-settings-editor-title = Alt text editor +pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image +pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text. +pdfjs-editor-alt-text-settings-close-button = Close + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Highlight removed +pdfjs-editor-undo-bar-message-freetext = Text removed +pdfjs-editor-undo-bar-message-ink = Drawing removed +pdfjs-editor-undo-bar-message-stamp = Image removed +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removed + *[other] { $count } annotations removed + } + +pdfjs-editor-undo-bar-undo-button = + .title = Undo +pdfjs-editor-undo-bar-undo-button-label = Undo +pdfjs-editor-undo-bar-close-button = + .title = Close +pdfjs-editor-undo-bar-close-button-label = Close diff --git a/public/assets/pdfjs/locale/eo/viewer.ftl b/public/assets/pdfjs/locale/eo/viewer.ftl new file mode 100644 index 0000000..ce45ebf --- /dev/null +++ b/public/assets/pdfjs/locale/eo/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Antaŭa paĝo +pdfjs-previous-button-label = Malantaŭen +pdfjs-next-button = + .title = Venonta paĝo +pdfjs-next-button-label = Antaŭen +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Paĝo +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = el { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } el { $pagesCount }) +pdfjs-zoom-out-button = + .title = Malpligrandigi +pdfjs-zoom-out-button-label = Malpligrandigi +pdfjs-zoom-in-button = + .title = Pligrandigi +pdfjs-zoom-in-button-label = Pligrandigi +pdfjs-zoom-select = + .title = Pligrandigilo +pdfjs-presentation-mode-button = + .title = Iri al prezenta reĝimo +pdfjs-presentation-mode-button-label = Prezenta reĝimo +pdfjs-open-file-button = + .title = Malfermi dosieron +pdfjs-open-file-button-label = Malfermi +pdfjs-print-button = + .title = Presi +pdfjs-print-button-label = Presi +pdfjs-save-button = + .title = Konservi +pdfjs-save-button-label = Konservi +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Elŝuti +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Elŝuti +pdfjs-bookmark-button = + .title = Nuna paĝo (Montri adreson de la nuna paĝo) +pdfjs-bookmark-button-label = Nuna paĝo + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Iloj +pdfjs-tools-button-label = Iloj +pdfjs-first-page-button = + .title = Iri al la unua paĝo +pdfjs-first-page-button-label = Iri al la unua paĝo +pdfjs-last-page-button = + .title = Iri al la lasta paĝo +pdfjs-last-page-button-label = Iri al la lasta paĝo +pdfjs-page-rotate-cw-button = + .title = Rotaciigi dekstrume +pdfjs-page-rotate-cw-button-label = Rotaciigi dekstrume +pdfjs-page-rotate-ccw-button = + .title = Rotaciigi maldekstrume +pdfjs-page-rotate-ccw-button-label = Rotaciigi maldekstrume +pdfjs-cursor-text-select-tool-button = + .title = Aktivigi tekstan elektilon +pdfjs-cursor-text-select-tool-button-label = Teksta elektilo +pdfjs-cursor-hand-tool-button = + .title = Aktivigi ilon de mano +pdfjs-cursor-hand-tool-button-label = Ilo de mano +pdfjs-scroll-page-button = + .title = Uzi rulumon de paĝo +pdfjs-scroll-page-button-label = Rulumo de paĝo +pdfjs-scroll-vertical-button = + .title = Uzi vertikalan rulumon +pdfjs-scroll-vertical-button-label = Vertikala rulumo +pdfjs-scroll-horizontal-button = + .title = Uzi horizontalan rulumon +pdfjs-scroll-horizontal-button-label = Horizontala rulumo +pdfjs-scroll-wrapped-button = + .title = Uzi ambaŭdirektan rulumon +pdfjs-scroll-wrapped-button-label = Ambaŭdirekta rulumo +pdfjs-spread-none-button = + .title = Ne montri paĝojn po du +pdfjs-spread-none-button-label = Unupaĝa vido +pdfjs-spread-odd-button = + .title = Kunigi paĝojn komencante per nepara paĝo +pdfjs-spread-odd-button-label = Po du paĝoj, neparaj maldekstre +pdfjs-spread-even-button = + .title = Kunigi paĝojn komencante per para paĝo +pdfjs-spread-even-button-label = Po du paĝoj, paraj maldekstre + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Atributoj de dokumento… +pdfjs-document-properties-button-label = Atributoj de dokumento… +pdfjs-document-properties-file-name = Nomo de dosiero: +pdfjs-document-properties-file-size = Grando de dosiero: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KO ({ $b } oktetoj) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } Mo ({ $b } oktetoj) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KO ({ $size_b } oktetoj) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MO ({ $size_b } oktetoj) +pdfjs-document-properties-title = Titolo: +pdfjs-document-properties-author = Aŭtoro: +pdfjs-document-properties-subject = Temo: +pdfjs-document-properties-keywords = Ŝlosilvorto: +pdfjs-document-properties-creation-date = Dato de kreado: +pdfjs-document-properties-modification-date = Dato de modifo: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Kreinto: +pdfjs-document-properties-producer = Produktinto de PDF: +pdfjs-document-properties-version = Versio de PDF: +pdfjs-document-properties-page-count = Nombro de paĝoj: +pdfjs-document-properties-page-size = Grando de paĝo: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertikala +pdfjs-document-properties-page-size-orientation-landscape = horizontala +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letera +pdfjs-document-properties-page-size-name-legal = Jura + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Rapida tekstaĵa vido: +pdfjs-document-properties-linearized-yes = Jes +pdfjs-document-properties-linearized-no = Ne +pdfjs-document-properties-close-button = Fermi + +## Print + +pdfjs-print-progress-message = Preparo de dokumento por presi ĝin … +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Nuligi +pdfjs-printing-not-supported = Averto: tiu ĉi retumilo ne plene subtenas presadon. +pdfjs-printing-not-ready = Averto: la PDF dosiero ne estas plene ŝargita por presado. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Montri/kaŝi flankan strion +pdfjs-toggle-sidebar-notification-button = + .title = Montri/kaŝi flankan strion (la dokumento enhavas konturon/kunsendaĵojn/tavolojn) +pdfjs-toggle-sidebar-button-label = Montri/kaŝi flankan strion +pdfjs-document-outline-button = + .title = Montri la konturon de dokumento (alklaku duoble por faldi/malfaldi ĉiujn elementojn) +pdfjs-document-outline-button-label = Konturo de dokumento +pdfjs-attachments-button = + .title = Montri kunsendaĵojn +pdfjs-attachments-button-label = Kunsendaĵojn +pdfjs-layers-button = + .title = Montri tavolojn (duoble alklaku por remeti ĉiujn tavolojn en la norman staton) +pdfjs-layers-button-label = Tavoloj +pdfjs-thumbs-button = + .title = Montri miniaturojn +pdfjs-thumbs-button-label = Miniaturoj +pdfjs-current-outline-item-button = + .title = Trovi nunan konturan elementon +pdfjs-current-outline-item-button-label = Nuna kontura elemento +pdfjs-findbar-button = + .title = Serĉi en dokumento +pdfjs-findbar-button-label = Serĉi +pdfjs-additional-layers = Aldonaj tavoloj + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Paĝo { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniaturo de paĝo { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Serĉi + .placeholder = Serĉi en dokumento… +pdfjs-find-previous-button = + .title = Serĉi la antaŭan aperon de la frazo +pdfjs-find-previous-button-label = Malantaŭen +pdfjs-find-next-button = + .title = Serĉi la venontan aperon de la frazo +pdfjs-find-next-button-label = Antaŭen +pdfjs-find-highlight-checkbox = Elstarigi ĉiujn +pdfjs-find-match-case-checkbox-label = Distingi inter majuskloj kaj minuskloj +pdfjs-find-match-diacritics-checkbox-label = Respekti supersignojn +pdfjs-find-entire-word-checkbox-label = Tutaj vortoj +pdfjs-find-reached-top = Komenco de la dokumento atingita, daŭrigado ekde la fino +pdfjs-find-reached-bottom = Fino de la dokumento atingita, daŭrigado ekde la komenco +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } el { $total } kongruo + *[other] { $current } el { $total } kongruoj + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Pli ol { $limit } kongruo + *[other] Pli ol { $limit } kongruoj + } +pdfjs-find-not-found = Frazo ne trovita + +## Predefined zoom values + +pdfjs-page-scale-width = Larĝo de paĝo +pdfjs-page-scale-fit = Adapti paĝon +pdfjs-page-scale-auto = Aŭtomata skalo +pdfjs-page-scale-actual = Reala grando +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Paĝo { $page } + +## Loading indicator messages + +pdfjs-loading-error = Okazis eraro dum la ŝargado de la PDF dosiero. +pdfjs-invalid-file-error = Nevalida aŭ difektita PDF dosiero. +pdfjs-missing-file-error = Mankas dosiero PDF. +pdfjs-unexpected-response-error = Neatendita respondo de servilo. +pdfjs-rendering-error = Okazis eraro dum la montro de la paĝo. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Prinoto: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Tajpu pasvorton por malfermi tiun ĉi dosieron PDF. +pdfjs-password-invalid = Nevalida pasvorto. Bonvolu provi denove. +pdfjs-password-ok-button = Akcepti +pdfjs-password-cancel-button = Nuligi +pdfjs-web-fonts-disabled = Neaktivaj teksaĵaj tiparoj: ne elbas uzi enmetitajn tiparojn de PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Teksto +pdfjs-editor-free-text-button-label = Teksto +pdfjs-editor-ink-button = + .title = Desegni +pdfjs-editor-ink-button-label = Desegni +pdfjs-editor-stamp-button = + .title = Aldoni aŭ modifi bildojn +pdfjs-editor-stamp-button-label = Aldoni aŭ modifi bildojn +pdfjs-editor-highlight-button = + .title = Elstarigi +pdfjs-editor-highlight-button-label = Elstarigi +pdfjs-highlight-floating-button1 = + .title = Elstarigi + .aria-label = Elstarigi +pdfjs-highlight-floating-button-label = Elstarigi + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Forigi desegnon +pdfjs-editor-remove-freetext-button = + .title = Forigi tekston +pdfjs-editor-remove-stamp-button = + .title = Forigi bildon +pdfjs-editor-remove-highlight-button = + .title = Forigi elstaraĵon + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Koloro +pdfjs-editor-free-text-size-input = Grando +pdfjs-editor-ink-color-input = Koloro +pdfjs-editor-ink-thickness-input = Dikeco +pdfjs-editor-ink-opacity-input = Maldiafaneco +pdfjs-editor-stamp-add-image-button = + .title = Aldoni bildon +pdfjs-editor-stamp-add-image-button-label = Aldoni bildon +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Dikeco +pdfjs-editor-free-highlight-thickness-title = + .title = Ŝanĝi dikecon dum elstarigo de netekstaj elementoj +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Teksta redaktilo + .default-content = Komencu tajpi… +pdfjs-free-text = + .aria-label = Teksta redaktilo +pdfjs-free-text-default-content = Ektajpi… +pdfjs-ink = + .aria-label = Desegnan redaktilon +pdfjs-ink-canvas = + .aria-label = Bildo kreita de uzanto + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternativa teksto +pdfjs-editor-alt-text-edit-button = + .aria-label = Redakti alternativan tekston +pdfjs-editor-alt-text-edit-button-label = Redakti alternativan tekston +pdfjs-editor-alt-text-dialog-label = Elektu eblon +pdfjs-editor-alt-text-dialog-description = Alternativa teksto helpas personojn, en la okazoj kiam ili ne povas vidi aŭ ŝargi la bildon. +pdfjs-editor-alt-text-add-description-label = Aldoni priskribon +pdfjs-editor-alt-text-add-description-description = La celo estas unu aŭ du frazoj, kiuj priskribas la temon, etoson aŭ agojn. +pdfjs-editor-alt-text-mark-decorative-label = Marki kiel ornaman +pdfjs-editor-alt-text-mark-decorative-description = Tio ĉi estas uzita por ornamaj bildoj, kiel randoj aŭ fonaj bildoj. +pdfjs-editor-alt-text-cancel-button = Nuligi +pdfjs-editor-alt-text-save-button = Konservi +pdfjs-editor-alt-text-decorative-tooltip = Markita kiel ornama +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Ekzemple: “Juna persono sidiĝas ĉetable por ekmanĝi” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternativa teksto + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Supra maldekstra angulo — ŝangi grandon +pdfjs-editor-resizer-label-top-middle = Supra mezo — ŝanĝi grandon +pdfjs-editor-resizer-label-top-right = Supran dekstran angulon — ŝanĝi grandon +pdfjs-editor-resizer-label-middle-right = Dekstra mezo — ŝanĝi grandon +pdfjs-editor-resizer-label-bottom-right = Malsupra deksta angulo — ŝanĝi grandon +pdfjs-editor-resizer-label-bottom-middle = Malsupra mezo — ŝanĝi grandon +pdfjs-editor-resizer-label-bottom-left = Malsupra maldekstra angulo — ŝanĝi grandon +pdfjs-editor-resizer-label-middle-left = Maldekstra mezo — ŝanĝi grandon +pdfjs-editor-resizer-top-left = + .aria-label = Supra maldekstra angulo — ŝangi grandon +pdfjs-editor-resizer-top-middle = + .aria-label = Supra mezo — ŝanĝi grandon +pdfjs-editor-resizer-top-right = + .aria-label = Supran dekstran angulon — ŝanĝi grandon +pdfjs-editor-resizer-middle-right = + .aria-label = Dekstra mezo — ŝanĝi grandon +pdfjs-editor-resizer-bottom-right = + .aria-label = Malsupra deksta angulo — ŝanĝi grandon +pdfjs-editor-resizer-bottom-middle = + .aria-label = Malsupra mezo — ŝanĝi grandon +pdfjs-editor-resizer-bottom-left = + .aria-label = Malsupra maldekstra angulo — ŝanĝi grandon +pdfjs-editor-resizer-middle-left = + .aria-label = Maldekstra mezo — ŝanĝi grandon + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Elstarigi koloron +pdfjs-editor-colorpicker-button = + .title = Ŝanĝi koloron +pdfjs-editor-colorpicker-dropdown = + .aria-label = Elekto de koloroj +pdfjs-editor-colorpicker-yellow = + .title = Flava +pdfjs-editor-colorpicker-green = + .title = Verda +pdfjs-editor-colorpicker-blue = + .title = Blua +pdfjs-editor-colorpicker-pink = + .title = Roza +pdfjs-editor-colorpicker-red = + .title = Ruĝa + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Montri ĉiujn +pdfjs-editor-highlight-show-all-button = + .title = Montri ĉiujn + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Modifi alternativan tekston (priskribo de bildo) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Aldoni alternativan tekston (priskribo de bildo) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skribu vian priskribon ĉi tie… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Mallonga priskribo por personoj kiuj ne povas vidi la bildon kaj por montri kiam la bildo ne ŝargeblas. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Tiu ĉi alternativa teksto estis aŭtomate kreita kaj povus esti malĝusta. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Pli da informo +pdfjs-editor-new-alt-text-create-automatically-button-label = Aŭtomate krei alternativan tekston +pdfjs-editor-new-alt-text-not-now-button = Ne nun +pdfjs-editor-new-alt-text-error-title = Ne eblis aŭtomate krei alternativan tekston +pdfjs-editor-new-alt-text-error-description = Bonvolu skribi vian propran alternativan tekston aŭ provi denove poste. +pdfjs-editor-new-alt-text-error-close-button = Fermi +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Elŝuto de modelo de artefarita intelekto por alternativa teksto ({ $downloadedSize } el { $totalSize } MO) + .aria-valuetext = Elŝuto de modelo de artefarita intelekto por alternativa teksto ({ $downloadedSize } el { $totalSize } MO) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativa teksto aldonita +pdfjs-editor-new-alt-text-added-button-label = Alternativa teksto aldonita +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Mankas alternativa teksto +pdfjs-editor-new-alt-text-missing-button-label = Mankas alternativa teksto +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Kontroli alternativan tekston +pdfjs-editor-new-alt-text-to-review-button-label = Kontroli alternativan tekston +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Aŭtomate kreita: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Agordoj por alternativa teksto de bildoj +pdfjs-image-alt-text-settings-button-label = Agordoj por alternativa teksto de bildoj +pdfjs-editor-alt-text-settings-dialog-label = Agordoj por alternativa teksto de bildoj +pdfjs-editor-alt-text-settings-automatic-title = Aŭtomata alternativa teksto +pdfjs-editor-alt-text-settings-create-model-button-label = Aŭtomate krei alternativan tekston +pdfjs-editor-alt-text-settings-create-model-description = Tio ĉi sugestas priskribojn por helpi personojn kiuj ne povas vidi aŭ ŝargi la bildon. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de artefarita intelekto por alternativa teksto ({ $totalSize } MO) +pdfjs-editor-alt-text-settings-ai-model-description = Ĝi funkcias en via aparato, do viaj datumoj restas privataj. Ĝi estas postulata por aŭtomata kreado de alternativa teksto. +pdfjs-editor-alt-text-settings-delete-model-button = Forigi +pdfjs-editor-alt-text-settings-download-model-button = Elŝuti +pdfjs-editor-alt-text-settings-downloading-model-button = Elŝuto… +pdfjs-editor-alt-text-settings-editor-title = Redaktilo de alternativa teksto +pdfjs-editor-alt-text-settings-show-dialog-button-label = Montri redaktilon de alternativa teksto tuj post aldono de bildo +pdfjs-editor-alt-text-settings-show-dialog-description = Tio ĉi helpas vin kontroli ĉu ĉiuj bildoj havas alternativan tekston. +pdfjs-editor-alt-text-settings-close-button = Fermi + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Elstaraĵo forigita +pdfjs-editor-undo-bar-message-freetext = Teksto forigita +pdfjs-editor-undo-bar-message-ink = Desegno forigita +pdfjs-editor-undo-bar-message-stamp = Bildo forigita +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] unu prinoto forigita + *[other] { $count } prinotoj forigitaj + } +pdfjs-editor-undo-bar-undo-button = + .title = Malfari +pdfjs-editor-undo-bar-undo-button-label = Malfari +pdfjs-editor-undo-bar-close-button = + .title = Fermi +pdfjs-editor-undo-bar-close-button-label = Fermi diff --git a/public/assets/pdfjs/locale/es-AR/viewer.ftl b/public/assets/pdfjs/locale/es-AR/viewer.ftl new file mode 100644 index 0000000..ca73dc7 --- /dev/null +++ b/public/assets/pdfjs/locale/es-AR/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Página siguiente +pdfjs-next-button-label = Siguiente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ( { $pageNumber } de { $pagesCount } ) +pdfjs-zoom-out-button = + .title = Alejar +pdfjs-zoom-out-button-label = Alejar +pdfjs-zoom-in-button = + .title = Acercar +pdfjs-zoom-in-button-label = Acercar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Cambiar a modo presentación +pdfjs-presentation-mode-button-label = Modo presentación +pdfjs-open-file-button = + .title = Abrir archivo +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Guardar +pdfjs-save-button-label = Guardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Descargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Descargar +pdfjs-bookmark-button = + .title = Página actual (Ver URL de la página actual) +pdfjs-bookmark-button-label = Página actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Herramientas +pdfjs-tools-button-label = Herramientas +pdfjs-first-page-button = + .title = Ir a primera página +pdfjs-first-page-button-label = Ir a primera página +pdfjs-last-page-button = + .title = Ir a última página +pdfjs-last-page-button-label = Ir a última página +pdfjs-page-rotate-cw-button = + .title = Rotar horario +pdfjs-page-rotate-cw-button-label = Rotar horario +pdfjs-page-rotate-ccw-button = + .title = Rotar antihorario +pdfjs-page-rotate-ccw-button-label = Rotar antihorario +pdfjs-cursor-text-select-tool-button = + .title = Habilitar herramienta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Herramienta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Habilitar herramienta mano +pdfjs-cursor-hand-tool-button-label = Herramienta mano +pdfjs-scroll-page-button = + .title = Usar desplazamiento de página +pdfjs-scroll-page-button-label = Desplazamiento de página +pdfjs-scroll-vertical-button = + .title = Usar desplazamiento vertical +pdfjs-scroll-vertical-button-label = Desplazamiento vertical +pdfjs-scroll-horizontal-button = + .title = Usar desplazamiento vertical +pdfjs-scroll-horizontal-button-label = Desplazamiento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar desplazamiento encapsulado +pdfjs-scroll-wrapped-button-label = Desplazamiento encapsulado +pdfjs-spread-none-button = + .title = No unir páginas dobles +pdfjs-spread-none-button-label = Sin dobles +pdfjs-spread-odd-button = + .title = Unir páginas dobles comenzando con las impares +pdfjs-spread-odd-button-label = Dobles impares +pdfjs-spread-even-button = + .title = Unir páginas dobles comenzando con las pares +pdfjs-spread-even-button-label = Dobles pares + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades del documento… +pdfjs-document-properties-button-label = Propiedades del documento… +pdfjs-document-properties-file-name = Nombre de archivo: +pdfjs-document-properties-file-size = Tamaño de archovo: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Asunto: +pdfjs-document-properties-keywords = Palabras clave: +pdfjs-document-properties-creation-date = Fecha de creación: +pdfjs-document-properties-modification-date = Fecha de modificación: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creador: +pdfjs-document-properties-producer = PDF Productor: +pdfjs-document-properties-version = Versión de PDF: +pdfjs-document-properties-page-count = Cantidad de páginas: +pdfjs-document-properties-page-size = Tamaño de página: +pdfjs-document-properties-page-size-unit-inches = en +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = normal +pdfjs-document-properties-page-size-orientation-landscape = apaisado +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista rápida de la Web: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Cerrar + +## Print + +pdfjs-print-progress-message = Preparando documento para imprimir… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Advertencia: La impresión no está totalmente soportada por este navegador. +pdfjs-printing-not-ready = Advertencia: El PDF no está completamente cargado para impresión. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Alternar barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Alternar barra lateral (el documento contiene esquemas/adjuntos/capas) +pdfjs-toggle-sidebar-button-label = Alternar barra lateral +pdfjs-document-outline-button = + .title = Mostrar esquema del documento (doble clic para expandir/colapsar todos los ítems) +pdfjs-document-outline-button-label = Esquema del documento +pdfjs-attachments-button = + .title = Mostrar adjuntos +pdfjs-attachments-button-label = Adjuntos +pdfjs-layers-button = + .title = Mostrar capas (doble clic para restablecer todas las capas al estado predeterminado) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Buscar elemento de esquema actual +pdfjs-current-outline-item-button-label = Elemento de esquema actual +pdfjs-findbar-button = + .title = Buscar en documento +pdfjs-findbar-button-label = Buscar +pdfjs-additional-layers = Capas adicionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Buscar + .placeholder = Buscar en documento… +pdfjs-find-previous-button = + .title = Buscar la aparición anterior de la frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Buscar la siguiente aparición de la frase +pdfjs-find-next-button-label = Siguiente +pdfjs-find-highlight-checkbox = Resaltar todo +pdfjs-find-match-case-checkbox-label = Coincidir mayúsculas +pdfjs-find-match-diacritics-checkbox-label = Coincidir diacríticos +pdfjs-find-entire-word-checkbox-label = Palabras completas +pdfjs-find-reached-top = Inicio de documento alcanzado, continuando desde abajo +pdfjs-find-reached-bottom = Fin de documento alcanzando, continuando desde arriba +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } coincidencia + *[other] { $current } de { $total } coincidencias + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Más de { $limit } coincidencia + *[other] Más de { $limit } coincidencias + } +pdfjs-find-not-found = Frase no encontrada + +## Predefined zoom values + +pdfjs-page-scale-width = Ancho de página +pdfjs-page-scale-fit = Ajustar página +pdfjs-page-scale-auto = Zoom automático +pdfjs-page-scale-actual = Tamaño real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ocurrió un error al cargar el PDF. +pdfjs-invalid-file-error = Archivo PDF no válido o cocrrupto. +pdfjs-missing-file-error = Archivo PDF faltante. +pdfjs-unexpected-response-error = Respuesta del servidor inesperada. +pdfjs-rendering-error = Ocurrió un error al dibujar la página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Anotación] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Ingrese la contraseña para abrir este archivo PDF +pdfjs-password-invalid = Contraseña inválida. Intente nuevamente. +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Tipografía web deshabilitada: no se pueden usar tipos incrustados en PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Dibujar +pdfjs-editor-ink-button-label = Dibujar +pdfjs-editor-stamp-button = + .title = Agregar o editar imágenes +pdfjs-editor-stamp-button-label = Agregar o editar imágenes +pdfjs-editor-highlight-button = + .title = Resaltar +pdfjs-editor-highlight-button-label = Resaltar +pdfjs-highlight-floating-button1 = + .title = Resaltar + .aria-label = Resaltar +pdfjs-highlight-floating-button-label = Resaltar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Eliminar dibujo +pdfjs-editor-remove-freetext-button = + .title = Eliminar texto +pdfjs-editor-remove-stamp-button = + .title = Eliminar imagen +pdfjs-editor-remove-highlight-button = + .title = Eliminar resaltado + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Tamaño +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Espesor +pdfjs-editor-ink-opacity-input = Opacidad +pdfjs-editor-stamp-add-image-button = + .title = Agregar una imagen +pdfjs-editor-stamp-add-image-button-label = Agregar una imagen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grosor +pdfjs-editor-free-highlight-thickness-title = + .title = Cambiar el grosor al resaltar elementos que no sean texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Comenzar a tipear… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Empezar a tipear… +pdfjs-ink = + .aria-label = Editor de dibujos +pdfjs-ink-canvas = + .aria-label = Imagen creada por el usuario + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar el texto alternativo +pdfjs-editor-alt-text-dialog-label = Eligir una opción +pdfjs-editor-alt-text-dialog-description = El texto alternativo (texto alternativo) ayuda cuando las personas no pueden ver la imagen o cuando no se carga. +pdfjs-editor-alt-text-add-description-label = Agregar una descripción +pdfjs-editor-alt-text-add-description-description = Intente escribir 1 o 2 oraciones que describan el tema, el entorno o las acciones. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativo +pdfjs-editor-alt-text-mark-decorative-description = Esto se usa para imágenes ornamentales, como bordes o marcas de agua. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Guardar +pdfjs-editor-alt-text-decorative-tooltip = Marcado como decorativo +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por ejemplo: “Un joven se sienta a la mesa a comer” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Esquina superior izquierda — cambiar el tamaño +pdfjs-editor-resizer-label-top-middle = Arriba en el medio — cambiar el tamaño +pdfjs-editor-resizer-label-top-right = Esquina superior derecha — cambiar el tamaño +pdfjs-editor-resizer-label-middle-right = Al centro a la derecha — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-right = Esquina inferior derecha — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-middle = Abajo en el medio — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-left = Esquina inferior izquierda — cambiar el tamaño +pdfjs-editor-resizer-label-middle-left = Al centro a la izquierda — cambiar el tamaño +pdfjs-editor-resizer-top-left = + .aria-label = Esquina superior izquierda — cambiar el tamaño +pdfjs-editor-resizer-top-middle = + .aria-label = Arriba en el medio — cambiar el tamaño +pdfjs-editor-resizer-top-right = + .aria-label = Esquina superior derecha — cambiar el tamaño +pdfjs-editor-resizer-middle-right = + .aria-label = Al centro a la derecha — cambiar el tamaño +pdfjs-editor-resizer-bottom-right = + .aria-label = Esquina inferior derecha — cambiar el tamaño +pdfjs-editor-resizer-bottom-middle = + .aria-label = Abajo en el medio — cambiar el tamaño +pdfjs-editor-resizer-bottom-left = + .aria-label = Esquina inferior izquierda — cambiar el tamaño +pdfjs-editor-resizer-middle-left = + .aria-label = Al centro a la izquierda — cambiar el tamaño + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de resaltado +pdfjs-editor-colorpicker-button = + .title = Cambiar el color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opciones de color +pdfjs-editor-colorpicker-yellow = + .title = Amarillo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosado +pdfjs-editor-colorpicker-red = + .title = Rojo + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar todo +pdfjs-editor-highlight-show-all-button = + .title = Mostrar todo + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descripción de la imagen) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Agregar texto alternativo (descripción de la imagen) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escribir la descripción aquí… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Descripción corta para las personas que no pueden ver la imagen o cuando la imagen no se carga. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo fue creado automáticamente y puede ser incorrecto. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Conocer más +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear texto alternativo automáticamente +pdfjs-editor-new-alt-text-not-now-button = No ahora +pdfjs-editor-new-alt-text-error-title = No se pudo crear el texto alternativo automáticamente +pdfjs-editor-new-alt-text-error-description = Escriba su propio texto alternativo o pruebe nuevamente más tarde. +pdfjs-editor-new-alt-text-error-close-button = Cerrar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Descargando modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Descargando modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Texto alternativo agregado +pdfjs-editor-new-alt-text-added-button-label = Texto alternativo agregado +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Falta el texto alternativo +pdfjs-editor-new-alt-text-missing-button-label = Falta el texto alternativo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Calificar el texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Revisar el texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creado automáticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Configuración de texto alternativo de la imagen +pdfjs-image-alt-text-settings-button-label = Configuración de texto alternativo de la imagen +pdfjs-editor-alt-text-settings-dialog-label = Configuración de texto alternativo de la imagen +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Crear texto alternativo automáticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugiere descripciones para ayudar a las personas que no pueden ver la imagen o cuando la imagen no se carga. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de IA de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Se ejecuta localmente en el dispositivo para que los datos se mantengan privados. Requerido para texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Borrar +pdfjs-editor-alt-text-settings-download-model-button = Descargar +pdfjs-editor-alt-text-settings-downloading-model-button = Descargando… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar el editor de texto alternativo inmediatamente al agregar una imagen +pdfjs-editor-alt-text-settings-show-dialog-description = Te ayuda a asegurarse de que todas las imágenes tengan texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Cerrar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Resaltado eliminado +pdfjs-editor-undo-bar-message-freetext = Texto eliminado +pdfjs-editor-undo-bar-message-ink = Dibujo eliminado +pdfjs-editor-undo-bar-message-stamp = Imagen eliminado +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotación eliminada + *[other] { $count } anotaciones eliminadas + } +pdfjs-editor-undo-bar-undo-button = + .title = Deshacer +pdfjs-editor-undo-bar-undo-button-label = Deshacer +pdfjs-editor-undo-bar-close-button = + .title = Cerrar +pdfjs-editor-undo-bar-close-button-label = Cerrar diff --git a/public/assets/pdfjs/locale/es-CL/viewer.ftl b/public/assets/pdfjs/locale/es-CL/viewer.ftl new file mode 100644 index 0000000..74389e4 --- /dev/null +++ b/public/assets/pdfjs/locale/es-CL/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Página siguiente +pdfjs-next-button-label = Siguiente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Alejar +pdfjs-zoom-out-button-label = Alejar +pdfjs-zoom-in-button = + .title = Acercar +pdfjs-zoom-in-button-label = Acercar +pdfjs-zoom-select = + .title = Ampliación +pdfjs-presentation-mode-button = + .title = Cambiar al modo de presentación +pdfjs-presentation-mode-button-label = Modo de presentación +pdfjs-open-file-button = + .title = Abrir archivo +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Guardar +pdfjs-save-button-label = Guardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Descargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Descargar +pdfjs-bookmark-button = + .title = Página actual (Ver URL de la página actual) +pdfjs-bookmark-button-label = Página actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Herramientas +pdfjs-tools-button-label = Herramientas +pdfjs-first-page-button = + .title = Ir a la primera página +pdfjs-first-page-button-label = Ir a la primera página +pdfjs-last-page-button = + .title = Ir a la última página +pdfjs-last-page-button-label = Ir a la última página +pdfjs-page-rotate-cw-button = + .title = Girar a la derecha +pdfjs-page-rotate-cw-button-label = Girar a la derecha +pdfjs-page-rotate-ccw-button = + .title = Girar a la izquierda +pdfjs-page-rotate-ccw-button-label = Girar a la izquierda +pdfjs-cursor-text-select-tool-button = + .title = Activar la herramienta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Herramienta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Activar la herramienta de mano +pdfjs-cursor-hand-tool-button-label = Herramienta de mano +pdfjs-scroll-page-button = + .title = Usar desplazamiento de página +pdfjs-scroll-page-button-label = Desplazamiento de página +pdfjs-scroll-vertical-button = + .title = Usar desplazamiento vertical +pdfjs-scroll-vertical-button-label = Desplazamiento vertical +pdfjs-scroll-horizontal-button = + .title = Usar desplazamiento horizontal +pdfjs-scroll-horizontal-button-label = Desplazamiento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar desplazamiento en bloque +pdfjs-scroll-wrapped-button-label = Desplazamiento en bloque +pdfjs-spread-none-button = + .title = No juntar páginas a modo de libro +pdfjs-spread-none-button-label = Vista de una página +pdfjs-spread-odd-button = + .title = Junta las páginas partiendo con una de número impar +pdfjs-spread-odd-button-label = Vista de libro impar +pdfjs-spread-even-button = + .title = Junta las páginas partiendo con una de número par +pdfjs-spread-even-button-label = Vista de libro par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades del documento… +pdfjs-document-properties-button-label = Propiedades del documento… +pdfjs-document-properties-file-name = Nombre de archivo: +pdfjs-document-properties-file-size = Tamaño del archivo: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Asunto: +pdfjs-document-properties-keywords = Palabras clave: +pdfjs-document-properties-creation-date = Fecha de creación: +pdfjs-document-properties-modification-date = Fecha de modificación: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creador: +pdfjs-document-properties-producer = Productor del PDF: +pdfjs-document-properties-version = Versión de PDF: +pdfjs-document-properties-page-count = Cantidad de páginas: +pdfjs-document-properties-page-size = Tamaño de la página: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Oficio + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista rápida en Web: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Cerrar + +## Print + +pdfjs-print-progress-message = Preparando documento para impresión… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Advertencia: Imprimir no está soportado completamente por este navegador. +pdfjs-printing-not-ready = Advertencia: El PDF no está completamente cargado para ser impreso. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Cambiar barra lateral (índice de contenidos del documento/adjuntos/capas) +pdfjs-toggle-sidebar-button-label = Mostrar u ocultar la barra lateral +pdfjs-document-outline-button = + .title = Mostrar esquema del documento (doble clic para expandir/contraer todos los elementos) +pdfjs-document-outline-button-label = Esquema del documento +pdfjs-attachments-button = + .title = Mostrar adjuntos +pdfjs-attachments-button-label = Adjuntos +pdfjs-layers-button = + .title = Mostrar capas (doble clic para restablecer todas las capas al estado predeterminado) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Buscar elemento de esquema actual +pdfjs-current-outline-item-button-label = Elemento de esquema actual +pdfjs-findbar-button = + .title = Buscar en el documento +pdfjs-findbar-button-label = Buscar +pdfjs-additional-layers = Capas adicionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de la página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Encontrar + .placeholder = Encontrar en el documento… +pdfjs-find-previous-button = + .title = Buscar la aparición anterior de la frase +pdfjs-find-previous-button-label = Previo +pdfjs-find-next-button = + .title = Buscar la siguiente aparición de la frase +pdfjs-find-next-button-label = Siguiente +pdfjs-find-highlight-checkbox = Destacar todos +pdfjs-find-match-case-checkbox-label = Coincidir mayús./minús. +pdfjs-find-match-diacritics-checkbox-label = Coincidir diacríticos +pdfjs-find-entire-word-checkbox-label = Palabras completas +pdfjs-find-reached-top = Se alcanzó el inicio del documento, continuando desde el final +pdfjs-find-reached-bottom = Se alcanzó el final del documento, continuando desde el inicio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Coincidencia { $current } de { $total } + *[other] Coincidencia { $current } de { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Más de { $limit } coincidencia + *[other] Más de { $limit } coincidencias + } +pdfjs-find-not-found = Frase no encontrada + +## Predefined zoom values + +pdfjs-page-scale-width = Ancho de página +pdfjs-page-scale-fit = Ajuste de página +pdfjs-page-scale-auto = Aumento automático +pdfjs-page-scale-actual = Tamaño actual +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ocurrió un error al cargar el PDF. +pdfjs-invalid-file-error = Archivo PDF inválido o corrupto. +pdfjs-missing-file-error = Falta el archivo PDF. +pdfjs-unexpected-response-error = Respuesta del servidor inesperada. +pdfjs-rendering-error = Ocurrió un error al renderizar la página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Anotación] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Ingresa la contraseña para abrir este archivo PDF. +pdfjs-password-invalid = Contraseña inválida. Por favor, vuelve a intentarlo. +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Las tipografías web están desactivadas: imposible usar las fuentes PDF embebidas. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Dibujar +pdfjs-editor-ink-button-label = Dibujar +pdfjs-editor-stamp-button = + .title = Añadir o editar imágenes +pdfjs-editor-stamp-button-label = Añadir o editar imágenes +pdfjs-editor-highlight-button = + .title = Destacar +pdfjs-editor-highlight-button-label = Destacar +pdfjs-highlight-floating-button1 = + .title = Destacar + .aria-label = Destacar +pdfjs-highlight-floating-button-label = Destacar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Eliminar dibujo +pdfjs-editor-remove-freetext-button = + .title = Eliminar texto +pdfjs-editor-remove-stamp-button = + .title = Eliminar imagen +pdfjs-editor-remove-highlight-button = + .title = Quitar resaltado + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Tamaño +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Grosor +pdfjs-editor-ink-opacity-input = Opacidad +pdfjs-editor-stamp-add-image-button = + .title = Añadir imagen +pdfjs-editor-stamp-add-image-button-label = Añadir imagen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grosor +pdfjs-editor-free-highlight-thickness-title = + .title = Cambia el grosor al resaltar elementos que no sean texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Empieza a escribir… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Empieza a escribir… +pdfjs-ink = + .aria-label = Editor de dibujos +pdfjs-ink-canvas = + .aria-label = Imagen creada por el usuario + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar texto alternativo +pdfjs-editor-alt-text-dialog-label = Elige una opción +pdfjs-editor-alt-text-dialog-description = El texto alternativo (alt text) ayuda cuando las personas no pueden ver la imagen o cuando no se carga. +pdfjs-editor-alt-text-add-description-label = Añade una descripción +pdfjs-editor-alt-text-add-description-description = Intenta escribir 1 o 2 oraciones que describan el tema, el ambiente o las acciones. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativa +pdfjs-editor-alt-text-mark-decorative-description = Se utiliza para imágenes ornamentales, como bordes o marcas de agua. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Guardar +pdfjs-editor-alt-text-decorative-tooltip = Marcada como decorativa +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por ejemplo: “Un joven se sienta a la mesa a comer” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Esquina superior izquierda — cambiar el tamaño +pdfjs-editor-resizer-label-top-middle = Borde superior en el medio — cambiar el tamaño +pdfjs-editor-resizer-label-top-right = Esquina superior derecha — cambiar el tamaño +pdfjs-editor-resizer-label-middle-right = Borde derecho en el medio — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-right = Esquina inferior derecha — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-middle = Borde inferior en el medio — cambiar el tamaño +pdfjs-editor-resizer-label-bottom-left = Esquina inferior izquierda — cambiar el tamaño +pdfjs-editor-resizer-label-middle-left = Borde izquierdo en el medio — cambiar el tamaño +pdfjs-editor-resizer-top-left = + .aria-label = Esquina superior izquierda — cambiar el tamaño +pdfjs-editor-resizer-top-middle = + .aria-label = Borde superior en el medio — cambiar el tamaño +pdfjs-editor-resizer-top-right = + .aria-label = Esquina superior derecha — cambiar el tamaño +pdfjs-editor-resizer-middle-right = + .aria-label = Borde derecho en el medio — cambiar el tamaño +pdfjs-editor-resizer-bottom-right = + .aria-label = Esquina inferior derecha — cambiar el tamaño +pdfjs-editor-resizer-bottom-middle = + .aria-label = Borde inferior en el medio — cambiar el tamaño +pdfjs-editor-resizer-bottom-left = + .aria-label = Esquina inferior izquierda — cambiar el tamaño +pdfjs-editor-resizer-middle-left = + .aria-label = Borde izquierdo en el medio — cambiar el tamaño + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de resaltado +pdfjs-editor-colorpicker-button = + .title = Cambiar color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opciones de color +pdfjs-editor-colorpicker-yellow = + .title = Amarillo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Rojo + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar todo +pdfjs-editor-highlight-show-all-button = + .title = Mostrar todo + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descripción de la imagen) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Añadir texto alternativo (descripción de la imagen) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escribe tu descripción aquí… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Breve descripción para las personas que no pueden ver la imagen o cuando la imagen no se carga. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo fue creado automáticamente y puede ser incorrecto. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Aprender más +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear texto alternativo automáticamente +pdfjs-editor-new-alt-text-not-now-button = Ahora no +pdfjs-editor-new-alt-text-error-title = No se pudo crear el texto alternativo automáticamente +pdfjs-editor-new-alt-text-error-description = Escribe tu propio texto alternativo o vuelve a intentarlo más tarde. +pdfjs-editor-new-alt-text-error-close-button = Cerrar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Se añadió el texto alternativo +pdfjs-editor-new-alt-text-added-button-label = Se añadió el texto alternativo +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Falta el texto alternativo +pdfjs-editor-new-alt-text-missing-button-label = Falta el texto alternativo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Revisar el texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Revisar el texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creado automáticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ajustes del texto alternativo de la imagen +pdfjs-image-alt-text-settings-button-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-dialog-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Crear texto alternativo automáticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugiere descripciones para ayudar a las personas que no pueden ver la imagen o cuando la imagen no se carga. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de IA de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Se ejecuta localmente en tu dispositivo para que tus datos permanezcan privados. Necesario para el texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Eliminar +pdfjs-editor-alt-text-settings-download-model-button = Descargar +pdfjs-editor-alt-text-settings-downloading-model-button = Bajando… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar el editor de texto alternativo inmediatamente al añadir una imagen +pdfjs-editor-alt-text-settings-show-dialog-description = Te ayuda a asegurarte de que todas tus imágenes tengan texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Cerrar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Resaltado eliminado +pdfjs-editor-undo-bar-message-freetext = Texto eliminado +pdfjs-editor-undo-bar-message-ink = Dibujo eliminado +pdfjs-editor-undo-bar-message-stamp = Imagen eliminada +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotación eliminada + *[other] { $count } anotaciones eliminadas + } +pdfjs-editor-undo-bar-undo-button = + .title = Deshacer +pdfjs-editor-undo-bar-undo-button-label = Deshacer +pdfjs-editor-undo-bar-close-button = + .title = Cerrar +pdfjs-editor-undo-bar-close-button-label = Cerrar diff --git a/public/assets/pdfjs/locale/es-ES/viewer.ftl b/public/assets/pdfjs/locale/es-ES/viewer.ftl new file mode 100644 index 0000000..f610a17 --- /dev/null +++ b/public/assets/pdfjs/locale/es-ES/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Página siguiente +pdfjs-next-button-label = Siguiente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Reducir +pdfjs-zoom-out-button-label = Reducir +pdfjs-zoom-in-button = + .title = Aumentar +pdfjs-zoom-in-button-label = Aumentar +pdfjs-zoom-select = + .title = Tamaño +pdfjs-presentation-mode-button = + .title = Cambiar al modo presentación +pdfjs-presentation-mode-button-label = Modo presentación +pdfjs-open-file-button = + .title = Abrir archivo +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Guardar +pdfjs-save-button-label = Guardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Descargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Descargar +pdfjs-bookmark-button = + .title = Página actual (Ver URL de la página actual) +pdfjs-bookmark-button-label = Página actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Herramientas +pdfjs-tools-button-label = Herramientas +pdfjs-first-page-button = + .title = Ir a la primera página +pdfjs-first-page-button-label = Ir a la primera página +pdfjs-last-page-button = + .title = Ir a la última página +pdfjs-last-page-button-label = Ir a la última página +pdfjs-page-rotate-cw-button = + .title = Rotar en sentido horario +pdfjs-page-rotate-cw-button-label = Rotar en sentido horario +pdfjs-page-rotate-ccw-button = + .title = Rotar en sentido antihorario +pdfjs-page-rotate-ccw-button-label = Rotar en sentido antihorario +pdfjs-cursor-text-select-tool-button = + .title = Activar herramienta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Herramienta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Activar herramienta de mano +pdfjs-cursor-hand-tool-button-label = Herramienta de mano +pdfjs-scroll-page-button = + .title = Usar desplazamiento de página +pdfjs-scroll-page-button-label = Desplazamiento de página +pdfjs-scroll-vertical-button = + .title = Usar desplazamiento vertical +pdfjs-scroll-vertical-button-label = Desplazamiento vertical +pdfjs-scroll-horizontal-button = + .title = Usar desplazamiento horizontal +pdfjs-scroll-horizontal-button-label = Desplazamiento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar desplazamiento en bloque +pdfjs-scroll-wrapped-button-label = Desplazamiento en bloque +pdfjs-spread-none-button = + .title = No juntar páginas en vista de libro +pdfjs-spread-none-button-label = Vista de libro +pdfjs-spread-odd-button = + .title = Juntar las páginas partiendo de una con número impar +pdfjs-spread-odd-button-label = Vista de libro impar +pdfjs-spread-even-button = + .title = Juntar las páginas partiendo de una con número par +pdfjs-spread-even-button-label = Vista de libro par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades del documento… +pdfjs-document-properties-button-label = Propiedades del documento… +pdfjs-document-properties-file-name = Nombre de archivo: +pdfjs-document-properties-file-size = Tamaño de archivo: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Asunto: +pdfjs-document-properties-keywords = Palabras clave: +pdfjs-document-properties-creation-date = Fecha de creación: +pdfjs-document-properties-modification-date = Fecha de modificación: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creador: +pdfjs-document-properties-producer = Productor PDF: +pdfjs-document-properties-version = Versión PDF: +pdfjs-document-properties-page-count = Número de páginas: +pdfjs-document-properties-page-size = Tamaño de la página: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista rápida de la web: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Cerrar + +## Print + +pdfjs-print-progress-message = Preparando documento para impresión… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Advertencia: Imprimir no está totalmente soportado por este navegador. +pdfjs-printing-not-ready = Advertencia: Este PDF no se ha cargado completamente para poder imprimirse. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Cambiar barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Alternar barra lateral (el documento contiene esquemas/adjuntos/capas) +pdfjs-toggle-sidebar-button-label = Cambiar barra lateral +pdfjs-document-outline-button = + .title = Mostrar resumen del documento (doble clic para expandir/contraer todos los elementos) +pdfjs-document-outline-button-label = Resumen de documento +pdfjs-attachments-button = + .title = Mostrar adjuntos +pdfjs-attachments-button-label = Adjuntos +pdfjs-layers-button = + .title = Mostrar capas (doble clic para restablecer todas las capas al estado predeterminado) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Encontrar elemento de esquema actual +pdfjs-current-outline-item-button-label = Elemento de esquema actual +pdfjs-findbar-button = + .title = Buscar en el documento +pdfjs-findbar-button-label = Buscar +pdfjs-additional-layers = Capas adicionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de la página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Buscar + .placeholder = Buscar en el documento… +pdfjs-find-previous-button = + .title = Encontrar la anterior aparición de la frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Encontrar la siguiente aparición de esta frase +pdfjs-find-next-button-label = Siguiente +pdfjs-find-highlight-checkbox = Resaltar todos +pdfjs-find-match-case-checkbox-label = Coincidencia de mayús./minús. +pdfjs-find-match-diacritics-checkbox-label = Coincidir diacríticos +pdfjs-find-entire-word-checkbox-label = Palabras completas +pdfjs-find-reached-top = Se alcanzó el inicio del documento, se continúa desde el final +pdfjs-find-reached-bottom = Se alcanzó el final del documento, se continúa desde el inicio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } coincidencia + *[other] { $current } de { $total } coincidencias + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Más de { $limit } coincidencia + *[other] Más de { $limit } coincidencias + } +pdfjs-find-not-found = Frase no encontrada + +## Predefined zoom values + +pdfjs-page-scale-width = Anchura de la página +pdfjs-page-scale-fit = Ajuste de la página +pdfjs-page-scale-auto = Tamaño automático +pdfjs-page-scale-actual = Tamaño real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ocurrió un error al cargar el PDF. +pdfjs-invalid-file-error = Fichero PDF no válido o corrupto. +pdfjs-missing-file-error = No hay fichero PDF. +pdfjs-unexpected-response-error = Respuesta inesperada del servidor. +pdfjs-rendering-error = Ocurrió un error al renderizar la página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotación { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Introduzca la contraseña para abrir este archivo PDF. +pdfjs-password-invalid = Contraseña no válida. Vuelva a intentarlo. +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Las tipografías web están desactivadas: es imposible usar las tipografías PDF embebidas. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Dibujar +pdfjs-editor-ink-button-label = Dibujar +pdfjs-editor-stamp-button = + .title = Añadir o editar imágenes +pdfjs-editor-stamp-button-label = Añadir o editar imágenes +pdfjs-editor-highlight-button = + .title = Resaltar +pdfjs-editor-highlight-button-label = Resaltar +pdfjs-highlight-floating-button1 = + .title = Resaltar + .aria-label = Resaltar +pdfjs-highlight-floating-button-label = Resaltar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Eliminar dibujo +pdfjs-editor-remove-freetext-button = + .title = Eliminar texto +pdfjs-editor-remove-stamp-button = + .title = Eliminar imagen +pdfjs-editor-remove-highlight-button = + .title = Quitar resaltado + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Tamaño +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Grosor +pdfjs-editor-ink-opacity-input = Opacidad +pdfjs-editor-stamp-add-image-button = + .title = Añadir imagen +pdfjs-editor-stamp-add-image-button-label = Añadir imagen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grosor +pdfjs-editor-free-highlight-thickness-title = + .title = Cambiar el grosor al resaltar elementos que no sean texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Empiece a escribir… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Empezar a escribir… +pdfjs-ink = + .aria-label = Editor de dibujos +pdfjs-ink-canvas = + .aria-label = Imagen creada por el usuario + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar el texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar el texto alternativo +pdfjs-editor-alt-text-dialog-label = Eligir una opción +pdfjs-editor-alt-text-dialog-description = El texto alternativo (texto alternativo) ayuda cuando las personas no pueden ver la imagen o cuando no se carga. +pdfjs-editor-alt-text-add-description-label = Añadir una descripción +pdfjs-editor-alt-text-add-description-description = Intente escribir 1 o 2 frases que describan el tema, el entorno o las acciones. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativa +pdfjs-editor-alt-text-mark-decorative-description = Se utiliza para imágenes ornamentales, como bordes o marcas de agua. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Guardar +pdfjs-editor-alt-text-decorative-tooltip = Marcada como decorativa +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por ejemplo: “Un joven se sienta a la mesa a comer” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Esquina superior izquierda — redimensionar +pdfjs-editor-resizer-label-top-middle = Borde superior en el medio — redimensionar +pdfjs-editor-resizer-label-top-right = Esquina superior derecha — redimensionar +pdfjs-editor-resizer-label-middle-right = Borde derecho en el medio — redimensionar +pdfjs-editor-resizer-label-bottom-right = Esquina inferior derecha — redimensionar +pdfjs-editor-resizer-label-bottom-middle = Borde inferior en el medio — redimensionar +pdfjs-editor-resizer-label-bottom-left = Esquina inferior izquierda — redimensionar +pdfjs-editor-resizer-label-middle-left = Borde izquierdo en el medio — redimensionar +pdfjs-editor-resizer-top-left = + .aria-label = Esquina superior izquierda — redimensionar +pdfjs-editor-resizer-top-middle = + .aria-label = Borde superior en el medio — redimensionar +pdfjs-editor-resizer-top-right = + .aria-label = Esquina superior derecha — redimensionar +pdfjs-editor-resizer-middle-right = + .aria-label = Borde derecho en el medio — redimensionar +pdfjs-editor-resizer-bottom-right = + .aria-label = Esquina inferior derecha — redimensionar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Borde inferior en el medio — redimensionar +pdfjs-editor-resizer-bottom-left = + .aria-label = Esquina inferior izquierda — redimensionar +pdfjs-editor-resizer-middle-left = + .aria-label = Borde izquierdo en el medio — redimensionar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de resaltado +pdfjs-editor-colorpicker-button = + .title = Cambiar color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opciones de color +pdfjs-editor-colorpicker-yellow = + .title = Amarillo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Rojo + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar todo +pdfjs-editor-highlight-show-all-button = + .title = Mostrar todo + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descripción de la imagen) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Añadir texto alternativo (descripción de la imagen) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escribir la descripción aquí… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Breve descripción para las personas que no pueden ver la imagen o cuando la imagen no se carga. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo fue creado automáticamente y puede ser inexacto. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Saber más +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear texto alternativo automáticamente +pdfjs-editor-new-alt-text-not-now-button = Ahora no +pdfjs-editor-new-alt-text-error-title = No se ha podido crear el texto alternativo automáticamente +pdfjs-editor-new-alt-text-error-description = Escriba su propio texto alternativo o inténtelo de nuevo más tarde. +pdfjs-editor-new-alt-text-error-close-button = Cerrar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Se añadió el texto alternativo +pdfjs-editor-new-alt-text-added-button-label = Se añadió el texto alternativo +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Falta el texto alternativo +pdfjs-editor-new-alt-text-missing-button-label = Falta el texto alternativo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Revisar el texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Revisar el texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creado automáticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ajustes del texto alternativo de la imagen +pdfjs-image-alt-text-settings-button-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-dialog-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Crear texto alternativo automáticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugiere descripciones para ayudar a las personas que no pueden ver la imagen o cuando la imagen no se carga. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de IA de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Se ejecuta localmente en el dispositivo para que los datos se mantengan privados. Requerido para texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Eliminar +pdfjs-editor-alt-text-settings-download-model-button = Descargar +pdfjs-editor-alt-text-settings-downloading-model-button = Descargando… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar el editor de texto alternativo inmediatamente al añadir una imagen +pdfjs-editor-alt-text-settings-show-dialog-description = Le ayuda a asegurarse de que todas sus imágenes tengan texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Cerrar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Resaltado eliminado +pdfjs-editor-undo-bar-message-freetext = Texto eliminado +pdfjs-editor-undo-bar-message-ink = Dibujo eliminado +pdfjs-editor-undo-bar-message-stamp = Imagen eliminada +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotación eliminada + *[other] { $count } anotaciones eliminadas + } +pdfjs-editor-undo-bar-undo-button = + .title = Deshacer +pdfjs-editor-undo-bar-undo-button-label = Deshacer +pdfjs-editor-undo-bar-close-button = + .title = Cerrar +pdfjs-editor-undo-bar-close-button-label = Cerrar diff --git a/public/assets/pdfjs/locale/es-MX/viewer.ftl b/public/assets/pdfjs/locale/es-MX/viewer.ftl new file mode 100644 index 0000000..03afe7c --- /dev/null +++ b/public/assets/pdfjs/locale/es-MX/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Página siguiente +pdfjs-next-button-label = Siguiente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Reducir +pdfjs-zoom-out-button-label = Reducir +pdfjs-zoom-in-button = + .title = Aumentar +pdfjs-zoom-in-button-label = Aumentar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Cambiar al modo presentación +pdfjs-presentation-mode-button-label = Modo presentación +pdfjs-open-file-button = + .title = Abrir archivo +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Guardar +pdfjs-save-button-label = Guardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Descargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Descargar +pdfjs-bookmark-button = + .title = Página actual (Ver URL de la página actual) +pdfjs-bookmark-button-label = Página actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Herramientas +pdfjs-tools-button-label = Herramientas +pdfjs-first-page-button = + .title = Ir a la primera página +pdfjs-first-page-button-label = Ir a la primera página +pdfjs-last-page-button = + .title = Ir a la última página +pdfjs-last-page-button-label = Ir a la última página +pdfjs-page-rotate-cw-button = + .title = Girar a la derecha +pdfjs-page-rotate-cw-button-label = Girar a la derecha +pdfjs-page-rotate-ccw-button = + .title = Girar a la izquierda +pdfjs-page-rotate-ccw-button-label = Girar a la izquierda +pdfjs-cursor-text-select-tool-button = + .title = Activar la herramienta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Herramienta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Activar la herramienta de mano +pdfjs-cursor-hand-tool-button-label = Herramienta de mano +pdfjs-scroll-page-button = + .title = Usar desplazamiento de página +pdfjs-scroll-page-button-label = Desplazamiento de página +pdfjs-scroll-vertical-button = + .title = Usar desplazamiento vertical +pdfjs-scroll-vertical-button-label = Desplazamiento vertical +pdfjs-scroll-horizontal-button = + .title = Usar desplazamiento horizontal +pdfjs-scroll-horizontal-button-label = Desplazamiento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar desplazamiento encapsulado +pdfjs-scroll-wrapped-button-label = Desplazamiento encapsulado +pdfjs-spread-none-button = + .title = No unir páginas separadas +pdfjs-spread-none-button-label = Vista de una página +pdfjs-spread-odd-button = + .title = Unir las páginas partiendo con una de número impar +pdfjs-spread-odd-button-label = Vista de libro impar +pdfjs-spread-even-button = + .title = Juntar las páginas partiendo con una de número par +pdfjs-spread-even-button-label = Vista de libro par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades del documento… +pdfjs-document-properties-button-label = Propiedades del documento… +pdfjs-document-properties-file-name = Nombre del archivo: +pdfjs-document-properties-file-size = Tamaño del archivo: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Asunto: +pdfjs-document-properties-keywords = Palabras claves: +pdfjs-document-properties-creation-date = Fecha de creación: +pdfjs-document-properties-modification-date = Fecha de modificación: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creador: +pdfjs-document-properties-producer = Productor PDF: +pdfjs-document-properties-version = Versión PDF: +pdfjs-document-properties-page-count = Número de páginas: +pdfjs-document-properties-page-size = Tamaño de la página: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Oficio + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista rápida de la web: +pdfjs-document-properties-linearized-yes = Sí +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Cerrar + +## Print + +pdfjs-print-progress-message = Preparando documento para impresión… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Advertencia: La impresión no esta completamente soportada por este navegador. +pdfjs-printing-not-ready = Advertencia: El PDF no cargo completamente para impresión. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Cambiar barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Alternar barra lateral (el documento contiene esquemas/adjuntos/capas) +pdfjs-toggle-sidebar-button-label = Cambiar barra lateral +pdfjs-document-outline-button = + .title = Mostrar esquema del documento (doble clic para expandir/contraer todos los elementos) +pdfjs-document-outline-button-label = Esquema del documento +pdfjs-attachments-button = + .title = Mostrar adjuntos +pdfjs-attachments-button-label = Adjuntos +pdfjs-layers-button = + .title = Mostrar capas (doble clic para restablecer todas las capas al estado predeterminado) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Buscar elemento de esquema actual +pdfjs-current-outline-item-button-label = Elemento de esquema actual +pdfjs-findbar-button = + .title = Buscar en el documento +pdfjs-findbar-button-label = Buscar +pdfjs-additional-layers = Capas adicionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de la página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Buscar + .placeholder = Buscar en el documento… +pdfjs-find-previous-button = + .title = Ir a la anterior frase encontrada +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Ir a la siguiente frase encontrada +pdfjs-find-next-button-label = Siguiente +pdfjs-find-highlight-checkbox = Resaltar todo +pdfjs-find-match-case-checkbox-label = Coincidir con mayúsculas y minúsculas +pdfjs-find-match-diacritics-checkbox-label = Coincidir diacríticos +pdfjs-find-entire-word-checkbox-label = Palabras completas +pdfjs-find-reached-top = Se alcanzó el inicio del documento, se buscará al final +pdfjs-find-reached-bottom = Se alcanzó el final del documento, se buscará al inicio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } coincidencia + *[other] { $current } de { $total } coincidencias + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Más de { $limit } coincidencia + *[other] Más de { $limit } coincidencias + } +pdfjs-find-not-found = No se encontró la frase + +## Predefined zoom values + +pdfjs-page-scale-width = Ancho de página +pdfjs-page-scale-fit = Ajustar página +pdfjs-page-scale-auto = Zoom automático +pdfjs-page-scale-actual = Tamaño real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Un error ocurrió al cargar el PDF. +pdfjs-invalid-file-error = Archivo PDF invalido o dañado. +pdfjs-missing-file-error = Archivo PDF no encontrado. +pdfjs-unexpected-response-error = Respuesta inesperada del servidor. +pdfjs-rendering-error = Un error ocurrió al renderizar la página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } anotación] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Ingresa la contraseña para abrir este archivo PDF. +pdfjs-password-invalid = Contraseña inválida. Por favor intenta de nuevo. +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Las fuentes web están desactivadas: es imposible usar las fuentes PDF embebidas. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Dibujar +pdfjs-editor-ink-button-label = Dibujar +pdfjs-editor-stamp-button = + .title = Agregar o editar imágenes +pdfjs-editor-stamp-button-label = Agregar o editar imágenes +pdfjs-editor-highlight-button = + .title = Destacar +pdfjs-editor-highlight-button-label = Destacar +pdfjs-highlight-floating-button1 = + .title = Destacados + .aria-label = Destacados +pdfjs-highlight-floating-button-label = Destacados + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Eliminar dibujo +pdfjs-editor-remove-freetext-button = + .title = Eliminar texto +pdfjs-editor-remove-stamp-button = + .title = Eliminar imagen +pdfjs-editor-remove-highlight-button = + .title = Eliminar destacado + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Tamaño +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Grossor +pdfjs-editor-ink-opacity-input = Opacidad +pdfjs-editor-stamp-add-image-button = + .title = Agregar imagen +pdfjs-editor-stamp-add-image-button-label = Agregar imagen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Espesor +pdfjs-editor-free-highlight-thickness-title = + .title = Cambiar el grosor al resaltar elementos que no sean texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Comenzar a escribir… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Empieza a escribir… +pdfjs-ink = + .aria-label = Editor de dibujo +pdfjs-ink-canvas = + .aria-label = Imagen creada por el usuario + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar texto alternativo +pdfjs-editor-alt-text-dialog-label = Elige una opción +pdfjs-editor-alt-text-dialog-description = El texto alternativo (texto alternativo) ayuda cuando las personas no pueden ver la imagen o cuando no se carga. +pdfjs-editor-alt-text-add-description-label = Añadir una descripción +pdfjs-editor-alt-text-add-description-description = Intente escribir 1 o 2 oraciones que describan el tema, el entorno o las acciones. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativo +pdfjs-editor-alt-text-mark-decorative-description = Se utiliza para imágenes ornamentales, como bordes o marcas de agua. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Guardar +pdfjs-editor-alt-text-decorative-tooltip = Marcado como decorativo +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por ejemplo: “Un joven se sienta a la mesa a comer” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Esquina superior izquierda: cambiar el tamaño +pdfjs-editor-resizer-label-top-middle = Arriba en el medio: cambiar el tamaño +pdfjs-editor-resizer-label-top-right = Esquina superior derecha: cambiar el tamaño +pdfjs-editor-resizer-label-middle-right = Centro derecha: cambiar el tamaño +pdfjs-editor-resizer-label-bottom-right = Esquina inferior derecha: cambiar el tamaño +pdfjs-editor-resizer-label-bottom-middle = Abajo en el medio: cambiar el tamaño +pdfjs-editor-resizer-label-bottom-left = Esquina inferior izquierda: cambiar el tamaño +pdfjs-editor-resizer-label-middle-left = Centro izquierda: cambiar el tamaño +pdfjs-editor-resizer-top-left = + .aria-label = Esquina superior izquierda — redimensionar +pdfjs-editor-resizer-top-middle = + .aria-label = Borde superior en el medio — redimensionar +pdfjs-editor-resizer-top-right = + .aria-label = Esquina superior derecha — redimensionar +pdfjs-editor-resizer-middle-right = + .aria-label = Borde derecho en el medio — redimensionar +pdfjs-editor-resizer-bottom-right = + .aria-label = Esquina inferior derecha — redimensionar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Borde inferior en el medio — redimensionar +pdfjs-editor-resizer-bottom-left = + .aria-label = Esquina inferior izquierda — redimensionar +pdfjs-editor-resizer-middle-left = + .aria-label = Borde izquierdo en el medio — redimensionar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de resaltado +pdfjs-editor-colorpicker-button = + .title = Cambiar color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opciones de color +pdfjs-editor-colorpicker-yellow = + .title = Amarillo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Rojo + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar todo +pdfjs-editor-highlight-show-all-button = + .title = Mostrar todo + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descripción de la imagen) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Agregar texto alternativo (descripción de la imagen) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escribe tu descripción aquí… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Breve descripción para las personas que no pueden ver la imagen o cuando la imagen no se carga. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo fue creado automáticamente y puede ser inexacto. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Saber más +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear texto alternativo automáticamente +pdfjs-editor-new-alt-text-not-now-button = Ahora no +pdfjs-editor-new-alt-text-error-title = No se pudo crear el texto alternativo automáticamente +pdfjs-editor-new-alt-text-error-description = Escribe tu propio texto alternativo o inténtalo de nuevo más tarde. +pdfjs-editor-new-alt-text-error-close-button = Cerrar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Descargando el modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Se agregó el texto alternativo +pdfjs-editor-new-alt-text-added-button-label = Se agregó el texto alternativo +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Falta el texto alternativo +pdfjs-editor-new-alt-text-missing-button-label = Falta texto alternativo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Revisar el texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Revisar el texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creado automáticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ajustes del texto alternativo de la imagen +pdfjs-image-alt-text-settings-button-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-dialog-label = Ajustes del texto alternativo de la imagen +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Crear texto alternativo automáticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugiere descripciones para ayudar a las personas que no pueden ver la imagen o cuando la imagen no se carga. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de IA de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Se ejecuta localmente en el dispositivo para que los datos se mantengan privados. Requerido para texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Eliminar +pdfjs-editor-alt-text-settings-download-model-button = Descargar +pdfjs-editor-alt-text-settings-downloading-model-button = Descargando… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar el editor de texto alternativo inmediatamente al añadir una imagen +pdfjs-editor-alt-text-settings-show-dialog-description = Te ayuda a asegurarte de que todas tus imágenes tengan texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Cerrar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Resaltado eliminado +pdfjs-editor-undo-bar-message-freetext = Texto eliminado +pdfjs-editor-undo-bar-message-ink = Dibujo eliminado +pdfjs-editor-undo-bar-message-stamp = Imagen eliminada +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotación eliminada + *[other] { $count } anotaciones eliminadas + } +pdfjs-editor-undo-bar-undo-button = + .title = Deshacer +pdfjs-editor-undo-bar-undo-button-label = Deshacer +pdfjs-editor-undo-bar-close-button = + .title = Cerrar +pdfjs-editor-undo-bar-close-button-label = Cerrar diff --git a/public/assets/pdfjs/locale/et/viewer.ftl b/public/assets/pdfjs/locale/et/viewer.ftl new file mode 100644 index 0000000..b28c6d5 --- /dev/null +++ b/public/assets/pdfjs/locale/et/viewer.ftl @@ -0,0 +1,268 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Eelmine lehekülg +pdfjs-previous-button-label = Eelmine +pdfjs-next-button = + .title = Järgmine lehekülg +pdfjs-next-button-label = Järgmine +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Leht +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber }/{ $pagesCount }) +pdfjs-zoom-out-button = + .title = Vähenda +pdfjs-zoom-out-button-label = Vähenda +pdfjs-zoom-in-button = + .title = Suurenda +pdfjs-zoom-in-button-label = Suurenda +pdfjs-zoom-select = + .title = Suurendamine +pdfjs-presentation-mode-button = + .title = Lülitu esitlusrežiimi +pdfjs-presentation-mode-button-label = Esitlusrežiim +pdfjs-open-file-button = + .title = Ava fail +pdfjs-open-file-button-label = Ava +pdfjs-print-button = + .title = Prindi +pdfjs-print-button-label = Prindi + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tööriistad +pdfjs-tools-button-label = Tööriistad +pdfjs-first-page-button = + .title = Mine esimesele leheküljele +pdfjs-first-page-button-label = Mine esimesele leheküljele +pdfjs-last-page-button = + .title = Mine viimasele leheküljele +pdfjs-last-page-button-label = Mine viimasele leheküljele +pdfjs-page-rotate-cw-button = + .title = Pööra päripäeva +pdfjs-page-rotate-cw-button-label = Pööra päripäeva +pdfjs-page-rotate-ccw-button = + .title = Pööra vastupäeva +pdfjs-page-rotate-ccw-button-label = Pööra vastupäeva +pdfjs-cursor-text-select-tool-button = + .title = Luba teksti valimise tööriist +pdfjs-cursor-text-select-tool-button-label = Teksti valimise tööriist +pdfjs-cursor-hand-tool-button = + .title = Luba sirvimistööriist +pdfjs-cursor-hand-tool-button-label = Sirvimistööriist +pdfjs-scroll-page-button = + .title = Kasutatakse lehe kaupa kerimist +pdfjs-scroll-page-button-label = Lehe kaupa kerimine +pdfjs-scroll-vertical-button = + .title = Kasuta vertikaalset kerimist +pdfjs-scroll-vertical-button-label = Vertikaalne kerimine +pdfjs-scroll-horizontal-button = + .title = Kasuta horisontaalset kerimist +pdfjs-scroll-horizontal-button-label = Horisontaalne kerimine +pdfjs-scroll-wrapped-button = + .title = Kasuta rohkem mahutavat kerimist +pdfjs-scroll-wrapped-button-label = Rohkem mahutav kerimine +pdfjs-spread-none-button = + .title = Ära kõrvuta lehekülgi +pdfjs-spread-none-button-label = Lehtede kõrvutamine puudub +pdfjs-spread-odd-button = + .title = Kõrvuta leheküljed, alustades paaritute numbritega lehekülgedega +pdfjs-spread-odd-button-label = Kõrvutamine paaritute numbritega alustades +pdfjs-spread-even-button = + .title = Kõrvuta leheküljed, alustades paarisnumbritega lehekülgedega +pdfjs-spread-even-button-label = Kõrvutamine paarisnumbritega alustades + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumendi omadused… +pdfjs-document-properties-button-label = Dokumendi omadused… +pdfjs-document-properties-file-name = Faili nimi: +pdfjs-document-properties-file-size = Faili suurus: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KiB ({ $size_b } baiti) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MiB ({ $size_b } baiti) +pdfjs-document-properties-title = Pealkiri: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Teema: +pdfjs-document-properties-keywords = Märksõnad: +pdfjs-document-properties-creation-date = Loodud: +pdfjs-document-properties-modification-date = Muudetud: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } { $time } +pdfjs-document-properties-creator = Looja: +pdfjs-document-properties-producer = Generaator: +pdfjs-document-properties-version = Generaatori versioon: +pdfjs-document-properties-page-count = Lehekülgi: +pdfjs-document-properties-page-size = Lehe suurus: +pdfjs-document-properties-page-size-unit-inches = tolli +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertikaalpaigutus +pdfjs-document-properties-page-size-orientation-landscape = rõhtpaigutus +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = "Fast Web View" tugi: +pdfjs-document-properties-linearized-yes = Jah +pdfjs-document-properties-linearized-no = Ei +pdfjs-document-properties-close-button = Sulge + +## Print + +pdfjs-print-progress-message = Dokumendi ettevalmistamine printimiseks… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Loobu +pdfjs-printing-not-supported = Hoiatus: printimine pole selle brauseri poolt täielikult toetatud. +pdfjs-printing-not-ready = Hoiatus: PDF pole printimiseks täielikult laaditud. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Näita külgriba +pdfjs-toggle-sidebar-notification-button = + .title = Näita külgriba (dokument sisaldab sisukorda/manuseid/kihte) +pdfjs-toggle-sidebar-button-label = Näita külgriba +pdfjs-document-outline-button = + .title = Näita sisukorda (kõigi punktide laiendamiseks/ahendamiseks topeltklõpsa) +pdfjs-document-outline-button-label = Näita sisukorda +pdfjs-attachments-button = + .title = Näita manuseid +pdfjs-attachments-button-label = Manused +pdfjs-layers-button = + .title = Näita kihte (kõikide kihtide vaikeolekusse lähtestamiseks topeltklõpsa) +pdfjs-layers-button-label = Kihid +pdfjs-thumbs-button = + .title = Näita pisipilte +pdfjs-thumbs-button-label = Pisipildid +pdfjs-current-outline-item-button = + .title = Otsi üles praegune kontuuriüksus +pdfjs-current-outline-item-button-label = Praegune kontuuriüksus +pdfjs-findbar-button = + .title = Otsi dokumendist +pdfjs-findbar-button-label = Otsi +pdfjs-additional-layers = Täiendavad kihid + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page }. lehekülg +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page }. lehekülje pisipilt + +## Find panel button title and messages + +pdfjs-find-input = + .title = Otsi + .placeholder = Otsi dokumendist… +pdfjs-find-previous-button = + .title = Otsi fraasi eelmine esinemiskoht +pdfjs-find-previous-button-label = Eelmine +pdfjs-find-next-button = + .title = Otsi fraasi järgmine esinemiskoht +pdfjs-find-next-button-label = Järgmine +pdfjs-find-highlight-checkbox = Too kõik esile +pdfjs-find-match-case-checkbox-label = Tõstutundlik +pdfjs-find-match-diacritics-checkbox-label = Otsitakse diakriitiliselt +pdfjs-find-entire-word-checkbox-label = Täissõnad +pdfjs-find-reached-top = Jõuti dokumendi algusesse, jätkati lõpust +pdfjs-find-reached-bottom = Jõuti dokumendi lõppu, jätkati algusest +pdfjs-find-not-found = Fraasi ei leitud + +## Predefined zoom values + +pdfjs-page-scale-width = Mahuta laiusele +pdfjs-page-scale-fit = Mahuta leheküljele +pdfjs-page-scale-auto = Automaatne suurendamine +pdfjs-page-scale-actual = Tegelik suurus +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Lehekülg { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDFi laadimisel esines viga. +pdfjs-invalid-file-error = Vigane või rikutud PDF-fail. +pdfjs-missing-file-error = PDF-fail puudub. +pdfjs-unexpected-response-error = Ootamatu vastus serverilt. +pdfjs-rendering-error = Lehe renderdamisel esines viga. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = PDF-faili avamiseks sisesta parool. +pdfjs-password-invalid = Vigane parool. Palun proovi uuesti. +pdfjs-password-ok-button = Sobib +pdfjs-password-cancel-button = Loobu +pdfjs-web-fonts-disabled = Veebifondid on keelatud: PDFiga kaasatud fonte pole võimalik kasutada. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/eu/viewer.ftl b/public/assets/pdfjs/locale/eu/viewer.ftl new file mode 100644 index 0000000..2b2660b --- /dev/null +++ b/public/assets/pdfjs/locale/eu/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Aurreko orria +pdfjs-previous-button-label = Aurrekoa +pdfjs-next-button = + .title = Hurrengo orria +pdfjs-next-button-label = Hurrengoa +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Orria +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = { $pagesCount }/{ $pageNumber } +pdfjs-zoom-out-button = + .title = Urrundu zooma +pdfjs-zoom-out-button-label = Urrundu zooma +pdfjs-zoom-in-button = + .title = Gerturatu zooma +pdfjs-zoom-in-button-label = Gerturatu zooma +pdfjs-zoom-select = + .title = Zooma +pdfjs-presentation-mode-button = + .title = Aldatu aurkezpen modura +pdfjs-presentation-mode-button-label = Arkezpen modua +pdfjs-open-file-button = + .title = Ireki fitxategia +pdfjs-open-file-button-label = Ireki +pdfjs-print-button = + .title = Inprimatu +pdfjs-print-button-label = Inprimatu +pdfjs-save-button = + .title = Gorde +pdfjs-save-button-label = Gorde +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Deskargatu +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Deskargatu +pdfjs-bookmark-button = + .title = Uneko orria (ikusi uneko orriaren URLa) +pdfjs-bookmark-button-label = Uneko orria + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tresnak +pdfjs-tools-button-label = Tresnak +pdfjs-first-page-button = + .title = Joan lehen orrira +pdfjs-first-page-button-label = Joan lehen orrira +pdfjs-last-page-button = + .title = Joan azken orrira +pdfjs-last-page-button-label = Joan azken orrira +pdfjs-page-rotate-cw-button = + .title = Biratu erlojuaren norantzan +pdfjs-page-rotate-cw-button-label = Biratu erlojuaren norantzan +pdfjs-page-rotate-ccw-button = + .title = Biratu erlojuaren aurkako norantzan +pdfjs-page-rotate-ccw-button-label = Biratu erlojuaren aurkako norantzan +pdfjs-cursor-text-select-tool-button = + .title = Gaitu testuaren hautapen tresna +pdfjs-cursor-text-select-tool-button-label = Testuaren hautapen tresna +pdfjs-cursor-hand-tool-button = + .title = Gaitu eskuaren tresna +pdfjs-cursor-hand-tool-button-label = Eskuaren tresna +pdfjs-scroll-page-button = + .title = Erabili orriaren korritzea +pdfjs-scroll-page-button-label = Orriaren korritzea +pdfjs-scroll-vertical-button = + .title = Erabili korritze bertikala +pdfjs-scroll-vertical-button-label = Korritze bertikala +pdfjs-scroll-horizontal-button = + .title = Erabili korritze horizontala +pdfjs-scroll-horizontal-button-label = Korritze horizontala +pdfjs-scroll-wrapped-button = + .title = Erabili korritze egokitua +pdfjs-scroll-wrapped-button-label = Korritze egokitua +pdfjs-spread-none-button = + .title = Ez elkartu barreiatutako orriak +pdfjs-spread-none-button-label = Barreiatzerik ez +pdfjs-spread-odd-button = + .title = Elkartu barreiatutako orriak bakoiti zenbakidunekin hasita +pdfjs-spread-odd-button-label = Barreiatze bakoitia +pdfjs-spread-even-button = + .title = Elkartu barreiatutako orriak bikoiti zenbakidunekin hasita +pdfjs-spread-even-button-label = Barreiatze bikoitia + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentuaren propietateak… +pdfjs-document-properties-button-label = Dokumentuaren propietateak… +pdfjs-document-properties-file-name = Fitxategi-izena: +pdfjs-document-properties-file-size = Fitxategiaren tamaina: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Izenburua: +pdfjs-document-properties-author = Egilea: +pdfjs-document-properties-subject = Gaia: +pdfjs-document-properties-keywords = Gako-hitzak: +pdfjs-document-properties-creation-date = Sortze-data: +pdfjs-document-properties-modification-date = Aldatze-data: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Sortzailea: +pdfjs-document-properties-producer = PDFaren ekoizlea: +pdfjs-document-properties-version = PDF bertsioa: +pdfjs-document-properties-page-count = Orrialde kopurua: +pdfjs-document-properties-page-size = Orriaren tamaina: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = bertikala +pdfjs-document-properties-page-size-orientation-landscape = horizontala +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Gutuna +pdfjs-document-properties-page-size-name-legal = Legala + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Webeko ikuspegi bizkorra: +pdfjs-document-properties-linearized-yes = Bai +pdfjs-document-properties-linearized-no = Ez +pdfjs-document-properties-close-button = Itxi + +## Print + +pdfjs-print-progress-message = Dokumentua inprimatzeko prestatzen… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = %{ $progress } +pdfjs-print-progress-close-button = Utzi +pdfjs-printing-not-supported = Abisua: inprimatzeko euskarria ez da erabatekoa nabigatzaile honetan. +pdfjs-printing-not-ready = Abisua: PDFa ez dago erabat kargatuta inprimatzeko. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Txandakatu alboko barra +pdfjs-toggle-sidebar-notification-button = + .title = Txandakatu alboko barra (dokumentuak eskema/eranskinak/geruzak ditu) +pdfjs-toggle-sidebar-button-label = Txandakatu alboko barra +pdfjs-document-outline-button = + .title = Erakutsi dokumentuaren eskema (klik bikoitza elementu guztiak zabaltzeko/tolesteko) +pdfjs-document-outline-button-label = Dokumentuaren eskema +pdfjs-attachments-button = + .title = Erakutsi eranskinak +pdfjs-attachments-button-label = Eranskinak +pdfjs-layers-button = + .title = Erakutsi geruzak (klik bikoitza geruza guztiak egoera lehenetsira berrezartzeko) +pdfjs-layers-button-label = Geruzak +pdfjs-thumbs-button = + .title = Erakutsi koadro txikiak +pdfjs-thumbs-button-label = Koadro txikiak +pdfjs-current-outline-item-button = + .title = Bilatu uneko eskemaren elementua +pdfjs-current-outline-item-button-label = Uneko eskemaren elementua +pdfjs-findbar-button = + .title = Bilatu dokumentuan +pdfjs-findbar-button-label = Bilatu +pdfjs-additional-layers = Geruza gehigarriak + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page }. orria +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page }. orriaren koadro txikia + +## Find panel button title and messages + +pdfjs-find-input = + .title = Bilatu + .placeholder = Bilatu dokumentuan… +pdfjs-find-previous-button = + .title = Bilatu esaldiaren aurreko parekatzea +pdfjs-find-previous-button-label = Aurrekoa +pdfjs-find-next-button = + .title = Bilatu esaldiaren hurrengo parekatzea +pdfjs-find-next-button-label = Hurrengoa +pdfjs-find-highlight-checkbox = Nabarmendu guztia +pdfjs-find-match-case-checkbox-label = Bat etorri maiuskulekin/minuskulekin +pdfjs-find-match-diacritics-checkbox-label = Bereizi diakritikoak +pdfjs-find-entire-word-checkbox-label = Hitz osoak +pdfjs-find-reached-top = Dokumentuaren hasierara heldu da, bukaeratik jarraitzen +pdfjs-find-reached-bottom = Dokumentuaren bukaerara heldu da, hasieratik jarraitzen +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $total }/{ $current }. bat-etortzea + *[other] { $total }/{ $current }. bat-etortzea + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Bat datorren { $limit } baino gehiago + *[other] Bat datozen { $limit } baino gehiago + } +pdfjs-find-not-found = Esaldia ez da aurkitu + +## Predefined zoom values + +pdfjs-page-scale-width = Orriaren zabalera +pdfjs-page-scale-fit = Doitu orrira +pdfjs-page-scale-auto = Zoom automatikoa +pdfjs-page-scale-actual = Benetako tamaina +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = %{ $scale } + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page }. orria + +## Loading indicator messages + +pdfjs-loading-error = Errorea gertatu da PDFa kargatzean. +pdfjs-invalid-file-error = PDF fitxategi baliogabe edo hondatua. +pdfjs-missing-file-error = PDF fitxategia falta da. +pdfjs-unexpected-response-error = Espero gabeko zerbitzariaren erantzuna. +pdfjs-rendering-error = Errorea gertatu da orria errendatzean. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ohartarazpena] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Idatzi PDF fitxategi hau irekitzeko pasahitza. +pdfjs-password-invalid = Pasahitz baliogabea. Saiatu berriro mesedez. +pdfjs-password-ok-button = Ados +pdfjs-password-cancel-button = Utzi +pdfjs-web-fonts-disabled = Webeko letra-tipoak desgaituta daude: ezin dira kapsulatutako PDF letra-tipoak erabili. + +## Editing + +pdfjs-editor-free-text-button = + .title = Testua +pdfjs-editor-free-text-button-label = Testua +pdfjs-editor-ink-button = + .title = Marrazkia +pdfjs-editor-ink-button-label = Marrazkia +pdfjs-editor-stamp-button = + .title = Gehitu edo editatu irudiak +pdfjs-editor-stamp-button-label = Gehitu edo editatu irudiak +pdfjs-editor-highlight-button = + .title = Nabarmendu +pdfjs-editor-highlight-button-label = Nabarmendu +pdfjs-highlight-floating-button1 = + .title = Nabarmendu + .aria-label = Nabarmendu +pdfjs-highlight-floating-button-label = Nabarmendu + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Kendu marrazkia +pdfjs-editor-remove-freetext-button = + .title = Kendu testua +pdfjs-editor-remove-stamp-button = + .title = Kendu irudia +pdfjs-editor-remove-highlight-button = + .title = Kendu nabarmentzea + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Kolorea +pdfjs-editor-free-text-size-input = Tamaina +pdfjs-editor-ink-color-input = Kolorea +pdfjs-editor-ink-thickness-input = Loditasuna +pdfjs-editor-ink-opacity-input = Opakutasuna +pdfjs-editor-stamp-add-image-button = + .title = Gehitu irudia +pdfjs-editor-stamp-add-image-button-label = Gehitu irudia +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Loditasuna +pdfjs-editor-free-highlight-thickness-title = + .title = Aldatu loditasuna testua ez beste elementuak nabarmentzean +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Testu-editorea + .default-content = Hasi idazten… +pdfjs-free-text = + .aria-label = Testu-editorea +pdfjs-free-text-default-content = Hasi idazten… +pdfjs-ink = + .aria-label = Marrazki-editorea +pdfjs-ink-canvas = + .aria-label = Erabiltzaileak sortutako irudia + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Testu alternatiboa +pdfjs-editor-alt-text-edit-button = + .aria-label = Editatu testu alternatiboa +pdfjs-editor-alt-text-edit-button-label = Editatu testu alternatiboa +pdfjs-editor-alt-text-dialog-label = Aukeratu aukera +pdfjs-editor-alt-text-dialog-description = Testu alternatiboak laguntzen du jendeak ezin duenean irudia ikusi edo ez denean kargatzen. +pdfjs-editor-alt-text-add-description-label = Gehitu azalpena +pdfjs-editor-alt-text-add-description-description = Saiatu idazten gaia, ezarpena edo ekintzak deskribatzen dituen esaldi 1 edo 2. +pdfjs-editor-alt-text-mark-decorative-label = Markatu apaingarri gisa +pdfjs-editor-alt-text-mark-decorative-description = Irudiak apaingarrientzat erabiltzen da, adibidez ertz edo ur-marketarako. +pdfjs-editor-alt-text-cancel-button = Utzi +pdfjs-editor-alt-text-save-button = Gorde +pdfjs-editor-alt-text-decorative-tooltip = Apaingarri gisa markatuta +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Adibidez, "gizon gaztea mahaian eserita dago bazkaltzeko" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Testu alternatiboa + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Goiko ezkerreko izkina — aldatu tamaina +pdfjs-editor-resizer-label-top-middle = Goian erdian — aldatu tamaina +pdfjs-editor-resizer-label-top-right = Goiko eskuineko izkina — aldatu tamaina +pdfjs-editor-resizer-label-middle-right = Erdian eskuinean — aldatu tamaina +pdfjs-editor-resizer-label-bottom-right = Beheko eskuineko izkina — aldatu tamaina +pdfjs-editor-resizer-label-bottom-middle = Behean erdian — aldatu tamaina +pdfjs-editor-resizer-label-bottom-left = Beheko ezkerreko izkina — aldatu tamaina +pdfjs-editor-resizer-label-middle-left = Erdian ezkerrean — aldatu tamaina +pdfjs-editor-resizer-top-left = + .aria-label = Goiko ezkerreko izkina — aldatu tamaina +pdfjs-editor-resizer-top-middle = + .aria-label = Goian erdian — aldatu tamaina +pdfjs-editor-resizer-top-right = + .aria-label = Goiko eskuineko izkina — aldatu tamaina +pdfjs-editor-resizer-middle-right = + .aria-label = Erdian eskuinean — aldatu tamaina +pdfjs-editor-resizer-bottom-right = + .aria-label = Beheko eskuineko izkina — aldatu tamaina +pdfjs-editor-resizer-bottom-middle = + .aria-label = Behean erdian — aldatu tamaina +pdfjs-editor-resizer-bottom-left = + .aria-label = Beheko ezkerreko izkina — aldatu tamaina +pdfjs-editor-resizer-middle-left = + .aria-label = Erdian ezkerrean — aldatu tamaina + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Nabarmentze kolorea +pdfjs-editor-colorpicker-button = + .title = Aldatu kolorea +pdfjs-editor-colorpicker-dropdown = + .aria-label = Kolore-aukerak +pdfjs-editor-colorpicker-yellow = + .title = Horia +pdfjs-editor-colorpicker-green = + .title = Berdea +pdfjs-editor-colorpicker-blue = + .title = Urdina +pdfjs-editor-colorpicker-pink = + .title = Arrosa +pdfjs-editor-colorpicker-red = + .title = Gorria + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Erakutsi denak +pdfjs-editor-highlight-show-all-button = + .title = Erakutsi denak + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editatu testu alternatiboa (irudiaren azalpena) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Gehitu testu alternatiboa (irudiaren azalpena) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Idatzi zure azalpena hemen… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Azalpen laburra irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Testu alternatibo hau automatikoki sortu da eta okerra izan liteke. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Argibide gehiago +pdfjs-editor-new-alt-text-create-automatically-button-label = Sortu testu alternatiboa automatikoki +pdfjs-editor-new-alt-text-not-now-button = Une honetan ez +pdfjs-editor-new-alt-text-error-title = Ezin da testu alternatiboa automatikoki sortu +pdfjs-editor-new-alt-text-error-description = Idatzi zure testu alternatibo propioa edo saiatu berriro geroago. +pdfjs-editor-new-alt-text-error-close-button = Itxi +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Testu alternatiboaren AA modeloa deskargatzen ({ $totalSize }/{ $downloadedSize } MB) + .aria-valuetext = Testu alternatiboaren AA modeloa deskargatzen ({ $totalSize }/{ $downloadedSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Testu alternatiboa gehituta +pdfjs-editor-new-alt-text-added-button-label = Testu alternatiboa gehituta +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Testu alternatiboa falta da +pdfjs-editor-new-alt-text-missing-button-label = Testu alternatiboa falta da +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Berrikusi testu alternatiboa +pdfjs-editor-new-alt-text-to-review-button-label = Berrikusi testu alternatiboa +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatikoki sortua: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Irudiaren testu alternatiboaren ezarpenak +pdfjs-image-alt-text-settings-button-label = Irudiaren testu alternatiboaren ezarpenak +pdfjs-editor-alt-text-settings-dialog-label = Irudiaren testu alternatiboaren ezarpenak +pdfjs-editor-alt-text-settings-automatic-title = Testu alternatibo automatikoa +pdfjs-editor-alt-text-settings-create-model-button-label = Sortu testu alternatiboa automatikoki +pdfjs-editor-alt-text-settings-create-model-description = Azalpenak iradokitzen ditu irudia ikusi ezin duen jendearentzat edo irudia kargatu ezin denerako. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Testu alternatiboaren AA modeloa ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Zure gailuan modu lokalean exekutatzen da eta zure datuak pribatu mantentzen dira. Testu alternatibo automatikorako beharrezkoa. +pdfjs-editor-alt-text-settings-delete-model-button = Ezabatu +pdfjs-editor-alt-text-settings-download-model-button = Deskargatu +pdfjs-editor-alt-text-settings-downloading-model-button = Deskargatzen… +pdfjs-editor-alt-text-settings-editor-title = Testu alternatiboaren editorea +pdfjs-editor-alt-text-settings-show-dialog-button-label = Erakutsi testu alternatiboa irudi bat gehitzean berehala +pdfjs-editor-alt-text-settings-show-dialog-description = Zure irudiek testu alternatiboa duela ziurtatzen laguntzen dizu. +pdfjs-editor-alt-text-settings-close-button = Itxi + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Nabarmentzea kenduta +pdfjs-editor-undo-bar-message-freetext = Testua kenduta +pdfjs-editor-undo-bar-message-ink = Marrazkia kenduta +pdfjs-editor-undo-bar-message-stamp = Irudia kenduta +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] Esku-ohar bat kenduta + *[other] { $count } esku-ohar kenduta + } +pdfjs-editor-undo-bar-undo-button = + .title = Desegin +pdfjs-editor-undo-bar-undo-button-label = Desegin +pdfjs-editor-undo-bar-close-button = + .title = Itxi +pdfjs-editor-undo-bar-close-button-label = Itxi diff --git a/public/assets/pdfjs/locale/fa/viewer.ftl b/public/assets/pdfjs/locale/fa/viewer.ftl new file mode 100644 index 0000000..4969209 --- /dev/null +++ b/public/assets/pdfjs/locale/fa/viewer.ftl @@ -0,0 +1,348 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = صفحهٔ قبلی +pdfjs-previous-button-label = قبلی +pdfjs-next-button = + .title = صفحهٔ بعدی +pdfjs-next-button-label = بعدی +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = صفحه +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = از { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber }از { $pagesCount }) +pdfjs-zoom-out-button = + .title = کوچک‌نمایی +pdfjs-zoom-out-button-label = کوچک‌نمایی +pdfjs-zoom-in-button = + .title = بزرگ‌نمایی +pdfjs-zoom-in-button-label = بزرگ‌نمایی +pdfjs-zoom-select = + .title = زوم +pdfjs-presentation-mode-button = + .title = تغییر به حالت ارائه +pdfjs-presentation-mode-button-label = حالت ارائه +pdfjs-open-file-button = + .title = باز کردن پرونده +pdfjs-open-file-button-label = باز کردن +pdfjs-print-button = + .title = چاپ +pdfjs-print-button-label = چاپ +pdfjs-save-button = + .title = ذخیره +pdfjs-save-button-label = ذخیره +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = دریافت +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = دریافت +pdfjs-bookmark-button = + .title = صفحه فعلی (مشاهده نشانی اینترنتی از صفحه فعلی) +pdfjs-bookmark-button-label = صفحه فعلی + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ابزارها +pdfjs-tools-button-label = ابزارها +pdfjs-first-page-button = + .title = برو به اولین صفحه +pdfjs-first-page-button-label = برو به اولین صفحه +pdfjs-last-page-button = + .title = برو به آخرین صفحه +pdfjs-last-page-button-label = برو به آخرین صفحه +pdfjs-page-rotate-cw-button = + .title = چرخش ساعتگرد +pdfjs-page-rotate-cw-button-label = چرخش ساعتگرد +pdfjs-page-rotate-ccw-button = + .title = چرخش پاد ساعتگرد +pdfjs-page-rotate-ccw-button-label = چرخش پاد ساعتگرد +pdfjs-cursor-text-select-tool-button = + .title = فعال کردن ابزارِ انتخابِ متن +pdfjs-cursor-text-select-tool-button-label = ابزارِ انتخابِ متن +pdfjs-cursor-hand-tool-button = + .title = فعال کردن ابزارِ دست +pdfjs-cursor-hand-tool-button-label = ابزار دست +pdfjs-scroll-page-button = + .title = استفاده از پیمایش صفحه +pdfjs-scroll-page-button-label = پیمایش صفحه +pdfjs-scroll-vertical-button = + .title = استفاده از پیمایش عمودی +pdfjs-scroll-vertical-button-label = پیمایش عمودی +pdfjs-scroll-horizontal-button = + .title = استفاده از پیمایش افقی +pdfjs-scroll-horizontal-button-label = پیمایش افقی +pdfjs-spread-none-button = + .title = صفحات پیوسته را یکی نکنید +pdfjs-spread-none-button-label = بدون صفحات پیوسته + +## Document properties dialog + +pdfjs-document-properties-button = + .title = خصوصیات سند... +pdfjs-document-properties-button-label = خصوصیات سند... +pdfjs-document-properties-file-name = نام پرونده: +pdfjs-document-properties-file-size = حجم پرونده: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } کیلوبایت ({ $b } بایت) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } مگابایت ({ $b } بایت) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } کیلوبایت ({ $size_b } بایت) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } مگابایت ({ $size_b } بایت) +pdfjs-document-properties-title = عنوان: +pdfjs-document-properties-author = نویسنده: +pdfjs-document-properties-subject = موضوع: +pdfjs-document-properties-keywords = کلیدواژه‌ها: +pdfjs-document-properties-creation-date = تاریخ ایجاد: +pdfjs-document-properties-modification-date = تاریخ ویرایش: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }، { $time } +pdfjs-document-properties-creator = ایجاد کننده: +pdfjs-document-properties-producer = ایجاد کننده PDF: +pdfjs-document-properties-version = نسخه PDF: +pdfjs-document-properties-page-count = تعداد صفحات: +pdfjs-document-properties-page-size = اندازه صفحه: +pdfjs-document-properties-page-size-unit-inches = اینچ +pdfjs-document-properties-page-size-unit-millimeters = میلی‌متر +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = نامه +pdfjs-document-properties-page-size-name-legal = حقوقی + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = بله +pdfjs-document-properties-linearized-no = خیر +pdfjs-document-properties-close-button = بستن + +## Print + +pdfjs-print-progress-message = آماده سازی مدارک برای چاپ کردن… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = لغو +pdfjs-printing-not-supported = هشدار: قابلیت چاپ به‌طور کامل در این مرورگر پشتیبانی نمی‌شود. +pdfjs-printing-not-ready = اخطار: پرونده PDF بطور کامل بارگیری نشده و امکان چاپ وجود ندارد. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = باز و بسته کردن نوار کناری +pdfjs-toggle-sidebar-button-label = تغییرحالت نوارکناری +pdfjs-document-outline-button = + .title = نمایش رئوس مطالب مدارک(برای بازشدن/جمع شدن همه موارد دوبار کلیک کنید) +pdfjs-document-outline-button-label = طرح نوشتار +pdfjs-attachments-button = + .title = نمایش پیوست‌ها +pdfjs-attachments-button-label = پیوست‌ها +pdfjs-layers-button-label = لایه‌ها +pdfjs-thumbs-button = + .title = نمایش تصاویر بندانگشتی +pdfjs-thumbs-button-label = تصاویر بندانگشتی +pdfjs-findbar-button = + .title = جستجو در سند +pdfjs-findbar-button-label = پیدا کردن + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = صفحه { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = تصویر بند‌ انگشتی صفحه { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = پیدا کردن + .placeholder = پیدا کردن در سند… +pdfjs-find-previous-button = + .title = پیدا کردن رخداد قبلی عبارت +pdfjs-find-previous-button-label = قبلی +pdfjs-find-next-button = + .title = پیدا کردن رخداد بعدی عبارت +pdfjs-find-next-button-label = بعدی +pdfjs-find-highlight-checkbox = برجسته و هایلایت کردن همه موارد +pdfjs-find-match-case-checkbox-label = تطبیق کوچکی و بزرگی حروف +pdfjs-find-entire-word-checkbox-label = تمام کلمه‌ها +pdfjs-find-reached-top = به بالای صفحه رسیدیم، از پایین ادامه می‌دهیم +pdfjs-find-reached-bottom = به آخر صفحه رسیدیم، از بالا ادامه می‌دهیم +pdfjs-find-not-found = عبارت پیدا نشد + +## Predefined zoom values + +pdfjs-page-scale-width = عرض صفحه +pdfjs-page-scale-fit = اندازه کردن صفحه +pdfjs-page-scale-auto = بزرگنمایی خودکار +pdfjs-page-scale-actual = اندازه واقعی‌ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = صفحهٔ { $page } + +## Loading indicator messages + +pdfjs-loading-error = هنگام بارگیری پرونده PDF خطایی رخ داد. +pdfjs-invalid-file-error = پرونده PDF نامعتبر یامعیوب می‌باشد. +pdfjs-missing-file-error = پرونده PDF یافت نشد. +pdfjs-unexpected-response-error = پاسخ پیش بینی نشده سرور +pdfjs-rendering-error = هنگام بارگیری صفحه خطایی رخ داد. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }، { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = جهت باز کردن پرونده PDF گذرواژه را وارد نمائید. +pdfjs-password-invalid = گذرواژه نامعتبر. لطفا مجددا تلاش کنید. +pdfjs-password-ok-button = تأیید +pdfjs-password-cancel-button = لغو +pdfjs-web-fonts-disabled = فونت های تحت وب غیر فعال شده اند: امکان استفاده از نمایش دهنده داخلی PDF وجود ندارد. + +## Editing + +pdfjs-editor-free-text-button = + .title = متن +pdfjs-editor-free-text-button-label = متن +pdfjs-editor-ink-button = + .title = کشیدن +pdfjs-editor-ink-button-label = کشیدن +pdfjs-editor-stamp-button = + .title = افزودن یا ویرایش تصاویر +pdfjs-editor-stamp-button-label = افزودن یا ویرایش تصاویر +pdfjs-editor-highlight-button = + .title = برجسته کردن +pdfjs-editor-highlight-button-label = برجسته کردن +pdfjs-highlight-floating-button1 = + .title = برجسته کردن + .aria-label = برجسته کردن +pdfjs-highlight-floating-button-label = برجسته کردن + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = رنگ +pdfjs-editor-free-text-size-input = اندازه +pdfjs-editor-ink-color-input = رنگ +pdfjs-editor-stamp-add-image-button = + .title = افزودن تصویر +pdfjs-editor-stamp-add-image-button-label = افزودن تصویر +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = ویرایشگر متن + .default-content = شروع به نوشتن کنید… +pdfjs-free-text = + .aria-label = ویرایشگر متن +pdfjs-free-text-default-content = شروع به نوشتن کنید… + +## Alt-text dialog + +pdfjs-editor-alt-text-add-description-label = افزودن توضیحات +pdfjs-editor-alt-text-cancel-button = انصراف +pdfjs-editor-alt-text-save-button = ذخیره + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + +pdfjs-editor-colorpicker-button = + .title = تغییر رنگ +pdfjs-editor-colorpicker-dropdown = + .aria-label = انتخاب رنگ +pdfjs-editor-colorpicker-yellow = + .title = زرد +pdfjs-editor-colorpicker-green = + .title = سبز +pdfjs-editor-colorpicker-blue = + .title = آبی +pdfjs-editor-colorpicker-pink = + .title = صورتی +pdfjs-editor-colorpicker-red = + .title = قرمز + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = نمایش همه +pdfjs-editor-highlight-show-all-button = + .title = نمایش همه + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = بیشتر بدانید +pdfjs-editor-new-alt-text-not-now-button = اکنون نه +pdfjs-editor-new-alt-text-error-close-button = بستن + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-delete-model-button = حذف +pdfjs-editor-alt-text-settings-download-model-button = دریافت +pdfjs-editor-alt-text-settings-downloading-model-button = در حال دریافت… +pdfjs-editor-alt-text-settings-close-button = بستن diff --git a/public/assets/pdfjs/locale/ff/viewer.ftl b/public/assets/pdfjs/locale/ff/viewer.ftl new file mode 100644 index 0000000..d1419f5 --- /dev/null +++ b/public/assets/pdfjs/locale/ff/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Hello Ɓennungo +pdfjs-previous-button-label = Ɓennuɗo +pdfjs-next-button = + .title = Hello faango +pdfjs-next-button-label = Yeeso +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Hello +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = e nder { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Lonngo Woɗɗa +pdfjs-zoom-out-button-label = Lonngo Woɗɗa +pdfjs-zoom-in-button = + .title = Lonngo Ara +pdfjs-zoom-in-button-label = Lonngo Ara +pdfjs-zoom-select = + .title = Lonngo +pdfjs-presentation-mode-button = + .title = Faytu to Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Uddit Fiilde +pdfjs-open-file-button-label = Uddit +pdfjs-print-button = + .title = Winndito +pdfjs-print-button-label = Winndito + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Kuutorɗe +pdfjs-tools-button-label = Kuutorɗe +pdfjs-first-page-button = + .title = Yah to hello adanngo +pdfjs-first-page-button-label = Yah to hello adanngo +pdfjs-last-page-button = + .title = Yah to hello wattindiingo +pdfjs-last-page-button-label = Yah to hello wattindiingo +pdfjs-page-rotate-cw-button = + .title = Yiiltu Faya Ñaamo +pdfjs-page-rotate-cw-button-label = Yiiltu Faya Ñaamo +pdfjs-page-rotate-ccw-button = + .title = Yiiltu Faya Nano +pdfjs-page-rotate-ccw-button-label = Yiiltu Faya Nano +pdfjs-cursor-text-select-tool-button = + .title = Gollin kaɓirgel cuɓirgel binndi +pdfjs-cursor-text-select-tool-button-label = Kaɓirgel cuɓirgel binndi +pdfjs-cursor-hand-tool-button = + .title = Hurmin kuutorgal junngo +pdfjs-cursor-hand-tool-button-label = Kaɓirgel junngo +pdfjs-scroll-vertical-button = + .title = Huutoro gorwitol daringol +pdfjs-scroll-vertical-button-label = Gorwitol daringol +pdfjs-scroll-horizontal-button = + .title = Huutoro gorwitol lelingol +pdfjs-scroll-horizontal-button-label = Gorwitol daringol +pdfjs-scroll-wrapped-button = + .title = Huutoro gorwitol coomingol +pdfjs-scroll-wrapped-button-label = Gorwitol coomingol +pdfjs-spread-none-button = + .title = Hoto tawtu kelle kelle +pdfjs-spread-none-button-label = Alaa Spreads +pdfjs-spread-odd-button = + .title = Tawtu kelle puɗɗortooɗe kelle teelɗe +pdfjs-spread-odd-button-label = Kelle teelɗe +pdfjs-spread-even-button = + .title = Tawtu ɗereeji kelle puɗɗoriiɗi kelle teeltuɗe +pdfjs-spread-even-button-label = Kelle teeltuɗe + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Keeroraaɗi Winndannde… +pdfjs-document-properties-button-label = Keeroraaɗi Winndannde… +pdfjs-document-properties-file-name = Innde fiilde: +pdfjs-document-properties-file-size = Ɓetol fiilde: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bite) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bite) +pdfjs-document-properties-title = Tiitoonde: +pdfjs-document-properties-author = Binnduɗo: +pdfjs-document-properties-subject = Toɓɓere: +pdfjs-document-properties-keywords = Kelmekele jiytirɗe: +pdfjs-document-properties-creation-date = Ñalnde Sosaa: +pdfjs-document-properties-modification-date = Ñalnde Waylaa: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Cosɗo: +pdfjs-document-properties-producer = Paggiiɗo PDF: +pdfjs-document-properties-version = Yamre PDF: +pdfjs-document-properties-page-count = Limoore Kelle: +pdfjs-document-properties-page-size = Ɓeto Hello: +pdfjs-document-properties-page-size-unit-inches = nder +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = dariingo +pdfjs-document-properties-page-size-orientation-landscape = wertiingo +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Ɓataake +pdfjs-document-properties-page-size-name-legal = Laawol + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Ɗisngo geese yaawngo: +pdfjs-document-properties-linearized-yes = Eey +pdfjs-document-properties-linearized-no = Alaa +pdfjs-document-properties-close-button = Uddu + +## Print + +pdfjs-print-progress-message = Nana heboo winnditaade fiilannde… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Haaytu +pdfjs-printing-not-supported = Reentino: Winnditagol tammbitaaka no feewi e ndee wanngorde. +pdfjs-printing-not-ready = Reentino: PDF oo loowaaki haa timmi ngam winnditagol. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggilo Palal Sawndo +pdfjs-toggle-sidebar-button-label = Toggilo Palal Sawndo +pdfjs-document-outline-button = + .title = Hollu Ƴiyal Fiilannde (dobdobo ngam wertude/taggude teme fof) +pdfjs-document-outline-button-label = Toɓɓe Fiilannde +pdfjs-attachments-button = + .title = Hollu Ɗisanɗe +pdfjs-attachments-button-label = Ɗisanɗe +pdfjs-thumbs-button = + .title = Hollu Dooɓe +pdfjs-thumbs-button-label = Dooɓe +pdfjs-findbar-button = + .title = Yiylo e fiilannde +pdfjs-findbar-button-label = Yiytu + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Hello { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Dooɓre Hello { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Yiytu + .placeholder = Yiylo nder dokimaa +pdfjs-find-previous-button = + .title = Yiylo cilol ɓennugol konngol ngol +pdfjs-find-previous-button-label = Ɓennuɗo +pdfjs-find-next-button = + .title = Yiylo cilol garowol konngol ngol +pdfjs-find-next-button-label = Yeeso +pdfjs-find-highlight-checkbox = Jalbin fof +pdfjs-find-match-case-checkbox-label = Jaaɓnu darnde +pdfjs-find-entire-word-checkbox-label = Kelme timmuɗe tan +pdfjs-find-reached-top = Heɓii fuɗɗorde fiilannde, jokku faya les +pdfjs-find-reached-bottom = Heɓii hoore fiilannde, jokku faya les +pdfjs-find-not-found = Konngi njiyataa + +## Predefined zoom values + +pdfjs-page-scale-width = Njaajeendi Hello +pdfjs-page-scale-fit = Keƴeendi Hello +pdfjs-page-scale-auto = Loongorde Jaajol +pdfjs-page-scale-actual = Ɓetol Jaati +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Juumre waɗii tuma nde loowata PDF oo. +pdfjs-invalid-file-error = Fiilde PDF moƴƴaani walla jiibii. +pdfjs-missing-file-error = Fiilde PDF ena ŋakki. +pdfjs-unexpected-response-error = Jaabtol sarworde tijjinooka. +pdfjs-rendering-error = Juumre waɗii tuma nde yoŋkittoo hello. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Siiftannde] + +## Password + +pdfjs-password-label = Naatu finnde ngam uddite ndee fiilde PDF. +pdfjs-password-invalid = Finnde moƴƴaani. Tiiɗno eto kadi. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Haaytu +pdfjs-web-fonts-disabled = Ponte geese ko daaƴaaɗe: horiima huutoraade ponte PDF coomtoraaɗe. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/fi/viewer.ftl b/public/assets/pdfjs/locale/fi/viewer.ftl new file mode 100644 index 0000000..0819d0e --- /dev/null +++ b/public/assets/pdfjs/locale/fi/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Edellinen sivu +pdfjs-previous-button-label = Edellinen +pdfjs-next-button = + .title = Seuraava sivu +pdfjs-next-button-label = Seuraava +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Sivu +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = Loitonna +pdfjs-zoom-out-button-label = Loitonna +pdfjs-zoom-in-button = + .title = Lähennä +pdfjs-zoom-in-button-label = Lähennä +pdfjs-zoom-select = + .title = Suurennus +pdfjs-presentation-mode-button = + .title = Siirry esitystilaan +pdfjs-presentation-mode-button-label = Esitystila +pdfjs-open-file-button = + .title = Avaa tiedosto +pdfjs-open-file-button-label = Avaa +pdfjs-print-button = + .title = Tulosta +pdfjs-print-button-label = Tulosta +pdfjs-save-button = + .title = Tallenna +pdfjs-save-button-label = Tallenna +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Lataa +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Lataa +pdfjs-bookmark-button = + .title = Nykyinen sivu (Näytä URL-osoite nykyiseltä sivulta) +pdfjs-bookmark-button-label = Nykyinen sivu + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Siirry ensimmäiselle sivulle +pdfjs-first-page-button-label = Siirry ensimmäiselle sivulle +pdfjs-last-page-button = + .title = Siirry viimeiselle sivulle +pdfjs-last-page-button-label = Siirry viimeiselle sivulle +pdfjs-page-rotate-cw-button = + .title = Kierrä oikealle +pdfjs-page-rotate-cw-button-label = Kierrä oikealle +pdfjs-page-rotate-ccw-button = + .title = Kierrä vasemmalle +pdfjs-page-rotate-ccw-button-label = Kierrä vasemmalle +pdfjs-cursor-text-select-tool-button = + .title = Käytä tekstinvalintatyökalua +pdfjs-cursor-text-select-tool-button-label = Tekstinvalintatyökalu +pdfjs-cursor-hand-tool-button = + .title = Käytä käsityökalua +pdfjs-cursor-hand-tool-button-label = Käsityökalu +pdfjs-scroll-page-button = + .title = Käytä sivun vieritystä +pdfjs-scroll-page-button-label = Sivun vieritys +pdfjs-scroll-vertical-button = + .title = Käytä pystysuuntaista vieritystä +pdfjs-scroll-vertical-button-label = Pystysuuntainen vieritys +pdfjs-scroll-horizontal-button = + .title = Käytä vaakasuuntaista vieritystä +pdfjs-scroll-horizontal-button-label = Vaakasuuntainen vieritys +pdfjs-scroll-wrapped-button = + .title = Käytä rivittyvää vieritystä +pdfjs-scroll-wrapped-button-label = Rivittyvä vieritys +pdfjs-spread-none-button = + .title = Älä yhdistä sivuja aukeamiksi +pdfjs-spread-none-button-label = Ei aukeamia +pdfjs-spread-odd-button = + .title = Yhdistä sivut aukeamiksi alkaen parittomalta sivulta +pdfjs-spread-odd-button-label = Parittomalta alkavat aukeamat +pdfjs-spread-even-button = + .title = Yhdistä sivut aukeamiksi alkaen parilliselta sivulta +pdfjs-spread-even-button-label = Parilliselta alkavat aukeamat + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentin ominaisuudet… +pdfjs-document-properties-button-label = Dokumentin ominaisuudet… +pdfjs-document-properties-file-name = Tiedoston nimi: +pdfjs-document-properties-file-size = Tiedoston koko: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kt ({ $b } tavua) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } Mt ({ $b } tavua) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kt ({ $size_b } tavua) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } Mt ({ $size_b } tavua) +pdfjs-document-properties-title = Otsikko: +pdfjs-document-properties-author = Tekijä: +pdfjs-document-properties-subject = Aihe: +pdfjs-document-properties-keywords = Avainsanat: +pdfjs-document-properties-creation-date = Luomispäivämäärä: +pdfjs-document-properties-modification-date = Muokkauspäivämäärä: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Luoja: +pdfjs-document-properties-producer = PDF-tuottaja: +pdfjs-document-properties-version = PDF-versio: +pdfjs-document-properties-page-count = Sivujen määrä: +pdfjs-document-properties-page-size = Sivun koko: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = pysty +pdfjs-document-properties-page-size-orientation-landscape = vaaka +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Nopea web-katselu: +pdfjs-document-properties-linearized-yes = Kyllä +pdfjs-document-properties-linearized-no = Ei +pdfjs-document-properties-close-button = Sulje + +## Print + +pdfjs-print-progress-message = Valmistellaan dokumenttia tulostamista varten… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Peruuta +pdfjs-printing-not-supported = Varoitus: Selain ei tue kaikkia tulostustapoja. +pdfjs-printing-not-ready = Varoitus: PDF-tiedosto ei ole vielä latautunut kokonaan, eikä sitä voi vielä tulostaa. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Näytä/piilota sivupaneeli +pdfjs-toggle-sidebar-notification-button = + .title = Näytä/piilota sivupaneeli (dokumentissa on sisällys/liitteitä/tasoja) +pdfjs-toggle-sidebar-button-label = Näytä/piilota sivupaneeli +pdfjs-document-outline-button = + .title = Näytä dokumentin sisällys (laajenna tai kutista kohdat kaksoisnapsauttamalla) +pdfjs-document-outline-button-label = Dokumentin sisällys +pdfjs-attachments-button = + .title = Näytä liitteet +pdfjs-attachments-button-label = Liitteet +pdfjs-layers-button = + .title = Näytä tasot (kaksoisnapsauta palauttaaksesi kaikki tasot oletustilaan) +pdfjs-layers-button-label = Tasot +pdfjs-thumbs-button = + .title = Näytä pienoiskuvat +pdfjs-thumbs-button-label = Pienoiskuvat +pdfjs-current-outline-item-button = + .title = Etsi nykyinen sisällyksen kohta +pdfjs-current-outline-item-button-label = Nykyinen sisällyksen kohta +pdfjs-findbar-button = + .title = Etsi dokumentista +pdfjs-findbar-button-label = Etsi +pdfjs-additional-layers = Lisätasot + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Sivu { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Pienoiskuva sivusta { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Etsi + .placeholder = Etsi dokumentista… +pdfjs-find-previous-button = + .title = Etsi hakusanan edellinen osuma +pdfjs-find-previous-button-label = Edellinen +pdfjs-find-next-button = + .title = Etsi hakusanan seuraava osuma +pdfjs-find-next-button-label = Seuraava +pdfjs-find-highlight-checkbox = Korosta kaikki +pdfjs-find-match-case-checkbox-label = Huomioi kirjainkoko +pdfjs-find-match-diacritics-checkbox-label = Erota tarkkeet +pdfjs-find-entire-word-checkbox-label = Kokonaiset sanat +pdfjs-find-reached-top = Päästiin dokumentin alkuun, jatketaan lopusta +pdfjs-find-reached-bottom = Päästiin dokumentin loppuun, jatketaan alusta +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } / { $total } osuma + *[other] { $current } / { $total } osumaa + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Yli { $limit } osuma + *[other] Yli { $limit } osumaa + } +pdfjs-find-not-found = Hakusanaa ei löytynyt + +## Predefined zoom values + +pdfjs-page-scale-width = Sivun leveys +pdfjs-page-scale-fit = Koko sivu +pdfjs-page-scale-auto = Automaattinen suurennus +pdfjs-page-scale-actual = Todellinen koko +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Sivu { $page } + +## Loading indicator messages + +pdfjs-loading-error = Tapahtui virhe ladattaessa PDF-tiedostoa. +pdfjs-invalid-file-error = Virheellinen tai vioittunut PDF-tiedosto. +pdfjs-missing-file-error = Puuttuva PDF-tiedosto. +pdfjs-unexpected-response-error = Odottamaton vastaus palvelimelta. +pdfjs-rendering-error = Tapahtui virhe piirrettäessä sivua. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-merkintä] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Kirjoita PDF-tiedoston salasana. +pdfjs-password-invalid = Virheellinen salasana. Yritä uudestaan. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Peruuta +pdfjs-web-fonts-disabled = Verkkosivujen omat kirjasinlajit on estetty: ei voida käyttää upotettuja PDF-kirjasinlajeja. + +## Editing + +pdfjs-editor-free-text-button = + .title = Teksti +pdfjs-editor-free-text-button-label = Teksti +pdfjs-editor-ink-button = + .title = Piirros +pdfjs-editor-ink-button-label = Piirros +pdfjs-editor-stamp-button = + .title = Lisää tai muokkaa kuvia +pdfjs-editor-stamp-button-label = Lisää tai muokkaa kuvia +pdfjs-editor-highlight-button = + .title = Korostus +pdfjs-editor-highlight-button-label = Korostus +pdfjs-highlight-floating-button1 = + .title = Korostus + .aria-label = Korostus +pdfjs-highlight-floating-button-label = Korostus + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Poista piirros +pdfjs-editor-remove-freetext-button = + .title = Poista teksti +pdfjs-editor-remove-stamp-button = + .title = Poista kuva +pdfjs-editor-remove-highlight-button = + .title = Poista korostus + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Väri +pdfjs-editor-free-text-size-input = Koko +pdfjs-editor-ink-color-input = Väri +pdfjs-editor-ink-thickness-input = Paksuus +pdfjs-editor-ink-opacity-input = Peittävyys +pdfjs-editor-stamp-add-image-button = + .title = Lisää kuva +pdfjs-editor-stamp-add-image-button-label = Lisää kuva +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Paksuus +pdfjs-editor-free-highlight-thickness-title = + .title = Muuta paksuutta korostaessasi muita kohteita kuin tekstiä +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstimuokkain + .default-content = Aloita kirjoittaminen… +pdfjs-free-text = + .aria-label = Tekstimuokkain +pdfjs-free-text-default-content = Aloita kirjoittaminen… +pdfjs-ink = + .aria-label = Piirrustusmuokkain +pdfjs-ink-canvas = + .aria-label = Käyttäjän luoma kuva + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Vaihtoehtoinen teksti +pdfjs-editor-alt-text-edit-button = + .aria-label = Muokkaa vaihtoehtoista tekstiä +pdfjs-editor-alt-text-edit-button-label = Muokkaa vaihtoehtoista tekstiä +pdfjs-editor-alt-text-dialog-label = Valitse vaihtoehto +pdfjs-editor-alt-text-dialog-description = Vaihtoehtoinen teksti ("alt-teksti") auttaa ihmisiä, jotka eivät näe kuvaa tai kun kuva ei lataudu. +pdfjs-editor-alt-text-add-description-label = Lisää kuvaus +pdfjs-editor-alt-text-add-description-description = Pyri 1-2 lauseeseen, jotka kuvaavat aihetta, ympäristöä tai toimintaa. +pdfjs-editor-alt-text-mark-decorative-label = Merkitse koristeelliseksi +pdfjs-editor-alt-text-mark-decorative-description = Tätä käytetään koristekuville, kuten reunuksille tai vesileimoille. +pdfjs-editor-alt-text-cancel-button = Peruuta +pdfjs-editor-alt-text-save-button = Tallenna +pdfjs-editor-alt-text-decorative-tooltip = Merkitty koristeelliseksi +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Esimerkiksi "Nuori mies istuu pöytään syömään aterian" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Vaihtoehtoinen teksti + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Vasen yläkulma - muuta kokoa +pdfjs-editor-resizer-label-top-middle = Ylhäällä keskellä - muuta kokoa +pdfjs-editor-resizer-label-top-right = Oikea yläkulma - muuta kokoa +pdfjs-editor-resizer-label-middle-right = Keskellä oikealla - muuta kokoa +pdfjs-editor-resizer-label-bottom-right = Oikea alakulma - muuta kokoa +pdfjs-editor-resizer-label-bottom-middle = Alhaalla keskellä - muuta kokoa +pdfjs-editor-resizer-label-bottom-left = Vasen alakulma - muuta kokoa +pdfjs-editor-resizer-label-middle-left = Keskellä vasemmalla - muuta kokoa +pdfjs-editor-resizer-top-left = + .aria-label = Vasen yläkulma - muuta kokoa +pdfjs-editor-resizer-top-middle = + .aria-label = Ylhäällä keskellä - muuta kokoa +pdfjs-editor-resizer-top-right = + .aria-label = Oikea yläkulma - muuta kokoa +pdfjs-editor-resizer-middle-right = + .aria-label = Keskellä oikealla - muuta kokoa +pdfjs-editor-resizer-bottom-right = + .aria-label = Oikea alakulma - muuta kokoa +pdfjs-editor-resizer-bottom-middle = + .aria-label = Alhaalla keskellä - muuta kokoa +pdfjs-editor-resizer-bottom-left = + .aria-label = Vasen alakulma - muuta kokoa +pdfjs-editor-resizer-middle-left = + .aria-label = Keskellä vasemmalla - muuta kokoa + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Korostusväri +pdfjs-editor-colorpicker-button = + .title = Vaihda väri +pdfjs-editor-colorpicker-dropdown = + .aria-label = Värivalinnat +pdfjs-editor-colorpicker-yellow = + .title = Keltainen +pdfjs-editor-colorpicker-green = + .title = Vihreä +pdfjs-editor-colorpicker-blue = + .title = Sininen +pdfjs-editor-colorpicker-pink = + .title = Pinkki +pdfjs-editor-colorpicker-red = + .title = Punainen + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Näytä kaikki +pdfjs-editor-highlight-show-all-button = + .title = Näytä kaikki + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Muokkaa vaihtoehtoista tekstiä (kuvan kuvaus) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Lisää vaihtoehtoinen teksti (kuvan kuvaus) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Kirjoita kuvaus tähän… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Lyhyt kuvaus ihmisille, jotka eivät näe kuvaa tai kun kuva ei lataudu. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Tämä vaihtoehtoinen teksti luotiin automaattisesti, ja se voi olla epätarkka. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Lue lisää +pdfjs-editor-new-alt-text-create-automatically-button-label = Luo vaihtoehtoinen teksti automaattisesti +pdfjs-editor-new-alt-text-not-now-button = Ei nyt +pdfjs-editor-new-alt-text-error-title = Vaihtoehtotekstiä ei voitu luoda automaattisesti +pdfjs-editor-new-alt-text-error-description = Kirjoita oma vaihtoehtoinen teksti tai yritä myöhemmin uudelleen. +pdfjs-editor-new-alt-text-error-close-button = Sulje +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Ladataan vaihtoehtoisen tekstin tekoälymallia ({ $downloadedSize } / { $totalSize } Mt) + .aria-valuetext = Ladataan vaihtoehtoisen tekstin tekoälymallia ({ $downloadedSize } / { $totalSize } Mt) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Vaihtoehtoinen teksti lisätty +pdfjs-editor-new-alt-text-added-button-label = Vaihtoehtoinen teksti lisätty +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Vaihtoehtoinen teksti puuttuu +pdfjs-editor-new-alt-text-missing-button-label = Vaihtoehtoinen teksti puuttuu +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Tarkista vaihtoehtoinen teksti +pdfjs-editor-new-alt-text-to-review-button-label = Tarkista vaihtoehtoinen teksti +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Luotu automaattisesti: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Kuvan vaihtoehtoisen tekstin asetukset +pdfjs-image-alt-text-settings-button-label = Kuvan vaihtoehtoisen tekstin asetukset +pdfjs-editor-alt-text-settings-dialog-label = Kuvan vaihtoehtoisen tekstin asetukset +pdfjs-editor-alt-text-settings-automatic-title = Automaattinen vaihtoehtoinen teksti +pdfjs-editor-alt-text-settings-create-model-button-label = Luo vaihtoehtoinen teksti automaattisesti +pdfjs-editor-alt-text-settings-create-model-description = Ehdottaa kuvauksia, jotka auttavat ihmisiä, jotka eivät näe kuvaa tai kun kuva ei lataudu. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Vaihtoehtoisen tekstin tekoälymalli ({ $totalSize } Mt) +pdfjs-editor-alt-text-settings-ai-model-description = Toimii paikallisesti laitteellasi, joten tietosi pysyvät yksityisinä. Vaadittu automaattiselle vaihtoehtoiselle tekstille. +pdfjs-editor-alt-text-settings-delete-model-button = Poista +pdfjs-editor-alt-text-settings-download-model-button = Lataa +pdfjs-editor-alt-text-settings-downloading-model-button = Ladataan… +pdfjs-editor-alt-text-settings-editor-title = Vaihtoehtoisen tekstin muokkain +pdfjs-editor-alt-text-settings-show-dialog-button-label = Näytä vaihtoehtoisen tekstin muokkain heti, kun lisäät kuvan +pdfjs-editor-alt-text-settings-show-dialog-description = Auttaa varmistamaan, että kaikissa kuvissasi on vaihtoehtoinen teksti. +pdfjs-editor-alt-text-settings-close-button = Sulje + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Korostus poistettu +pdfjs-editor-undo-bar-message-freetext = Teksti poistettu +pdfjs-editor-undo-bar-message-ink = Piirustus poistettu +pdfjs-editor-undo-bar-message-stamp = Kuva poistettu +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } merkintä poistettu + *[other] { $count } merkintää poistettu + } +pdfjs-editor-undo-bar-undo-button = + .title = Kumoa +pdfjs-editor-undo-bar-undo-button-label = Kumoa +pdfjs-editor-undo-bar-close-button = + .title = Sulje +pdfjs-editor-undo-bar-close-button-label = Sulje diff --git a/public/assets/pdfjs/locale/fr/viewer.ftl b/public/assets/pdfjs/locale/fr/viewer.ftl new file mode 100644 index 0000000..d0a778f --- /dev/null +++ b/public/assets/pdfjs/locale/fr/viewer.ftl @@ -0,0 +1,511 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Page précédente +pdfjs-previous-button-label = Précédent +pdfjs-next-button = + .title = Page suivante +pdfjs-next-button-label = Suivant +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Page +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = sur { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } sur { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom arrière +pdfjs-zoom-out-button-label = Zoom arrière +pdfjs-zoom-in-button = + .title = Zoom avant +pdfjs-zoom-in-button-label = Zoom avant +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Basculer en mode présentation +pdfjs-presentation-mode-button-label = Mode présentation +pdfjs-open-file-button = + .title = Ouvrir le fichier +pdfjs-open-file-button-label = Ouvrir le fichier +pdfjs-print-button = + .title = Imprimer +pdfjs-print-button-label = Imprimer +pdfjs-save-button = + .title = Enregistrer +pdfjs-save-button-label = Enregistrer +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Télécharger +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Télécharger +pdfjs-bookmark-button = + .title = Page courante (montrer l’adresse de la page courante) +pdfjs-bookmark-button-label = Page courante + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Outils +pdfjs-tools-button-label = Outils +pdfjs-first-page-button = + .title = Aller à la première page +pdfjs-first-page-button-label = Aller à la première page +pdfjs-last-page-button = + .title = Aller à la dernière page +pdfjs-last-page-button-label = Aller à la dernière page +pdfjs-page-rotate-cw-button = + .title = Rotation horaire +pdfjs-page-rotate-cw-button-label = Rotation horaire +pdfjs-page-rotate-ccw-button = + .title = Rotation antihoraire +pdfjs-page-rotate-ccw-button-label = Rotation antihoraire +pdfjs-cursor-text-select-tool-button = + .title = Activer l’outil de sélection de texte +pdfjs-cursor-text-select-tool-button-label = Outil de sélection de texte +pdfjs-cursor-hand-tool-button = + .title = Activer l’outil main +pdfjs-cursor-hand-tool-button-label = Outil main +pdfjs-scroll-page-button = + .title = Utiliser le défilement par page +pdfjs-scroll-page-button-label = Défilement par page +pdfjs-scroll-vertical-button = + .title = Utiliser le défilement vertical +pdfjs-scroll-vertical-button-label = Défilement vertical +pdfjs-scroll-horizontal-button = + .title = Utiliser le défilement horizontal +pdfjs-scroll-horizontal-button-label = Défilement horizontal +pdfjs-scroll-wrapped-button = + .title = Utiliser le défilement par bloc +pdfjs-scroll-wrapped-button-label = Défilement par bloc +pdfjs-spread-none-button = + .title = Ne pas afficher les pages deux à deux +pdfjs-spread-none-button-label = Pas de double affichage +pdfjs-spread-odd-button = + .title = Afficher les pages par deux, impaires à gauche +pdfjs-spread-odd-button-label = Doubles pages, impaires à gauche +pdfjs-spread-even-button = + .title = Afficher les pages par deux, paires à gauche +pdfjs-spread-even-button-label = Doubles pages, paires à gauche + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propriétés du document… +pdfjs-document-properties-button-label = Propriétés du document… +pdfjs-document-properties-file-name = Nom du fichier : +pdfjs-document-properties-file-size = Taille du fichier : +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } Ko ({ $b } octets) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } Mo ({ $b } octets) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } Ko ({ $size_b } octets) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } Mo ({ $size_b } octets) +pdfjs-document-properties-title = Titre : +pdfjs-document-properties-author = Auteur : +pdfjs-document-properties-subject = Sujet : +pdfjs-document-properties-keywords = Mots-clés : +pdfjs-document-properties-creation-date = Date de création : +pdfjs-document-properties-modification-date = Modifié le : +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } à { $time } +pdfjs-document-properties-creator = Créé par : +pdfjs-document-properties-producer = Outil de conversion PDF : +pdfjs-document-properties-version = Version PDF : +pdfjs-document-properties-page-count = Nombre de pages : +pdfjs-document-properties-page-size = Taille de la page : +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = paysage +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = lettre +pdfjs-document-properties-page-size-name-legal = document juridique + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Affichage rapide des pages web : +pdfjs-document-properties-linearized-yes = Oui +pdfjs-document-properties-linearized-no = Non +pdfjs-document-properties-close-button = Fermer + +## Print + +pdfjs-print-progress-message = Préparation du document pour l’impression… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Annuler +pdfjs-printing-not-supported = Attention : l’impression n’est pas totalement prise en charge par ce navigateur. +pdfjs-printing-not-ready = Attention : le PDF n’est pas entièrement chargé pour pouvoir l’imprimer. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Afficher/Masquer le panneau latéral +pdfjs-toggle-sidebar-notification-button = + .title = Afficher/Masquer le panneau latéral (le document contient des signets/pièces jointes/calques) +pdfjs-toggle-sidebar-button-label = Afficher/Masquer le panneau latéral +pdfjs-document-outline-button = + .title = Afficher les signets du document (double-cliquer pour développer/réduire tous les éléments) +pdfjs-document-outline-button-label = Signets du document +pdfjs-attachments-button = + .title = Afficher les pièces jointes +pdfjs-attachments-button-label = Pièces jointes +pdfjs-layers-button = + .title = Afficher les calques (double-cliquer pour réinitialiser tous les calques à l’état par défaut) +pdfjs-layers-button-label = Calques +pdfjs-thumbs-button = + .title = Afficher les vignettes +pdfjs-thumbs-button-label = Vignettes +pdfjs-current-outline-item-button = + .title = Trouver l’élément de plan actuel +pdfjs-current-outline-item-button-label = Élément de plan actuel +pdfjs-findbar-button = + .title = Rechercher dans le document +pdfjs-findbar-button-label = Rechercher +pdfjs-additional-layers = Calques additionnels + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Vignette de la page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Rechercher + .placeholder = Rechercher dans le document… +pdfjs-find-previous-button = + .title = Trouver l’occurrence précédente de l’expression +pdfjs-find-previous-button-label = Précédent +pdfjs-find-next-button = + .title = Trouver la prochaine occurrence de l’expression +pdfjs-find-next-button-label = Suivant +pdfjs-find-highlight-checkbox = Tout surligner +pdfjs-find-match-case-checkbox-label = Respecter la casse +pdfjs-find-match-diacritics-checkbox-label = Respecter les accents et diacritiques +pdfjs-find-entire-word-checkbox-label = Mots entiers +pdfjs-find-reached-top = Haut de la page atteint, poursuite depuis la fin +pdfjs-find-reached-bottom = Bas de la page atteint, poursuite au début +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = Occurrence { $current } sur { $total } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Plus d’{ $limit } occurrence + *[other] Plus de { $limit } occurrences + } +pdfjs-find-not-found = Expression non trouvée + +## Predefined zoom values + +pdfjs-page-scale-width = Pleine largeur +pdfjs-page-scale-fit = Page entière +pdfjs-page-scale-auto = Zoom automatique +pdfjs-page-scale-actual = Taille réelle +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Page { $page } + +## Loading indicator messages + +pdfjs-loading-error = Une erreur s’est produite lors du chargement du fichier PDF. +pdfjs-invalid-file-error = Fichier PDF invalide ou corrompu. +pdfjs-missing-file-error = Fichier PDF manquant. +pdfjs-unexpected-response-error = Réponse inattendue du serveur. +pdfjs-rendering-error = Une erreur s’est produite lors de l’affichage de la page. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } à { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Annotation { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Veuillez saisir le mot de passe pour ouvrir ce fichier PDF. +pdfjs-password-invalid = Mot de passe incorrect. Veuillez réessayer. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Annuler +pdfjs-web-fonts-disabled = Les polices web sont désactivées : impossible d’utiliser les polices intégrées au PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texte +pdfjs-editor-free-text-button-label = Texte +pdfjs-editor-ink-button = + .title = Dessiner +pdfjs-editor-ink-button-label = Dessiner +pdfjs-editor-stamp-button = + .title = Ajouter ou modifier des images +pdfjs-editor-stamp-button-label = Ajouter ou modifier des images +pdfjs-editor-highlight-button = + .title = Surligner +pdfjs-editor-highlight-button-label = Surligner +pdfjs-highlight-floating-button1 = + .title = Surligner + .aria-label = Surligner +pdfjs-highlight-floating-button-label = Surligner + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Supprimer le dessin +pdfjs-editor-remove-freetext-button = + .title = Supprimer le texte +pdfjs-editor-remove-stamp-button = + .title = Supprimer l’image +pdfjs-editor-remove-highlight-button = + .title = Supprimer le surlignage + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Couleur +pdfjs-editor-free-text-size-input = Taille +pdfjs-editor-ink-color-input = Couleur +pdfjs-editor-ink-thickness-input = Épaisseur +pdfjs-editor-ink-opacity-input = Opacité +pdfjs-editor-stamp-add-image-button = + .title = Ajouter une image +pdfjs-editor-stamp-add-image-button-label = Ajouter une image +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Épaisseur +pdfjs-editor-free-highlight-thickness-title = + .title = Modifier l’épaisseur pour le surlignage d’éléments non textuels +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Éditeur de texte + .default-content = Commencez à écrire… +pdfjs-free-text = + .aria-label = Éditeur de texte +pdfjs-free-text-default-content = Commencer à écrire… +pdfjs-ink = + .aria-label = Éditeur de dessin +pdfjs-ink-canvas = + .aria-label = Image créée par l’utilisateur·trice + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texte alternatif +pdfjs-editor-alt-text-edit-button = + .aria-label = Modifier le texte alternatif +pdfjs-editor-alt-text-edit-button-label = Modifier le texte alternatif +pdfjs-editor-alt-text-dialog-label = Sélectionnez une option +pdfjs-editor-alt-text-dialog-description = Le texte alternatif est utile lorsque des personnes ne peuvent pas voir l’image ou que l’image ne se charge pas. +pdfjs-editor-alt-text-add-description-label = Ajouter une description +pdfjs-editor-alt-text-add-description-description = Il est conseillé de rédiger une ou deux phrases décrivant le sujet, le cadre ou les actions. +pdfjs-editor-alt-text-mark-decorative-label = Marquer comme décorative +pdfjs-editor-alt-text-mark-decorative-description = Cette option est utilisée pour les images décoratives, comme les bordures ou les filigranes. +pdfjs-editor-alt-text-cancel-button = Annuler +pdfjs-editor-alt-text-save-button = Enregistrer +pdfjs-editor-alt-text-decorative-tooltip = Marquée comme décorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Par exemple, « Un jeune homme est assis à une table pour prendre un repas » +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texte alternatif + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Coin supérieur gauche — redimensionner +pdfjs-editor-resizer-label-top-middle = Milieu haut — redimensionner +pdfjs-editor-resizer-label-top-right = Coin supérieur droit — redimensionner +pdfjs-editor-resizer-label-middle-right = Milieu droit — redimensionner +pdfjs-editor-resizer-label-bottom-right = Coin inférieur droit — redimensionner +pdfjs-editor-resizer-label-bottom-middle = Centre bas — redimensionner +pdfjs-editor-resizer-label-bottom-left = Coin inférieur gauche — redimensionner +pdfjs-editor-resizer-label-middle-left = Milieu gauche — redimensionner +pdfjs-editor-resizer-top-left = + .aria-label = Coin supérieur gauche — redimensionner +pdfjs-editor-resizer-top-middle = + .aria-label = Milieu haut — redimensionner +pdfjs-editor-resizer-top-right = + .aria-label = Coin supérieur droit — redimensionner +pdfjs-editor-resizer-middle-right = + .aria-label = Milieu droit — redimensionner +pdfjs-editor-resizer-bottom-right = + .aria-label = Coin inférieur droit — redimensionner +pdfjs-editor-resizer-bottom-middle = + .aria-label = Centre bas — redimensionner +pdfjs-editor-resizer-bottom-left = + .aria-label = Coin inférieur gauche — redimensionner +pdfjs-editor-resizer-middle-left = + .aria-label = Milieu gauche — redimensionner + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Couleur de surlignage +pdfjs-editor-colorpicker-button = + .title = Changer de couleur +pdfjs-editor-colorpicker-dropdown = + .aria-label = Choix de couleurs +pdfjs-editor-colorpicker-yellow = + .title = Jaune +pdfjs-editor-colorpicker-green = + .title = Vert +pdfjs-editor-colorpicker-blue = + .title = Bleu +pdfjs-editor-colorpicker-pink = + .title = Rose +pdfjs-editor-colorpicker-red = + .title = Rouge + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Tout afficher +pdfjs-editor-highlight-show-all-button = + .title = Tout afficher + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Modifier le texte alternatif (description de l’image) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Ajouter du texte alternatif (description de l’image) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Rédigez votre description ici… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Courte description pour les personnes qui ne peuvent pas voir l’image ou lorsque l’image ne se charge pas. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ce texte alternatif a été créé automatiquement et peut être inexact. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = En savoir plus +pdfjs-editor-new-alt-text-create-automatically-button-label = Créer automatiquement le texte alternatif +pdfjs-editor-new-alt-text-not-now-button = Pas maintenant +pdfjs-editor-new-alt-text-error-title = Impossible de créer automatiquement le texte alternatif +pdfjs-editor-new-alt-text-error-description = Veuillez rédiger votre propre texte alternatif ou réessayer plus tard. +pdfjs-editor-new-alt-text-error-close-button = Fermer +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Téléchargement du modèle d’IA de texte alternatif ({ $downloadedSize } sur { $totalSize } Mo) + .aria-valuetext = Téléchargement du modèle d’IA de texte alternatif ({ $downloadedSize } sur { $totalSize } Mo) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Texte alternatif ajouté +pdfjs-editor-new-alt-text-added-button-label = Texte alternatif ajouté +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Texte alternatif manquant +pdfjs-editor-new-alt-text-missing-button-label = Texte alternatif manquant +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Réviser le texte alternatif +pdfjs-editor-new-alt-text-to-review-button-label = Réviser le texte alternatif +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Créé automatiquement : { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Paramètres du texte alternatif des images +pdfjs-image-alt-text-settings-button-label = Paramètres du texte alternatif des images +pdfjs-editor-alt-text-settings-dialog-label = Paramètres du texte alternatif des images +pdfjs-editor-alt-text-settings-automatic-title = Texte alternatif automatique +pdfjs-editor-alt-text-settings-create-model-button-label = Créer automatiquement le texte alternatif +pdfjs-editor-alt-text-settings-create-model-description = Suggère des descriptions pour aider les personnes qui ne peuvent pas voir l’image ou lorsque l’image ne se charge pas. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modèle d’IA de texte alternatif ({ $totalSize } Mo) +pdfjs-editor-alt-text-settings-ai-model-description = Fonctionne localement sur votre appareil, vos données restent privées. Obligatoire pour la génération automatique de texte alternatif. +pdfjs-editor-alt-text-settings-delete-model-button = Supprimer +pdfjs-editor-alt-text-settings-download-model-button = Télécharger +pdfjs-editor-alt-text-settings-downloading-model-button = Téléchargement… +pdfjs-editor-alt-text-settings-editor-title = Éditeur de texte alternatif +pdfjs-editor-alt-text-settings-show-dialog-button-label = Afficher l’éditeur de texte alternatif immédiatement lors de l’ajout d’une image +pdfjs-editor-alt-text-settings-show-dialog-description = Vous aide à vous assurer que toutes vos images ont du texte alternatif. +pdfjs-editor-alt-text-settings-close-button = Fermer + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Surlignage supprimé +pdfjs-editor-undo-bar-message-freetext = Texte supprimé +pdfjs-editor-undo-bar-message-ink = Dessin supprimé +pdfjs-editor-undo-bar-message-stamp = Image supprimée +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation supprimée + *[other] { $count } annotations supprimées + } +pdfjs-editor-undo-bar-undo-button = + .title = Annuler +pdfjs-editor-undo-bar-undo-button-label = Annuler +pdfjs-editor-undo-bar-close-button = + .title = Fermer +pdfjs-editor-undo-bar-close-button-label = Fermer diff --git a/public/assets/pdfjs/locale/fur/viewer.ftl b/public/assets/pdfjs/locale/fur/viewer.ftl new file mode 100644 index 0000000..370af3f --- /dev/null +++ b/public/assets/pdfjs/locale/fur/viewer.ftl @@ -0,0 +1,485 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagjine precedente +pdfjs-previous-button-label = Indaûr +pdfjs-next-button = + .title = Prossime pagjine +pdfjs-next-button-label = Indevant +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagjine +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = di { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } di { $pagesCount }) +pdfjs-zoom-out-button = + .title = Impiçulìs +pdfjs-zoom-out-button-label = Impiçulìs +pdfjs-zoom-in-button = + .title = Ingrandìs +pdfjs-zoom-in-button-label = Ingrandìs +pdfjs-zoom-select = + .title = Ingrandiment +pdfjs-presentation-mode-button = + .title = Passe ae modalitât presentazion +pdfjs-presentation-mode-button-label = Modalitât presentazion +pdfjs-open-file-button = + .title = Vierç un file +pdfjs-open-file-button-label = Vierç +pdfjs-print-button = + .title = Stampe +pdfjs-print-button-label = Stampe +pdfjs-save-button = + .title = Salve +pdfjs-save-button-label = Salve +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Discjame +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Discjame +pdfjs-bookmark-button = + .title = Pagjine corinte (mostre URL de pagjine atuâl) +pdfjs-bookmark-button-label = Pagjine corinte + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Struments +pdfjs-tools-button-label = Struments +pdfjs-first-page-button = + .title = Va ae prime pagjine +pdfjs-first-page-button-label = Va ae prime pagjine +pdfjs-last-page-button = + .title = Va ae ultime pagjine +pdfjs-last-page-button-label = Va ae ultime pagjine +pdfjs-page-rotate-cw-button = + .title = Zire in sens orari +pdfjs-page-rotate-cw-button-label = Zire in sens orari +pdfjs-page-rotate-ccw-button = + .title = Zire in sens antiorari +pdfjs-page-rotate-ccw-button-label = Zire in sens antiorari +pdfjs-cursor-text-select-tool-button = + .title = Ative il strument di selezion dal test +pdfjs-cursor-text-select-tool-button-label = Strument di selezion dal test +pdfjs-cursor-hand-tool-button = + .title = Ative il strument manute +pdfjs-cursor-hand-tool-button-label = Strument manute +pdfjs-scroll-page-button = + .title = Dopre il scoriment des pagjinis +pdfjs-scroll-page-button-label = Scoriment pagjinis +pdfjs-scroll-vertical-button = + .title = Dopre scoriment verticâl +pdfjs-scroll-vertical-button-label = Scoriment verticâl +pdfjs-scroll-horizontal-button = + .title = Dopre scoriment orizontâl +pdfjs-scroll-horizontal-button-label = Scoriment orizontâl +pdfjs-scroll-wrapped-button = + .title = Dopre scoriment par blocs +pdfjs-scroll-wrapped-button-label = Scoriment par blocs +pdfjs-spread-none-button = + .title = No sta meti dongje pagjinis in cubie +pdfjs-spread-none-button-label = No cubiis di pagjinis +pdfjs-spread-odd-button = + .title = Met dongje cubiis di pagjinis scomençant des pagjinis dispar +pdfjs-spread-odd-button-label = Cubiis di pagjinis, dispar a çampe +pdfjs-spread-even-button = + .title = Met dongje cubiis di pagjinis scomençant des pagjinis pâr +pdfjs-spread-even-button-label = Cubiis di pagjinis, pâr a çampe + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Proprietâts dal document… +pdfjs-document-properties-button-label = Proprietâts dal document… +pdfjs-document-properties-file-name = Non dal file: +pdfjs-document-properties-file-size = Dimension dal file: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titul: +pdfjs-document-properties-author = Autôr: +pdfjs-document-properties-subject = Ogjet: +pdfjs-document-properties-keywords = Peraulis clâf: +pdfjs-document-properties-creation-date = Date di creazion: +pdfjs-document-properties-modification-date = Date di modifiche: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creatôr +pdfjs-document-properties-producer = Gjeneradôr PDF: +pdfjs-document-properties-version = Version PDF: +pdfjs-document-properties-page-count = Numar di pagjinis: +pdfjs-document-properties-page-size = Dimension de pagjine: +pdfjs-document-properties-page-size-unit-inches = oncis +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = verticâl +pdfjs-document-properties-page-size-orientation-landscape = orizontâl +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letare +pdfjs-document-properties-page-size-name-legal = Legâl + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Visualizazion web svelte: +pdfjs-document-properties-linearized-yes = Sì +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Siere + +## Print + +pdfjs-print-progress-message = Daûr a prontâ il document pe stampe… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Anule +pdfjs-printing-not-supported = Atenzion: la stampe no je supuartade ad implen di chest navigadôr. +pdfjs-printing-not-ready = Atenzion: il PDF nol è stât cjamât dal dut pe stampe. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Ative/Disative sbare laterâl +pdfjs-toggle-sidebar-notification-button = + .title = Ative/Disative sbare laterâl (il document al conten struture/zontis/strâts) +pdfjs-toggle-sidebar-button-label = Ative/Disative sbare laterâl +pdfjs-document-outline-button = + .title = Mostre la struture dal document (dopli clic par slargjâ/strenzi ducj i elements) +pdfjs-document-outline-button-label = Struture dal document +pdfjs-attachments-button = + .title = Mostre lis zontis +pdfjs-attachments-button-label = Zontis +pdfjs-layers-button = + .title = Mostre i strâts (dopli clic par ristabilî ducj i strâts al stât predefinît) +pdfjs-layers-button-label = Strâts +pdfjs-thumbs-button = + .title = Mostre miniaturis +pdfjs-thumbs-button-label = Miniaturis +pdfjs-current-outline-item-button = + .title = Cjate l'element de struture atuâl +pdfjs-current-outline-item-button-label = Element de struture atuâl +pdfjs-findbar-button = + .title = Cjate tal document +pdfjs-findbar-button-label = Cjate +pdfjs-additional-layers = Strâts adizionâi + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagjine { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniature de pagjine { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Cjate + .placeholder = Cjate tal document… +pdfjs-find-previous-button = + .title = Cjate il câs precedent dal test +pdfjs-find-previous-button-label = Precedent +pdfjs-find-next-button = + .title = Cjate il câs sucessîf dal test +pdfjs-find-next-button-label = Sucessîf +pdfjs-find-highlight-checkbox = Evidenzie dut +pdfjs-find-match-case-checkbox-label = Fâs distinzion tra maiusculis e minusculis +pdfjs-find-match-diacritics-checkbox-label = Corispondence diacritiche +pdfjs-find-entire-word-checkbox-label = Peraulis interiis +pdfjs-find-reached-top = Si è rivâts al inizi dal document e si à continuât de fin +pdfjs-find-reached-bottom = Si è rivât ae fin dal document e si à continuât dal inizi +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } di { $total } corispondence + *[other] { $current } di { $total } corispondencis + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Plui di { $limit } corispondence + *[other] Plui di { $limit } corispondencis + } +pdfjs-find-not-found = Test no cjatât + +## Predefined zoom values + +pdfjs-page-scale-width = Largjece de pagjine +pdfjs-page-scale-fit = Pagjine interie +pdfjs-page-scale-auto = Ingrandiment automatic +pdfjs-page-scale-actual = Dimension reâl +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagjine { $page } + +## Loading indicator messages + +pdfjs-loading-error = Al è vignût fûr un erôr intant che si cjariave il PDF. +pdfjs-invalid-file-error = File PDF no valit o ruvinât. +pdfjs-missing-file-error = Al mancje il file PDF. +pdfjs-unexpected-response-error = Rispueste dal servidôr inspietade. +pdfjs-rendering-error = Al è vignût fûr un erôr tal realizâ la visualizazion de pagjine. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotazion { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Inserìs la password par vierzi chest file PDF. +pdfjs-password-invalid = Password no valide. Par plasê torne prove. +pdfjs-password-ok-button = Va ben +pdfjs-password-cancel-button = Anule +pdfjs-web-fonts-disabled = I caratars dal Web a son disativâts: Impussibil doprâ i caratars PDF incorporâts. + +## Editing + +pdfjs-editor-free-text-button = + .title = Test +pdfjs-editor-free-text-button-label = Test +pdfjs-editor-ink-button = + .title = Dissen +pdfjs-editor-ink-button-label = Dissen +pdfjs-editor-stamp-button = + .title = Zonte o modifiche imagjins +pdfjs-editor-stamp-button-label = Zonte o modifiche imagjins +pdfjs-editor-highlight-button = + .title = Evidenzie +pdfjs-editor-highlight-button-label = Evidenzie +pdfjs-highlight-floating-button1 = + .title = Evidenzie + .aria-label = Evidenzie +pdfjs-highlight-floating-button-label = Evidenzie + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Gjave dissen +pdfjs-editor-remove-freetext-button = + .title = Gjave test +pdfjs-editor-remove-stamp-button = + .title = Gjave imagjin +pdfjs-editor-remove-highlight-button = + .title = Gjave evidenziazion + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colôr +pdfjs-editor-free-text-size-input = Dimension +pdfjs-editor-ink-color-input = Colôr +pdfjs-editor-ink-thickness-input = Spessôr +pdfjs-editor-ink-opacity-input = Opacitât +pdfjs-editor-stamp-add-image-button = + .title = Zonte imagjin +pdfjs-editor-stamp-add-image-button-label = Zonte imagjin +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Spessôr +pdfjs-editor-free-highlight-thickness-title = + .title = Modifiche il spessôr de selezion pai elements che no son testuâi +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editôr di test + .default-content = Scomence a scrivi… +pdfjs-free-text = + .aria-label = Editôr di test +pdfjs-free-text-default-content = Scomence a scrivi… +pdfjs-ink = + .aria-label = Editôr dissens +pdfjs-ink-canvas = + .aria-label = Imagjin creade dal utent + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = Test alternatîf +pdfjs-editor-alt-text-edit-button-label = Modifiche test alternatîf +pdfjs-editor-alt-text-dialog-label = Sielç une opzion +pdfjs-editor-alt-text-dialog-description = Il test alternatîf (“alt text”) al jude cuant che lis personis no puedin viodi la imagjin o cuant che la imagjine no ven cjariade. +pdfjs-editor-alt-text-add-description-label = Zonte une descrizion +pdfjs-editor-alt-text-add-description-description = Ponte a une o dôs frasis che a descrivin l’argoment, la ambientazion o lis azions. +pdfjs-editor-alt-text-mark-decorative-label = Segne come decorative +pdfjs-editor-alt-text-mark-decorative-description = Chest al ven doprât pes imagjins ornamentâls, come i ôrs o lis filigranis. +pdfjs-editor-alt-text-cancel-button = Anule +pdfjs-editor-alt-text-save-button = Salve +pdfjs-editor-alt-text-decorative-tooltip = Segnade come decorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Par esempli, “Un zovin si sente a taule par mangjâ” + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Cjanton in alt a çampe — ridimensione +pdfjs-editor-resizer-label-top-middle = Bande superiôr tal mieç — ridimensione +pdfjs-editor-resizer-label-top-right = Cjanton in alt a diestre — ridimensione +pdfjs-editor-resizer-label-middle-right = Bande diestre tal mieç — ridimensione +pdfjs-editor-resizer-label-bottom-right = Cjanton in bas a diestre — ridimensione +pdfjs-editor-resizer-label-bottom-middle = Bande inferiôr tal mieç — ridimensione +pdfjs-editor-resizer-label-bottom-left = Cjanton in bas a çampe — ridimensione +pdfjs-editor-resizer-label-middle-left = Bande di çampe tal mieç — ridimensione +pdfjs-editor-resizer-top-left = + .aria-label = Cjanton in alt a çampe — ridimensione +pdfjs-editor-resizer-top-middle = + .aria-label = Bande superiôr tal mieç — ridimensione +pdfjs-editor-resizer-top-right = + .aria-label = Cjanton in alt a diestre — ridimensione +pdfjs-editor-resizer-middle-right = + .aria-label = Bande diestre tal mieç — ridimensione +pdfjs-editor-resizer-bottom-right = + .aria-label = Cjanton in bas a diestre — ridimensione +pdfjs-editor-resizer-bottom-middle = + .aria-label = Bande inferiôr tal mieç — ridimensione +pdfjs-editor-resizer-bottom-left = + .aria-label = Cjanton in bas a çampe — ridimensione +pdfjs-editor-resizer-middle-left = + .aria-label = Bande di çampe tal mieç — ridimensione + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Colôr par evidenziâ +pdfjs-editor-colorpicker-button = + .title = Cambie colôr +pdfjs-editor-colorpicker-dropdown = + .aria-label = Sieltis di colôr +pdfjs-editor-colorpicker-yellow = + .title = Zâl +pdfjs-editor-colorpicker-green = + .title = Vert +pdfjs-editor-colorpicker-blue = + .title = Blu +pdfjs-editor-colorpicker-pink = + .title = Rose +pdfjs-editor-colorpicker-red = + .title = Ros + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostre dut +pdfjs-editor-highlight-show-all-button = + .title = Mostre dut + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Modifiche test alternatîf (descrizion de imagjin) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Zonte test alternatîf (descrizion de imagjin) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Scrîf achì la tô descrizion… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Curte descrizion par personis che no rivin a viodi la imagjin, o che e ven mostrade cuant che no si rive a cjariâle. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Chest test alternatîf al è stât creât in automatic e al è pussibil che nol sedi cret. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Plui informazions +pdfjs-editor-new-alt-text-create-automatically-button-label = Cree test alternatîf in automatic +pdfjs-editor-new-alt-text-not-now-button = No cumò +pdfjs-editor-new-alt-text-error-title = Impussibil creâ test alternatîf in automatic +pdfjs-editor-new-alt-text-error-description = Scrîf il to test alternatîf o prove plui tart. +pdfjs-editor-new-alt-text-error-close-button = Siere +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Daûr a discjariâil model IA pal test alternatîf ({ $downloadedSize } di { $totalSize } MB) + .aria-valuetext = Daûr a discjariâ il model IA pal test alternatîf ({ $downloadedSize } di { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button-label = Test alternatîf zontât +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button-label = Al mancje il test alternatîf +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button-label = Verifiche test alternatîf +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creât in automatic: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Impostazions test alternatîf pes imagjins +pdfjs-image-alt-text-settings-button-label = Impostazions test alternatîf pes imagjins +pdfjs-editor-alt-text-settings-dialog-label = Impostazions test alternatîf pes imagjins +pdfjs-editor-alt-text-settings-automatic-title = Test alternatîf automatic +pdfjs-editor-alt-text-settings-create-model-button-label = Cree test alternatîf in automatic +pdfjs-editor-alt-text-settings-create-model-description = Al sugjerìs descrizions par judâ lis personis che no rivin a viodi la imagjin o cuant che la imagjin no ven cjariade. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model IA pal test alternatîf ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Al ven eseguît in locâl sul to dispositîf, cussì che i tiei dâts a restin riservâts. Al è necessari pe gjenerazion automatiche dal test alternatîf. +pdfjs-editor-alt-text-settings-delete-model-button = Elimine +pdfjs-editor-alt-text-settings-download-model-button = Discjame +pdfjs-editor-alt-text-settings-downloading-model-button = Daûr a discjariâ… +pdfjs-editor-alt-text-settings-editor-title = Modifiche test alternatîf +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostre l'editôr dal test alternatîf a pene che e ven zontade une imagjin +pdfjs-editor-alt-text-settings-show-dialog-description = Ti jude a sigurâti che dutis lis tôs imagjins a vedin il test alternatîf. +pdfjs-editor-alt-text-settings-close-button = Siere diff --git a/public/assets/pdfjs/locale/fy-NL/viewer.ftl b/public/assets/pdfjs/locale/fy-NL/viewer.ftl new file mode 100644 index 0000000..15850b4 --- /dev/null +++ b/public/assets/pdfjs/locale/fy-NL/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Foarige side +pdfjs-previous-button-label = Foarige +pdfjs-next-button = + .title = Folgjende side +pdfjs-next-button-label = Folgjende +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Side +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = fan { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } fan { $pagesCount }) +pdfjs-zoom-out-button = + .title = Utzoome +pdfjs-zoom-out-button-label = Utzoome +pdfjs-zoom-in-button = + .title = Ynzoome +pdfjs-zoom-in-button-label = Ynzoome +pdfjs-zoom-select = + .title = Zoome +pdfjs-presentation-mode-button = + .title = Wikselje nei presintaasjemodus +pdfjs-presentation-mode-button-label = Presintaasjemodus +pdfjs-open-file-button = + .title = Bestân iepenje +pdfjs-open-file-button-label = Iepenje +pdfjs-print-button = + .title = Ofdrukke +pdfjs-print-button-label = Ofdrukke +pdfjs-save-button = + .title = Bewarje +pdfjs-save-button-label = Bewarje +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Downloade +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Downloade +pdfjs-bookmark-button = + .title = Aktuele side (URL fan aktuele side besjen) +pdfjs-bookmark-button-label = Aktuele side + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ark +pdfjs-tools-button-label = Ark +pdfjs-first-page-button = + .title = Gean nei earste side +pdfjs-first-page-button-label = Gean nei earste side +pdfjs-last-page-button = + .title = Gean nei lêste side +pdfjs-last-page-button-label = Gean nei lêste side +pdfjs-page-rotate-cw-button = + .title = Rjochtsom draaie +pdfjs-page-rotate-cw-button-label = Rjochtsom draaie +pdfjs-page-rotate-ccw-button = + .title = Linksom draaie +pdfjs-page-rotate-ccw-button-label = Linksom draaie +pdfjs-cursor-text-select-tool-button = + .title = Tekstseleksjehelpmiddel ynskeakelje +pdfjs-cursor-text-select-tool-button-label = Tekstseleksjehelpmiddel +pdfjs-cursor-hand-tool-button = + .title = Hânhelpmiddel ynskeakelje +pdfjs-cursor-hand-tool-button-label = Hânhelpmiddel +pdfjs-scroll-page-button = + .title = Sideskowen brûke +pdfjs-scroll-page-button-label = Sideskowen +pdfjs-scroll-vertical-button = + .title = Fertikaal skowe brûke +pdfjs-scroll-vertical-button-label = Fertikaal skowe +pdfjs-scroll-horizontal-button = + .title = Horizontaal skowe brûke +pdfjs-scroll-horizontal-button-label = Horizontaal skowe +pdfjs-scroll-wrapped-button = + .title = Skowe mei oersjoch brûke +pdfjs-scroll-wrapped-button-label = Skowe mei oersjoch +pdfjs-spread-none-button = + .title = Sidesprieding net gearfetsje +pdfjs-spread-none-button-label = Gjin sprieding +pdfjs-spread-odd-button = + .title = Sidesprieding gearfetsje te starten mei ûneven nûmers +pdfjs-spread-odd-button-label = Uneven sprieding +pdfjs-spread-even-button = + .title = Sidesprieding gearfetsje te starten mei even nûmers +pdfjs-spread-even-button-label = Even sprieding + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokuminteigenskippen… +pdfjs-document-properties-button-label = Dokuminteigenskippen… +pdfjs-document-properties-file-name = Bestânsnamme: +pdfjs-document-properties-file-size = Bestânsgrutte: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Auteur: +pdfjs-document-properties-subject = Underwerp: +pdfjs-document-properties-keywords = Kaaiwurden: +pdfjs-document-properties-creation-date = Oanmaakdatum: +pdfjs-document-properties-modification-date = Bewurkingsdatum: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Makker: +pdfjs-document-properties-producer = PDF-makker: +pdfjs-document-properties-version = PDF-ferzje: +pdfjs-document-properties-page-count = Siden: +pdfjs-document-properties-page-size = Sideformaat: +pdfjs-document-properties-page-size-unit-inches = yn +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = steand +pdfjs-document-properties-page-size-orientation-landscape = lizzend +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Juridysk + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Flugge webwerjefte: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nee +pdfjs-document-properties-close-button = Slute + +## Print + +pdfjs-print-progress-message = Dokumint tariede oar ôfdrukken… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Annulearje +pdfjs-printing-not-supported = Warning: Printen is net folslein stipe troch dizze browser. +pdfjs-printing-not-ready = Warning: PDF is net folslein laden om ôf te drukken. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Sidebalke yn-/útskeakelje +pdfjs-toggle-sidebar-notification-button = + .title = Sidebalke yn-/útskeakelje (dokumint befettet oersjoch/bylagen/lagen) +pdfjs-toggle-sidebar-button-label = Sidebalke yn-/útskeakelje +pdfjs-document-outline-button = + .title = Dokumintoersjoch toane (dûbelklik om alle items út/yn te klappen) +pdfjs-document-outline-button-label = Dokumintoersjoch +pdfjs-attachments-button = + .title = Bylagen toane +pdfjs-attachments-button-label = Bylagen +pdfjs-layers-button = + .title = Lagen toane (dûbelklik om alle lagen nei de standertsteat werom te setten) +pdfjs-layers-button-label = Lagen +pdfjs-thumbs-button = + .title = Foarbylden toane +pdfjs-thumbs-button-label = Foarbylden +pdfjs-current-outline-item-button = + .title = Aktueel item yn ynhâldsopjefte sykje +pdfjs-current-outline-item-button-label = Aktueel item yn ynhâldsopjefte +pdfjs-findbar-button = + .title = Sykje yn dokumint +pdfjs-findbar-button-label = Sykje +pdfjs-additional-layers = Oanfoljende lagen + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Side { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Foarbyld fan side { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Sykje + .placeholder = Sykje yn dokumint… +pdfjs-find-previous-button = + .title = It foarige foarkommen fan de tekst sykje +pdfjs-find-previous-button-label = Foarige +pdfjs-find-next-button = + .title = It folgjende foarkommen fan de tekst sykje +pdfjs-find-next-button-label = Folgjende +pdfjs-find-highlight-checkbox = Alles markearje +pdfjs-find-match-case-checkbox-label = Haadlettergefoelich +pdfjs-find-match-diacritics-checkbox-label = Diakrityske tekens brûke +pdfjs-find-entire-word-checkbox-label = Hiele wurden +pdfjs-find-reached-top = Boppekant fan dokumint berikt, trochgien fan ûnder ôf +pdfjs-find-reached-bottom = Ein fan dokumint berikt, trochgien fan boppe ôf +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } fan { $total } oerienkomst + *[other] { $current } fan { $total } oerienkomsten + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mear as { $limit } oerienkomst + *[other] Mear as { $limit } oerienkomsten + } +pdfjs-find-not-found = Tekst net fûn + +## Predefined zoom values + +pdfjs-page-scale-width = Sidebreedte +pdfjs-page-scale-fit = Hiele side +pdfjs-page-scale-auto = Automatysk zoome +pdfjs-page-scale-actual = Werklike grutte +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Side { $page } + +## Loading indicator messages + +pdfjs-loading-error = Der is in flater bard by it laden fan de PDF. +pdfjs-invalid-file-error = Ynfalide of korruptearre PDF-bestân. +pdfjs-missing-file-error = PDF-bestân ûntbrekt. +pdfjs-unexpected-response-error = Unferwacht serverantwurd. +pdfjs-rendering-error = Der is in flater bard by it renderjen fan de side. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-annotaasje] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Jou it wachtwurd om dit PDF-bestân te iepenjen. +pdfjs-password-invalid = Ferkeard wachtwurd. Probearje opnij. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Annulearje +pdfjs-web-fonts-disabled = Weblettertypen binne útskeakele: gebrûk fan ynsluten PDF-lettertypen is net mooglik. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Tekenje +pdfjs-editor-ink-button-label = Tekenje +pdfjs-editor-stamp-button = + .title = Ofbyldingen tafoegje of bewurkje +pdfjs-editor-stamp-button-label = Ofbyldingen tafoegje of bewurkje +pdfjs-editor-highlight-button = + .title = Markearje +pdfjs-editor-highlight-button-label = Markearje +pdfjs-highlight-floating-button1 = + .title = Markearje + .aria-label = Markearje +pdfjs-highlight-floating-button-label = Markearje + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Tekening fuortsmite +pdfjs-editor-remove-freetext-button = + .title = Tekst fuortsmite +pdfjs-editor-remove-stamp-button = + .title = Ofbylding fuortsmite +pdfjs-editor-remove-highlight-button = + .title = Markearring fuortsmite + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Kleur +pdfjs-editor-free-text-size-input = Grutte +pdfjs-editor-ink-color-input = Kleur +pdfjs-editor-ink-thickness-input = Tsjokte +pdfjs-editor-ink-opacity-input = Transparânsje +pdfjs-editor-stamp-add-image-button = + .title = Ofbylding tafoegje +pdfjs-editor-stamp-add-image-button-label = Ofbylding tafoegje +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tsjokte +pdfjs-editor-free-highlight-thickness-title = + .title = Tsjokte wizigje by aksintuearring fan oare items as tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstbewurker + .default-content = Start mei typen… +pdfjs-free-text = + .aria-label = Tekstbewurker +pdfjs-free-text-default-content = Begjin mei typen… +pdfjs-ink = + .aria-label = Tekeningbewurker +pdfjs-ink-canvas = + .aria-label = Troch brûker makke ôfbylding + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternative tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternative tekst bewurkje +pdfjs-editor-alt-text-edit-button-label = Alternative tekst bewurkje +pdfjs-editor-alt-text-dialog-label = Kies in opsje +pdfjs-editor-alt-text-dialog-description = Alternative tekst helpt wannear’t minsken de ôfbylding net sjen kinne of wannear’t dizze net laden wurdt. +pdfjs-editor-alt-text-add-description-label = Foegje in beskriuwing ta +pdfjs-editor-alt-text-add-description-description = Stribje nei 1-2 sinnen dy’t it ûnderwerp, de omjouwing of de aksjes beskriuwe. +pdfjs-editor-alt-text-mark-decorative-label = As dekoratyf markearje +pdfjs-editor-alt-text-mark-decorative-description = Dit wurdt brûkt foar sierlike ôfbyldingen, lykas rânen of wettermerken. +pdfjs-editor-alt-text-cancel-button = Annulearje +pdfjs-editor-alt-text-save-button = Bewarje +pdfjs-editor-alt-text-decorative-tooltip = As dekoratyf markearre +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Bygelyks, ‘In jonge man sit oan in tafel om te iten’ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternative tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Linkerboppehoek – formaat wizigje +pdfjs-editor-resizer-label-top-middle = Midden boppe – formaat wizigje +pdfjs-editor-resizer-label-top-right = Rjochterboppehoek – formaat wizigje +pdfjs-editor-resizer-label-middle-right = Midden rjochts – formaat wizigje +pdfjs-editor-resizer-label-bottom-right = Rjochterûnderhoek – formaat wizigje +pdfjs-editor-resizer-label-bottom-middle = Midden ûnder – formaat wizigje +pdfjs-editor-resizer-label-bottom-left = Linkerûnderhoek – formaat wizigje +pdfjs-editor-resizer-label-middle-left = Links midden – formaat wizigje +pdfjs-editor-resizer-top-left = + .aria-label = Linkerboppehoek – formaat wizigje +pdfjs-editor-resizer-top-middle = + .aria-label = Midden boppe – formaat wizigje +pdfjs-editor-resizer-top-right = + .aria-label = Rjochterboppehoek – formaat wizigje +pdfjs-editor-resizer-middle-right = + .aria-label = Midden rjochts – formaat wizigje +pdfjs-editor-resizer-bottom-right = + .aria-label = Rjochterûnderhoek – formaat wizigje +pdfjs-editor-resizer-bottom-middle = + .aria-label = Midden ûnder – formaat wizigje +pdfjs-editor-resizer-bottom-left = + .aria-label = Linkerûnderhoek – formaat wizigje +pdfjs-editor-resizer-middle-left = + .aria-label = Links midden – formaat wizigje + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Markearringskleur +pdfjs-editor-colorpicker-button = + .title = Kleur wizigje +pdfjs-editor-colorpicker-dropdown = + .aria-label = Kleurkarren +pdfjs-editor-colorpicker-yellow = + .title = Giel +pdfjs-editor-colorpicker-green = + .title = Grien +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Roze +pdfjs-editor-colorpicker-red = + .title = Read + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Alles toane +pdfjs-editor-highlight-show-all-button = + .title = Alles toane + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternative tekst (ôfbyldingsbeskriuwing) bewurkje +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternative tekst (ôfbyldingsbeskriuwing) tafoegje +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skriuw hjir jo beskriuwing… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Koarte beskriuwing foar minsken dy’t de ôfbylding net sjen kinne of wannear’t de ôfbylding net laden wurdt. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Dizze alternative tekst is automatysk makke en is mooglik net korrekt. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Mear ynfo +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternative tekst automatysk oanmeitsje +pdfjs-editor-new-alt-text-not-now-button = No net +pdfjs-editor-new-alt-text-error-title = Kin alternative tekst net automatysk oanmeitsje +pdfjs-editor-new-alt-text-error-description = Skriuw jo eigen alternative tekst of probearje it letter nochris. +pdfjs-editor-new-alt-text-error-close-button = Slute +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = AI-model foar alternative tekst downloade ({ $downloadedSize } fan { $totalSize } MB) + .aria-valuetext = AI-model foar alternative tekst downloade ({ $downloadedSize } fan { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternative tekst tafoege +pdfjs-editor-new-alt-text-added-button-label = Alternative tekst tafoege +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Alternative tekst ûntbrekt +pdfjs-editor-new-alt-text-missing-button-label = Alternative tekst ûntbrekt +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternative tekst beoardiele +pdfjs-editor-new-alt-text-to-review-button-label = Alternative tekst beoardiele +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatysk oanmakke: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ynstellingen foar alternative tekst fan ôfbyldingen +pdfjs-image-alt-text-settings-button-label = Ynstellingen foar alternative tekst fan ôfbyldingen +pdfjs-editor-alt-text-settings-dialog-label = Ynstellingen foar alternative tekst fan ôfbyldingen +pdfjs-editor-alt-text-settings-automatic-title = Automatyske alternative tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Alternative tekst automatysk oanmeitsje +pdfjs-editor-alt-text-settings-create-model-description = Stelt beskriuwingen foar om minsken te helpen dy’t de ôfbylding net sjen kinne of foar wa’t de ôfbylding net laden wurdt. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = AI-model foar alternative tekst ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Wurdt lokaal op jo apparaat útfierd, sadat jo gegevens privee bliuwe. Fereaske foar automatyske alternative tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Fuortsmite +pdfjs-editor-alt-text-settings-download-model-button = Downloade +pdfjs-editor-alt-text-settings-downloading-model-button = Downloade… +pdfjs-editor-alt-text-settings-editor-title = Alternative-tekstbewurker +pdfjs-editor-alt-text-settings-show-dialog-button-label = Alternative-tekstbewurker daliks toane by tafoegjen fan in ôfbylding +pdfjs-editor-alt-text-settings-show-dialog-description = Helpt jo derfoar te soargjen dat al jo ôfbyldingen alternative tekst hawwe. +pdfjs-editor-alt-text-settings-close-button = Slute + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Markearring fuortsmiten +pdfjs-editor-undo-bar-message-freetext = Tekst fuortsmiten +pdfjs-editor-undo-bar-message-ink = Tekening fuortsmiten +pdfjs-editor-undo-bar-message-stamp = Ofbylding fuortsmiten +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotaasje fuortsmiten + *[other] { $count } annotaasjes fuortsmiten + } +pdfjs-editor-undo-bar-undo-button = + .title = Ungedien meitsje +pdfjs-editor-undo-bar-undo-button-label = Ungedien meitsje +pdfjs-editor-undo-bar-close-button = + .title = Slute +pdfjs-editor-undo-bar-close-button-label = Slute diff --git a/public/assets/pdfjs/locale/ga-IE/viewer.ftl b/public/assets/pdfjs/locale/ga-IE/viewer.ftl new file mode 100644 index 0000000..cb59308 --- /dev/null +++ b/public/assets/pdfjs/locale/ga-IE/viewer.ftl @@ -0,0 +1,213 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = An Leathanach Roimhe Seo +pdfjs-previous-button-label = Roimhe Seo +pdfjs-next-button = + .title = An Chéad Leathanach Eile +pdfjs-next-button-label = Ar Aghaidh +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Leathanach +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = as { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } as { $pagesCount }) +pdfjs-zoom-out-button = + .title = Súmáil Amach +pdfjs-zoom-out-button-label = Súmáil Amach +pdfjs-zoom-in-button = + .title = Súmáil Isteach +pdfjs-zoom-in-button-label = Súmáil Isteach +pdfjs-zoom-select = + .title = Súmáil +pdfjs-presentation-mode-button = + .title = Úsáid an Mód Láithreoireachta +pdfjs-presentation-mode-button-label = Mód Láithreoireachta +pdfjs-open-file-button = + .title = Oscail Comhad +pdfjs-open-file-button-label = Oscail +pdfjs-print-button = + .title = Priontáil +pdfjs-print-button-label = Priontáil + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Uirlisí +pdfjs-tools-button-label = Uirlisí +pdfjs-first-page-button = + .title = Go dtí an chéad leathanach +pdfjs-first-page-button-label = Go dtí an chéad leathanach +pdfjs-last-page-button = + .title = Go dtí an leathanach deiridh +pdfjs-last-page-button-label = Go dtí an leathanach deiridh +pdfjs-page-rotate-cw-button = + .title = Rothlaigh ar deiseal +pdfjs-page-rotate-cw-button-label = Rothlaigh ar deiseal +pdfjs-page-rotate-ccw-button = + .title = Rothlaigh ar tuathal +pdfjs-page-rotate-ccw-button-label = Rothlaigh ar tuathal +pdfjs-cursor-text-select-tool-button = + .title = Cumasaigh an Uirlis Roghnaithe Téacs +pdfjs-cursor-text-select-tool-button-label = Uirlis Roghnaithe Téacs +pdfjs-cursor-hand-tool-button = + .title = Cumasaigh an Uirlis Láimhe +pdfjs-cursor-hand-tool-button-label = Uirlis Láimhe + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Airíonna na Cáipéise… +pdfjs-document-properties-button-label = Airíonna na Cáipéise… +pdfjs-document-properties-file-name = Ainm an chomhaid: +pdfjs-document-properties-file-size = Méid an chomhaid: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } beart) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } beart) +pdfjs-document-properties-title = Teideal: +pdfjs-document-properties-author = Údar: +pdfjs-document-properties-subject = Ábhar: +pdfjs-document-properties-keywords = Eochairfhocail: +pdfjs-document-properties-creation-date = Dáta Cruthaithe: +pdfjs-document-properties-modification-date = Dáta Athraithe: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Cruthaitheoir: +pdfjs-document-properties-producer = Cruthaitheoir an PDF: +pdfjs-document-properties-version = Leagan PDF: +pdfjs-document-properties-page-count = Líon Leathanach: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = Dún + +## Print + +pdfjs-print-progress-message = Cáipéis á hullmhú le priontáil… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cealaigh +pdfjs-printing-not-supported = Rabhadh: Ní thacaíonn an brabhsálaí le priontáil go hiomlán. +pdfjs-printing-not-ready = Rabhadh: Ní féidir an PDF a phriontáil go dtí go mbeidh an cháipéis iomlán lódáilte. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Scoránaigh an Barra Taoibh +pdfjs-toggle-sidebar-button-label = Scoránaigh an Barra Taoibh +pdfjs-document-outline-button = + .title = Taispeáin Imlíne na Cáipéise (déchliceáil chun chuile rud a leathnú nó a laghdú) +pdfjs-document-outline-button-label = Creatlach na Cáipéise +pdfjs-attachments-button = + .title = Taispeáin Iatáin +pdfjs-attachments-button-label = Iatáin +pdfjs-thumbs-button = + .title = Taispeáin Mionsamhlacha +pdfjs-thumbs-button-label = Mionsamhlacha +pdfjs-findbar-button = + .title = Aimsigh sa Cháipéis +pdfjs-findbar-button-label = Aimsigh + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Leathanach { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Mionsamhail Leathanaigh { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Aimsigh + .placeholder = Aimsigh sa cháipéis… +pdfjs-find-previous-button = + .title = Aimsigh an sampla roimhe seo den nath seo +pdfjs-find-previous-button-label = Roimhe seo +pdfjs-find-next-button = + .title = Aimsigh an chéad sampla eile den nath sin +pdfjs-find-next-button-label = Ar aghaidh +pdfjs-find-highlight-checkbox = Aibhsigh uile +pdfjs-find-match-case-checkbox-label = Cásíogair +pdfjs-find-entire-word-checkbox-label = Focail iomlána +pdfjs-find-reached-top = Ag barr na cáipéise, ag leanúint ón mbun +pdfjs-find-reached-bottom = Ag bun na cáipéise, ag leanúint ón mbarr +pdfjs-find-not-found = Frása gan aimsiú + +## Predefined zoom values + +pdfjs-page-scale-width = Leithead Leathanaigh +pdfjs-page-scale-fit = Laghdaigh go dtí an Leathanach +pdfjs-page-scale-auto = Súmáil Uathoibríoch +pdfjs-page-scale-actual = Fíormhéid +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Tharla earráid agus an cháipéis PDF á lódáil. +pdfjs-invalid-file-error = Comhad neamhbhailí nó truaillithe PDF. +pdfjs-missing-file-error = Comhad PDF ar iarraidh. +pdfjs-unexpected-response-error = Freagra ón bhfreastalaí nach rabhthas ag súil leis. +pdfjs-rendering-error = Tharla earráid agus an leathanach á leagan amach. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anótáil { $type }] + +## Password + +pdfjs-password-label = Cuir an focal faire isteach chun an comhad PDF seo a oscailt. +pdfjs-password-invalid = Focal faire mícheart. Déan iarracht eile. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cealaigh +pdfjs-web-fonts-disabled = Tá clófhoirne Gréasáin díchumasaithe: ní féidir clófhoirne leabaithe PDF a úsáid. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/gd/viewer.ftl b/public/assets/pdfjs/locale/gd/viewer.ftl new file mode 100644 index 0000000..a3d62a0 --- /dev/null +++ b/public/assets/pdfjs/locale/gd/viewer.ftl @@ -0,0 +1,313 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = An duilleag roimhe +pdfjs-previous-button-label = Air ais +pdfjs-next-button = + .title = An ath-dhuilleag +pdfjs-next-button-label = Air adhart +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Duilleag +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = à { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } à { $pagesCount }) +pdfjs-zoom-out-button = + .title = Sùm a-mach +pdfjs-zoom-out-button-label = Sùm a-mach +pdfjs-zoom-in-button = + .title = Sùm a-steach +pdfjs-zoom-in-button-label = Sùm a-steach +pdfjs-zoom-select = + .title = Sùm +pdfjs-presentation-mode-button = + .title = Gearr leum dhan mhodh taisbeanaidh +pdfjs-presentation-mode-button-label = Am modh taisbeanaidh +pdfjs-open-file-button = + .title = Fosgail faidhle +pdfjs-open-file-button-label = Fosgail +pdfjs-print-button = + .title = Clò-bhuail +pdfjs-print-button-label = Clò-bhuail +pdfjs-save-button = + .title = Sàbhail +pdfjs-save-button-label = Sàbhail +pdfjs-bookmark-button = + .title = An duilleag làithreach (Seall an URL on duilleag làithreach) +pdfjs-bookmark-button-label = An duilleag làithreach + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Innealan +pdfjs-tools-button-label = Innealan +pdfjs-first-page-button = + .title = Rach gun chiad duilleag +pdfjs-first-page-button-label = Rach gun chiad duilleag +pdfjs-last-page-button = + .title = Rach gun duilleag mu dheireadh +pdfjs-last-page-button-label = Rach gun duilleag mu dheireadh +pdfjs-page-rotate-cw-button = + .title = Cuairtich gu deiseil +pdfjs-page-rotate-cw-button-label = Cuairtich gu deiseil +pdfjs-page-rotate-ccw-button = + .title = Cuairtich gu tuathail +pdfjs-page-rotate-ccw-button-label = Cuairtich gu tuathail +pdfjs-cursor-text-select-tool-button = + .title = Cuir an comas inneal taghadh an teacsa +pdfjs-cursor-text-select-tool-button-label = Inneal taghadh an teacsa +pdfjs-cursor-hand-tool-button = + .title = Cuir inneal na làimhe an comas +pdfjs-cursor-hand-tool-button-label = Inneal na làimhe +pdfjs-scroll-page-button = + .title = Cleachd sgroladh duilleige +pdfjs-scroll-page-button-label = Sgroladh duilleige +pdfjs-scroll-vertical-button = + .title = Cleachd sgroladh inghearach +pdfjs-scroll-vertical-button-label = Sgroladh inghearach +pdfjs-scroll-horizontal-button = + .title = Cleachd sgroladh còmhnard +pdfjs-scroll-horizontal-button-label = Sgroladh còmhnard +pdfjs-scroll-wrapped-button = + .title = Cleachd sgroladh paisgte +pdfjs-scroll-wrapped-button-label = Sgroladh paisgte +pdfjs-spread-none-button = + .title = Na cuir còmhla sgoileadh dhuilleagan +pdfjs-spread-none-button-label = Gun sgaoileadh dhuilleagan +pdfjs-spread-odd-button = + .title = Cuir còmhla duilleagan sgaoilte a thòisicheas le duilleagan aig a bheil àireamh chorr +pdfjs-spread-odd-button-label = Sgaoileadh dhuilleagan corra +pdfjs-spread-even-button = + .title = Cuir còmhla duilleagan sgaoilte a thòisicheas le duilleagan aig a bheil àireamh chothrom +pdfjs-spread-even-button-label = Sgaoileadh dhuilleagan cothrom + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Roghainnean na sgrìobhainne… +pdfjs-document-properties-button-label = Roghainnean na sgrìobhainne… +pdfjs-document-properties-file-name = Ainm an fhaidhle: +pdfjs-document-properties-file-size = Meud an fhaidhle: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Tiotal: +pdfjs-document-properties-author = Ùghdar: +pdfjs-document-properties-subject = Cuspair: +pdfjs-document-properties-keywords = Faclan-luirg: +pdfjs-document-properties-creation-date = Latha a chruthachaidh: +pdfjs-document-properties-modification-date = Latha atharrachaidh: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Cruthadair: +pdfjs-document-properties-producer = Saothraiche a' PDF: +pdfjs-document-properties-version = Tionndadh a' PDF: +pdfjs-document-properties-page-count = Àireamh de dhuilleagan: +pdfjs-document-properties-page-size = Meud na duilleige: +pdfjs-document-properties-page-size-unit-inches = ann an +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portraid +pdfjs-document-properties-page-size-orientation-landscape = dreach-tìre +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Litir +pdfjs-document-properties-page-size-name-legal = Laghail + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Grad shealladh-lìn: +pdfjs-document-properties-linearized-yes = Tha +pdfjs-document-properties-linearized-no = Chan eil +pdfjs-document-properties-close-button = Dùin + +## Print + +pdfjs-print-progress-message = Ag ullachadh na sgrìobhainn airson clò-bhualadh… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Sguir dheth +pdfjs-printing-not-supported = Rabhadh: Chan eil am brabhsair seo a' cur làn-taic ri clò-bhualadh. +pdfjs-printing-not-ready = Rabhadh: Cha deach am PDF a luchdadh gu tur airson clò-bhualadh. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toglaich am bàr-taoibh +pdfjs-toggle-sidebar-notification-button = + .title = Toglaich am bàr-taoibh (tha oir-loidhne/ceanglachain/breathan aig an sgrìobhainn) +pdfjs-toggle-sidebar-button-label = Toglaich am bàr-taoibh +pdfjs-document-outline-button = + .title = Seall oir-loidhne na sgrìobhainn (dèan briogadh dùbailte airson a h-uile nì a leudachadh/a cho-theannadh) +pdfjs-document-outline-button-label = Oir-loidhne na sgrìobhainne +pdfjs-attachments-button = + .title = Seall na ceanglachain +pdfjs-attachments-button-label = Ceanglachain +pdfjs-layers-button = + .title = Seall na breathan (dèan briogadh dùbailte airson a h-uile breath ath-shuidheachadh dhan staid bhunaiteach) +pdfjs-layers-button-label = Breathan +pdfjs-thumbs-button = + .title = Seall na dealbhagan +pdfjs-thumbs-button-label = Dealbhagan +pdfjs-current-outline-item-button = + .title = Lorg nì làithreach na h-oir-loidhne +pdfjs-current-outline-item-button-label = Nì làithreach na h-oir-loidhne +pdfjs-findbar-button = + .title = Lorg san sgrìobhainn +pdfjs-findbar-button-label = Lorg +pdfjs-additional-layers = Barrachd breathan + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Duilleag a { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Dealbhag duilleag a { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Lorg + .placeholder = Lorg san sgrìobhainn... +pdfjs-find-previous-button = + .title = Lorg làthair roimhe na h-abairt seo +pdfjs-find-previous-button-label = Air ais +pdfjs-find-next-button = + .title = Lorg ath-làthair na h-abairt seo +pdfjs-find-next-button-label = Air adhart +pdfjs-find-highlight-checkbox = Soillsich a h-uile +pdfjs-find-match-case-checkbox-label = Aire do litrichean mòra is beaga +pdfjs-find-match-diacritics-checkbox-label = Aire do stràcan +pdfjs-find-entire-word-checkbox-label = Faclan-slàna +pdfjs-find-reached-top = Ràinig sinn barr na duilleige, a' leantainn air adhart o bhonn na duilleige +pdfjs-find-reached-bottom = Ràinig sinn bonn na duilleige, a' leantainn air adhart o bharr na duilleige +pdfjs-find-not-found = Cha deach an abairt a lorg + +## Predefined zoom values + +pdfjs-page-scale-width = Leud na duilleige +pdfjs-page-scale-fit = Freagair ri meud na duilleige +pdfjs-page-scale-auto = Sùm fèin-obrachail +pdfjs-page-scale-actual = Am fìor-mheud +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Duilleag { $page } + +## Loading indicator messages + +pdfjs-loading-error = Thachair mearachd rè luchdadh a' PDF. +pdfjs-invalid-file-error = Faidhle PDF a tha mì-dhligheach no coirbte. +pdfjs-missing-file-error = Faidhle PDF a tha a dhìth. +pdfjs-unexpected-response-error = Freagairt on fhrithealaiche ris nach robh dùil. +pdfjs-rendering-error = Thachair mearachd rè reandaradh na duilleige. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Nòtachadh { $type }] + +## Password + +pdfjs-password-label = Cuir a-steach am facal-faire gus am faidhle PDF seo fhosgladh. +pdfjs-password-invalid = Tha am facal-faire cearr. Nach fheuch thu ris a-rithist? +pdfjs-password-ok-button = Ceart ma-thà +pdfjs-password-cancel-button = Sguir dheth +pdfjs-web-fonts-disabled = Tha cruthan-clò lìn à comas: Chan urrainn dhuinn cruthan-clò PDF leabaichte a chleachdadh. + +## Editing + +pdfjs-editor-free-text-button = + .title = Teacsa +pdfjs-editor-free-text-button-label = Teacsa +pdfjs-editor-ink-button = + .title = Tarraing +pdfjs-editor-ink-button-label = Tarraing + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Dath +pdfjs-editor-free-text-size-input = Meud +pdfjs-editor-ink-color-input = Dath +pdfjs-editor-ink-thickness-input = Tighead +pdfjs-editor-ink-opacity-input = Trìd-dhoilleireachd +pdfjs-free-text = + .aria-label = An deasaiche teacsa +pdfjs-free-text-default-content = Tòisich air sgrìobhadh… +pdfjs-ink = + .aria-label = An deasaiche tharraingean +pdfjs-ink-canvas = + .aria-label = Dealbh a chruthaich cleachdaiche + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/gl/viewer.ftl b/public/assets/pdfjs/locale/gl/viewer.ftl new file mode 100644 index 0000000..641a607 --- /dev/null +++ b/public/assets/pdfjs/locale/gl/viewer.ftl @@ -0,0 +1,385 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Páxina anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Seguinte páxina +pdfjs-next-button-label = Seguinte +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Páxina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Reducir +pdfjs-zoom-out-button-label = Reducir +pdfjs-zoom-in-button = + .title = Ampliar +pdfjs-zoom-in-button-label = Ampliar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Cambiar ao modo presentación +pdfjs-presentation-mode-button-label = Modo presentación +pdfjs-open-file-button = + .title = Abrir ficheiro +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Gardar +pdfjs-save-button-label = Gardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Descargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Descargar +pdfjs-bookmark-button = + .title = Páxina actual (ver o URL da páxina actual) +pdfjs-bookmark-button-label = Páxina actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ferramentas +pdfjs-tools-button-label = Ferramentas +pdfjs-first-page-button = + .title = Ir á primeira páxina +pdfjs-first-page-button-label = Ir á primeira páxina +pdfjs-last-page-button = + .title = Ir á última páxina +pdfjs-last-page-button-label = Ir á última páxina +pdfjs-page-rotate-cw-button = + .title = Rotar no sentido das agullas do reloxo +pdfjs-page-rotate-cw-button-label = Rotar no sentido das agullas do reloxo +pdfjs-page-rotate-ccw-button = + .title = Rotar no sentido contrario ás agullas do reloxo +pdfjs-page-rotate-ccw-button-label = Rotar no sentido contrario ás agullas do reloxo +pdfjs-cursor-text-select-tool-button = + .title = Activar a ferramenta de selección de texto +pdfjs-cursor-text-select-tool-button-label = Ferramenta de selección de texto +pdfjs-cursor-hand-tool-button = + .title = Activar a ferramenta de man +pdfjs-cursor-hand-tool-button-label = Ferramenta de man +pdfjs-scroll-page-button = + .title = Usar o desprazamento da páxina +pdfjs-scroll-page-button-label = Desprazamento da páxina +pdfjs-scroll-vertical-button = + .title = Usar o desprazamento vertical +pdfjs-scroll-vertical-button-label = Desprazamento vertical +pdfjs-scroll-horizontal-button = + .title = Usar o desprazamento horizontal +pdfjs-scroll-horizontal-button-label = Desprazamento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar o desprazamento en bloque +pdfjs-scroll-wrapped-button-label = Desprazamento por bloque +pdfjs-spread-none-button = + .title = Non agrupar páxinas +pdfjs-spread-none-button-label = Ningún agrupamento +pdfjs-spread-odd-button = + .title = Crea grupo de páxinas que comezan con números de páxina impares +pdfjs-spread-odd-button-label = Agrupamento impar +pdfjs-spread-even-button = + .title = Crea grupo de páxinas que comezan con números de páxina pares +pdfjs-spread-even-button-label = Agrupamento par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades do documento… +pdfjs-document-properties-button-label = Propiedades do documento… +pdfjs-document-properties-file-name = Nome do ficheiro: +pdfjs-document-properties-file-size = Tamaño do ficheiro: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Asunto: +pdfjs-document-properties-keywords = Palabras clave: +pdfjs-document-properties-creation-date = Data de creación: +pdfjs-document-properties-modification-date = Data de modificación: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creado por: +pdfjs-document-properties-producer = Xenerador do PDF: +pdfjs-document-properties-version = Versión de PDF: +pdfjs-document-properties-page-count = Número de páxinas: +pdfjs-document-properties-page-size = Tamaño da páxina: +pdfjs-document-properties-page-size-unit-inches = pol +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Visualización rápida das páxinas web: +pdfjs-document-properties-linearized-yes = Si +pdfjs-document-properties-linearized-no = Non +pdfjs-document-properties-close-button = Pechar + +## Print + +pdfjs-print-progress-message = Preparando o documento para imprimir… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Aviso: A impresión non é compatíbel de todo con este navegador. +pdfjs-printing-not-ready = Aviso: O PDF non se cargou completamente para imprimirse. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Amosar/agochar a barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Alternar barra lateral (o documento contén esquema/anexos/capas) +pdfjs-toggle-sidebar-button-label = Amosar/agochar a barra lateral +pdfjs-document-outline-button = + .title = Amosar a estrutura do documento (dobre clic para expandir/contraer todos os elementos) +pdfjs-document-outline-button-label = Estrutura do documento +pdfjs-attachments-button = + .title = Amosar anexos +pdfjs-attachments-button-label = Anexos +pdfjs-layers-button = + .title = Mostrar capas (prema dúas veces para restaurar todas as capas o estado predeterminado) +pdfjs-layers-button-label = Capas +pdfjs-thumbs-button = + .title = Amosar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Atopar o elemento delimitado actualmente +pdfjs-current-outline-item-button-label = Elemento delimitado actualmente +pdfjs-findbar-button = + .title = Atopar no documento +pdfjs-findbar-button-label = Atopar +pdfjs-additional-layers = Capas adicionais + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Páxina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura da páxina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Atopar + .placeholder = Atopar no documento… +pdfjs-find-previous-button = + .title = Atopar a anterior aparición da frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Atopar a seguinte aparición da frase +pdfjs-find-next-button-label = Seguinte +pdfjs-find-highlight-checkbox = Realzar todo +pdfjs-find-match-case-checkbox-label = Diferenciar maiúsculas de minúsculas +pdfjs-find-match-diacritics-checkbox-label = Distinguir os diacríticos +pdfjs-find-entire-word-checkbox-label = Palabras completas +pdfjs-find-reached-top = Chegouse ao inicio do documento, continuar desde o final +pdfjs-find-reached-bottom = Chegouse ao final do documento, continuar desde o inicio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Coincidencia { $current } de { $total } + *[other] Coincidencia { $current } de { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Máis de { $limit } coincidencia + *[other] Máis de { $limit } coincidencias + } +pdfjs-find-not-found = Non se atopou a frase + +## Predefined zoom values + +pdfjs-page-scale-width = Largura da páxina +pdfjs-page-scale-fit = Axuste de páxina +pdfjs-page-scale-auto = Zoom automático +pdfjs-page-scale-actual = Tamaño actual +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Páxina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Produciuse un erro ao cargar o PDF. +pdfjs-invalid-file-error = Ficheiro PDF danado ou non válido. +pdfjs-missing-file-error = Falta o ficheiro PDF. +pdfjs-unexpected-response-error = Resposta inesperada do servidor. +pdfjs-rendering-error = Produciuse un erro ao representar a páxina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotación { $type }] + +## Password + +pdfjs-password-label = Escriba o contrasinal para abrir este ficheiro PDF. +pdfjs-password-invalid = Contrasinal incorrecto. Tente de novo. +pdfjs-password-ok-button = Aceptar +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Desactiváronse as fontes web: foi imposíbel usar as fontes incrustadas no PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Debuxo +pdfjs-editor-ink-button-label = Debuxo +pdfjs-editor-stamp-button = + .title = Engadir ou editar imaxes +pdfjs-editor-stamp-button-label = Engadir ou editar imaxes + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-freetext-button = + .title = Eliminar o texto +pdfjs-editor-remove-stamp-button = + .title = Eliminar a imaxe +pdfjs-editor-remove-highlight-button = + .title = Eliminar o resaltado + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Cor +pdfjs-editor-free-text-size-input = Tamaño +pdfjs-editor-ink-color-input = Cor +pdfjs-editor-ink-thickness-input = Grosor +pdfjs-editor-ink-opacity-input = Opacidade +pdfjs-editor-stamp-add-image-button = + .title = Engadir imaxe +pdfjs-editor-stamp-add-image-button-label = Engadir imaxe +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grosor +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Comezar a teclear… +pdfjs-ink = + .aria-label = Editor de debuxos +pdfjs-ink-canvas = + .aria-label = Imaxe creada por unha usuaria + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar o texto alternativo +pdfjs-editor-alt-text-dialog-label = Escoller unha opción +pdfjs-editor-alt-text-add-description-label = Engadir unha descrición +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativo +pdfjs-editor-alt-text-mark-decorative-description = Utilízase para imaxes ornamentais, como bordos ou marcas de auga. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Gardar +pdfjs-editor-alt-text-decorative-tooltip = Marcado como decorativo +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por exemplo, «Un mozo séntase á mesa para comer» + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Esquina superior esquerda: cambia o tamaño +pdfjs-editor-resizer-label-top-middle = Medio superior: cambia o tamaño +pdfjs-editor-resizer-label-top-right = Esquina superior dereita: cambia o tamaño +pdfjs-editor-resizer-label-middle-right = Medio dereito: cambia o tamaño +pdfjs-editor-resizer-label-bottom-right = Esquina inferior dereita: cambia o tamaño +pdfjs-editor-resizer-label-bottom-middle = Abaixo medio: cambia o tamaño +pdfjs-editor-resizer-label-bottom-left = Esquina inferior esquerda: cambia o tamaño +pdfjs-editor-resizer-label-middle-left = Medio esquerdo: cambia o tamaño +pdfjs-editor-resizer-top-left = + .aria-label = Esquina superior esquerda: cambia o tamaño +pdfjs-editor-resizer-top-middle = + .aria-label = Medio superior: cambia o tamaño +pdfjs-editor-resizer-top-right = + .aria-label = Esquina superior dereita: cambia o tamaño +pdfjs-editor-resizer-middle-right = + .aria-label = Medio dereito: cambia o tamaño +pdfjs-editor-resizer-bottom-right = + .aria-label = Esquina inferior dereita: cambia o tamaño +pdfjs-editor-resizer-bottom-middle = + .aria-label = Abaixo medio: cambia o tamaño +pdfjs-editor-resizer-bottom-left = + .aria-label = Esquina inferior esquerda: cambia o tamaño +pdfjs-editor-resizer-middle-left = + .aria-label = Medio esquerdo: cambia o tamaño + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/gn/viewer.ftl b/public/assets/pdfjs/locale/gn/viewer.ftl new file mode 100644 index 0000000..6402c6f --- /dev/null +++ b/public/assets/pdfjs/locale/gn/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Kuatiarogue mboyvegua +pdfjs-previous-button-label = Mboyvegua +pdfjs-next-button = + .title = Kuatiarogue upeigua +pdfjs-next-button-label = Upeigua +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Kuatiarogue +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } gui +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Momichĩ +pdfjs-zoom-out-button-label = Momichĩ +pdfjs-zoom-in-button = + .title = Mbotuicha +pdfjs-zoom-in-button-label = Mbotuicha +pdfjs-zoom-select = + .title = Tuichakue +pdfjs-presentation-mode-button = + .title = Jehechauka reko moambue +pdfjs-presentation-mode-button-label = Jehechauka reko +pdfjs-open-file-button = + .title = Marandurendápe jeike +pdfjs-open-file-button-label = Jeike +pdfjs-print-button = + .title = Monguatia +pdfjs-print-button-label = Monguatia +pdfjs-save-button = + .title = Ñongatu +pdfjs-save-button-label = Ñongatu +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Mboguejy +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Mboguejy +pdfjs-bookmark-button = + .title = Kuatiarogue ag̃agua (Ehecha URL kuatiarogue ag̃agua) +pdfjs-bookmark-button-label = Kuatiarogue Ag̃agua + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tembiporu +pdfjs-tools-button-label = Tembiporu +pdfjs-first-page-button = + .title = Kuatiarogue ñepyrũme jeho +pdfjs-first-page-button-label = Kuatiarogue ñepyrũme jeho +pdfjs-last-page-button = + .title = Kuatiarogue pahápe jeho +pdfjs-last-page-button-label = Kuatiarogue pahápe jeho +pdfjs-page-rotate-cw-button = + .title = Aravóicha mbojere +pdfjs-page-rotate-cw-button-label = Aravóicha mbojere +pdfjs-page-rotate-ccw-button = + .title = Aravo rapykue gotyo mbojere +pdfjs-page-rotate-ccw-button-label = Aravo rapykue gotyo mbojere +pdfjs-cursor-text-select-tool-button = + .title = Emyandy moñe’ẽrã jeporavo rembiporu +pdfjs-cursor-text-select-tool-button-label = Moñe’ẽrã jeporavo rembiporu +pdfjs-cursor-hand-tool-button = + .title = Tembiporu po pegua myandy +pdfjs-cursor-hand-tool-button-label = Tembiporu po pegua +pdfjs-scroll-page-button = + .title = Eiporu kuatiarogue jeku’e +pdfjs-scroll-page-button-label = Kuatiarogue jeku’e +pdfjs-scroll-vertical-button = + .title = Eiporu jeku’e ykeguáva +pdfjs-scroll-vertical-button-label = Jeku’e ykeguáva +pdfjs-scroll-horizontal-button = + .title = Eiporu jeku’e yvate gotyo +pdfjs-scroll-horizontal-button-label = Jeku’e yvate gotyo +pdfjs-scroll-wrapped-button = + .title = Eiporu jeku’e mbohyrupyre +pdfjs-scroll-wrapped-button-label = Jeku’e mbohyrupyre +pdfjs-spread-none-button = + .title = Ani ejuaju spreads kuatiarogue ndive +pdfjs-spread-none-button-label = Spreads ỹre +pdfjs-spread-odd-button = + .title = Embojuaju kuatiarogue jepysokue eñepyrũvo kuatiarogue impar-vagui +pdfjs-spread-odd-button-label = Spreads impar +pdfjs-spread-even-button = + .title = Embojuaju kuatiarogue jepysokue eñepyrũvo kuatiarogue par-vagui +pdfjs-spread-even-button-label = Ipukuve uvei + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Kuatia mba’etee… +pdfjs-document-properties-button-label = Kuatia mba’etee… +pdfjs-document-properties-file-name = Marandurenda réra: +pdfjs-document-properties-file-size = Marandurenda tuichakue: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Teratee: +pdfjs-document-properties-author = Apohára: +pdfjs-document-properties-subject = Mba’egua: +pdfjs-document-properties-keywords = Jehero: +pdfjs-document-properties-creation-date = Teñoihague arange: +pdfjs-document-properties-modification-date = Iñambue hague arange: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Apo’ypyha: +pdfjs-document-properties-producer = PDF mbosako’iha: +pdfjs-document-properties-version = PDF mbojuehegua: +pdfjs-document-properties-page-count = Kuatiarogue papapy: +pdfjs-document-properties-page-size = Kuatiarogue tuichakue: +pdfjs-document-properties-page-size-unit-inches = Amo +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = Oĩháicha +pdfjs-document-properties-page-size-orientation-landscape = apaisado +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Kuatiañe’ẽ +pdfjs-document-properties-page-size-name-legal = Tee + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Ñanduti jahecha pya’e: +pdfjs-document-properties-linearized-yes = Añete +pdfjs-document-properties-linearized-no = Ahániri +pdfjs-document-properties-close-button = Mboty + +## Print + +pdfjs-print-progress-message = Embosako’i kuatia emonguatia hag̃ua… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Heja +pdfjs-printing-not-supported = Kyhyjerã: Ñembokuatia ndojokupytypái ko kundahára ndive. +pdfjs-printing-not-ready = Kyhyjerã: Ko PDF nahenyhẽmbái oñembokuatia hag̃uáicha. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Tenda yke moambue +pdfjs-toggle-sidebar-notification-button = + .title = Embojopyru tenda ykegua (kuatia oguereko kuaakaha/moirũha/ñuãha) +pdfjs-toggle-sidebar-button-label = Tenda yke moambue +pdfjs-document-outline-button = + .title = Ehechauka kuatia rape (eikutu mokõi jey embotuicha/emomichĩ hag̃ua opavavete mba’eporu) +pdfjs-document-outline-button-label = Kuatia apopyre +pdfjs-attachments-button = + .title = Moirũha jehechauka +pdfjs-attachments-button-label = Moirũha +pdfjs-layers-button = + .title = Ehechauka ñuãha (eikutu jo’a emomba’apo hag̃ua opaite ñuãha tekoypýpe) +pdfjs-layers-button-label = Ñuãha +pdfjs-thumbs-button = + .title = Mba’emirĩ jehechauka +pdfjs-thumbs-button-label = Mba’emirĩ +pdfjs-current-outline-item-button = + .title = Eheka mba’eporu ag̃aguaitéva +pdfjs-current-outline-item-button-label = Mba’eporu ag̃aguaitéva +pdfjs-findbar-button = + .title = Kuatiápe jeheka +pdfjs-findbar-button-label = Juhu +pdfjs-additional-layers = Ñuãha moirũguáva + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Kuatiarogue { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Kuatiarogue mba’emirĩ { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Juhu + .placeholder = Kuatiápe jejuhu… +pdfjs-find-previous-button = + .title = Ejuhu ñe’ẽrysýi osẽ’ypy hague +pdfjs-find-previous-button-label = Mboyvegua +pdfjs-find-next-button = + .title = Eho ñe’ẽ juhupyre upeiguávape +pdfjs-find-next-button-label = Upeigua +pdfjs-find-highlight-checkbox = Embojekuaavepa +pdfjs-find-match-case-checkbox-label = Ejesareko taiguasu/taimichĩre +pdfjs-find-match-diacritics-checkbox-label = Diacrítico moñondive +pdfjs-find-entire-word-checkbox-label = Ñe’ẽ oĩmbáva +pdfjs-find-reached-top = Ojehupyty kuatia ñepyrũ, oku’ejeýta kuatia paha guive +pdfjs-find-reached-bottom = Ojehupyty kuatia paha, oku’ejeýta kuatia ñepyrũ guive +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } ha { $total } ojueheguáva + *[other] { $current } ha { $total } ojueheguáva + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Hetave { $limit } ojueheguáva + *[other] Hetave { $limit } ojueheguáva + } +pdfjs-find-not-found = Ñe’ẽrysýi ojejuhu’ỹva + +## Predefined zoom values + +pdfjs-page-scale-width = Kuatiarogue pekue +pdfjs-page-scale-fit = Kuatiarogue ñemoĩporã +pdfjs-page-scale-auto = Tuichakue ijeheguíva +pdfjs-page-scale-actual = Tuichakue ag̃agua +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Kuatiarogue { $page } + +## Loading indicator messages + +pdfjs-loading-error = Oiko jejavy PDF oñemyeñyhẽnguévo. +pdfjs-invalid-file-error = PDF marandurenda ndoikóiva térã ivaipyréva. +pdfjs-missing-file-error = Ndaipóri PDF marandurenda +pdfjs-unexpected-response-error = Mohendahavusu mbohovái eha’ãrõ’ỹva. +pdfjs-rendering-error = Oiko jejavy ehechaukasévo kuatiarogue. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Jehaipy { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Emoinge ñe’ẽñemi eipe’a hag̃ua ko marandurenda PDF. +pdfjs-password-invalid = Ñe’ẽñemi ndoikóiva. Eha’ã jey. +pdfjs-password-ok-button = MONEĨ +pdfjs-password-cancel-button = Heja +pdfjs-web-fonts-disabled = Ñanduti taity oñemongéma: ndaikatumo’ãi eiporu PDF jehai’íva taity. + +## Editing + +pdfjs-editor-free-text-button = + .title = Moñe’ẽrã +pdfjs-editor-free-text-button-label = Moñe’ẽrã +pdfjs-editor-ink-button = + .title = Moha’ãnga +pdfjs-editor-ink-button-label = Moha’ãnga +pdfjs-editor-stamp-button = + .title = Embojuaju térã embosako’i ta’ãnga +pdfjs-editor-stamp-button-label = Embojuaju térã embosako’i ta’ãnga +pdfjs-editor-highlight-button = + .title = Mbosa’y +pdfjs-editor-highlight-button-label = Mbosa’y +pdfjs-highlight-floating-button1 = + .title = Mbosa’y + .aria-label = Mbosa’y +pdfjs-highlight-floating-button-label = Mbosa’y + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Emboguete ta’ãnga +pdfjs-editor-remove-freetext-button = + .title = Emboguete moñe’ẽrã +pdfjs-editor-remove-stamp-button = + .title = Emboguete ta’ãnga +pdfjs-editor-remove-highlight-button = + .title = Eipe’a jehechaveha + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Sa’y +pdfjs-editor-free-text-size-input = Tuichakue +pdfjs-editor-ink-color-input = Sa’y +pdfjs-editor-ink-thickness-input = Anambusu +pdfjs-editor-ink-opacity-input = Pytũngy +pdfjs-editor-stamp-add-image-button = + .title = Embojuaju ta’ãnga +pdfjs-editor-stamp-add-image-button-label = Embojuaju ta’ãnga +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Anambusu +pdfjs-editor-free-highlight-thickness-title = + .title = Emoambue anambusukue embosa’ývo mba’eporu ha’e’ỹva moñe’ẽrã +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Moñe’ẽrã moheñoiha + .default-content = Eñepyrũ ehai… +pdfjs-free-text = + .aria-label = Moñe’ẽrã moheñoiha +pdfjs-free-text-default-content = Ehai ñepyrũ… +pdfjs-ink = + .aria-label = Ta’ãnga moheñoiha +pdfjs-ink-canvas = + .aria-label = Ta’ãnga omoheñóiva poruhára + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Moñe’ẽrã mokõiháva +pdfjs-editor-alt-text-edit-button = + .aria-label = Embojuruja moñe’ẽrã mokõiháva +pdfjs-editor-alt-text-edit-button-label = Embojuruja moñe’ẽrã mokõiháva +pdfjs-editor-alt-text-dialog-label = Eiporavo poravorã +pdfjs-editor-alt-text-dialog-description = Moñe’ẽrã ykepegua (moñe’ẽrã ykepegua) nepytyvõ nderehecháiramo ta’ãnga térã nahenyhẽiramo. +pdfjs-editor-alt-text-add-description-label = Embojuaju ñemoha’ãnga +pdfjs-editor-alt-text-add-description-description = Ehaimi 1 térã 2 ñe’ẽjuaju oñe’ẽva pe téma rehe, ijere térã mba’eapóre. +pdfjs-editor-alt-text-mark-decorative-label = Emongurusu jeguakárõ +pdfjs-editor-alt-text-mark-decorative-description = Ojeporu ta’ãnga jeguakarã, tembe’y térã ta’ãnga ruguarãramo. +pdfjs-editor-alt-text-cancel-button = Heja +pdfjs-editor-alt-text-save-button = Ñongatu +pdfjs-editor-alt-text-decorative-tooltip = Jeguakárõ mongurusupyre +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Techapyrã: “Peteĩ mitãrusu oguapy mesápe okaru hag̃ua” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Moñe’ẽrã mokõiháva + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Yvate asu gotyo — emoambue tuichakue +pdfjs-editor-resizer-label-top-middle = Yvate mbytépe — emoambue tuichakue +pdfjs-editor-resizer-label-top-right = Yvate akatúape — emoambue tuichakue +pdfjs-editor-resizer-label-middle-right = Mbyte akatúape — emoambue tuichakue +pdfjs-editor-resizer-label-bottom-right = Yvy gotyo akatúape — emoambue tuichakue +pdfjs-editor-resizer-label-bottom-middle = Yvy gotyo mbytépe — emoambue tuichakue +pdfjs-editor-resizer-label-bottom-left = Iguýpe asu gotyo — emoambue tuichakue +pdfjs-editor-resizer-label-middle-left = Mbyte asu gotyo — emoambue tuichakue +pdfjs-editor-resizer-top-left = + .aria-label = Yvate asu gotyo — emoambue tuichakue +pdfjs-editor-resizer-top-middle = + .aria-label = Yvate mbytépe — emoambue tuichakue +pdfjs-editor-resizer-top-right = + .aria-label = Yvate akatúape — emoambue tuichakue +pdfjs-editor-resizer-middle-right = + .aria-label = Mbyte akatúape — emoambue tuichakue +pdfjs-editor-resizer-bottom-right = + .aria-label = Yvy gotyo akatúape — emoambue tuichakue +pdfjs-editor-resizer-bottom-middle = + .aria-label = Yvy gotyo mbytépe — emoambue tuichakue +pdfjs-editor-resizer-bottom-left = + .aria-label = Iguýpe asu gotyo — emoambue tuichakue +pdfjs-editor-resizer-middle-left = + .aria-label = Mbyte asu gotyo — emoambue tuichakue + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Jehechaveha sa’y +pdfjs-editor-colorpicker-button = + .title = Emoambue sa’y +pdfjs-editor-colorpicker-dropdown = + .aria-label = Sa’y poravopyrã +pdfjs-editor-colorpicker-yellow = + .title = Sa’yju +pdfjs-editor-colorpicker-green = + .title = Hovyũ +pdfjs-editor-colorpicker-blue = + .title = Hovy +pdfjs-editor-colorpicker-pink = + .title = Pytãngy +pdfjs-editor-colorpicker-red = + .title = Pyha + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Techaukapa +pdfjs-editor-highlight-show-all-button = + .title = Techaukapa + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Embosako’i moñe’ẽrã mokõiha (ta’ãngáre ñeñe’ẽ) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Embojuaju moñe’ẽrã mokõiha (ta’ãngáre ñeñe’ẽ) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Edescribi ko’ápe… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Ñemyesakã mbykymi opavave ohecha’ỹva upe ta’ãnga térã pe ta’ãnga nahenyhẽiramo. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ko moñe’ẽrã mokõiha oñemoheñói ijehegui ha ikatu ndoikoporãi. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Eikuaave +pdfjs-editor-new-alt-text-create-automatically-button-label = Emoheñói moñe’ẽrã mokõiha ijeheguíva +pdfjs-editor-new-alt-text-not-now-button = Ani ko’ág̃a +pdfjs-editor-new-alt-text-error-title = Noñemoheñói moñe’ẽrã mokõiha ijeheguíva +pdfjs-editor-new-alt-text-error-description = Ehai ne moñe’ẽrã mokõiha térã eha’ã jey ag̃amieve. +pdfjs-editor-new-alt-text-error-close-button = Mboty +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Emboguejyhína IA moñe’ẽrã mokõiháva ({ $downloadedSize } { $totalSize } MB) mba’e + .aria-valuetext = Emboguejyhína IA moñe’ẽrã mokõiháva ({ $downloadedSize } { $totalSize } MB) mba’e +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Moñe’ẽrã mokõiha mbojuajupyre +pdfjs-editor-new-alt-text-added-button-label = Oñembojuaju moñe’ẽrã mokõiha +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Ndaipóri moñe’ẽrã mokõiha +pdfjs-editor-new-alt-text-missing-button-label = Ndaipóri moñe’ẽrã mokõiha +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Ehechajey moñe’ẽrã mokõiha +pdfjs-editor-new-alt-text-to-review-button-label = Ehechajey moñe’ẽrã mokõiha +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Heñóiva ijeheguiete: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ta’ãnga moñe’ẽrã mokõiha ñemboheko +pdfjs-image-alt-text-settings-button-label = Ta’ãnga moñe’ẽrã mokõiha ñemboheko +pdfjs-editor-alt-text-settings-dialog-label = Ta’ãnga moñe’ẽrã mokõiha ñemboheko +pdfjs-editor-alt-text-settings-automatic-title = Moñe’ẽrã mokõiha ijeheguíva +pdfjs-editor-alt-text-settings-create-model-button-label = Emoheñói moñe’ẽrã mokõiha ijeheguíva +pdfjs-editor-alt-text-settings-create-model-description = Ñemyesakã mbykymi opavave tapicha ohecha’ỹva upe ta’ãnga térã pe ta’ãnga nahenyhẽiramo. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Peteĩva IA moñe’ẽrã mokõiha ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Oku’e mba’e’okaitépe umi mba’ekuaarã hekoñemi hag̃ua. Tekotevẽva moñe’ẽrã ykegua ijeheguívape. +pdfjs-editor-alt-text-settings-delete-model-button = Mboguete +pdfjs-editor-alt-text-settings-download-model-button = Mboguejy +pdfjs-editor-alt-text-settings-downloading-model-button = Emboguejyhína… +pdfjs-editor-alt-text-settings-editor-title = Moñe’ẽrã mokõiha mbosako’iha +pdfjs-editor-alt-text-settings-show-dialog-button-label = Ehechauka moñe’ẽrã mokõiha mbosako’iha embojuajúvo ta’ãnga +pdfjs-editor-alt-text-settings-show-dialog-description = Nepytyvõta ta’ãngakuéra orekotaha moñe’ẽrã mokõiha. +pdfjs-editor-alt-text-settings-close-button = Mboty + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Mbosa’ýva mboguete +pdfjs-editor-undo-bar-message-freetext = Moñe’ẽrã mboguepyre +pdfjs-editor-undo-bar-message-ink = Ta’ãnga mboguepyre +pdfjs-editor-undo-bar-message-stamp = Ta’ãnga mboguepyre +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } jehaikue mboguepyre + *[other] { $count } jehaikue mboguepyre + } +pdfjs-editor-undo-bar-undo-button = + .title = Mboguevi +pdfjs-editor-undo-bar-undo-button-label = Mboguevi +pdfjs-editor-undo-bar-close-button = + .title = Mboty +pdfjs-editor-undo-bar-close-button-label = Mboty diff --git a/public/assets/pdfjs/locale/gu-IN/viewer.ftl b/public/assets/pdfjs/locale/gu-IN/viewer.ftl new file mode 100644 index 0000000..5d8bb54 --- /dev/null +++ b/public/assets/pdfjs/locale/gu-IN/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = પહેલાનુ પાનું +pdfjs-previous-button-label = પહેલાનુ +pdfjs-next-button = + .title = આગળનુ પાનું +pdfjs-next-button-label = આગળનું +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = પાનું +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = નો { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } નો { $pagesCount }) +pdfjs-zoom-out-button = + .title = મોટુ કરો +pdfjs-zoom-out-button-label = મોટુ કરો +pdfjs-zoom-in-button = + .title = નાનું કરો +pdfjs-zoom-in-button-label = નાનું કરો +pdfjs-zoom-select = + .title = નાનું મોટુ કરો +pdfjs-presentation-mode-button = + .title = રજૂઆત સ્થિતિમાં જાવ +pdfjs-presentation-mode-button-label = રજૂઆત સ્થિતિ +pdfjs-open-file-button = + .title = ફાઇલ ખોલો +pdfjs-open-file-button-label = ખોલો +pdfjs-print-button = + .title = છાપો +pdfjs-print-button-label = છારો + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = સાધનો +pdfjs-tools-button-label = સાધનો +pdfjs-first-page-button = + .title = પહેલાં પાનામાં જાવ +pdfjs-first-page-button-label = પ્રથમ પાનાં પર જાવ +pdfjs-last-page-button = + .title = છેલ્લા પાનાં પર જાવ +pdfjs-last-page-button-label = છેલ્લા પાનાં પર જાવ +pdfjs-page-rotate-cw-button = + .title = ઘડિયાળનાં કાંટા તરફ ફેરવો +pdfjs-page-rotate-cw-button-label = ઘડિયાળનાં કાંટા તરફ ફેરવો +pdfjs-page-rotate-ccw-button = + .title = ઘડિયાળનાં કાંટાની ઉલટી દિશામાં ફેરવો +pdfjs-page-rotate-ccw-button-label = ઘડિયાળનાં કાંટાની વિરુદ્દ ફેરવો +pdfjs-cursor-text-select-tool-button = + .title = ટેક્સ્ટ પસંદગી ટૂલ સક્ષમ કરો +pdfjs-cursor-text-select-tool-button-label = ટેક્સ્ટ પસંદગી ટૂલ +pdfjs-cursor-hand-tool-button = + .title = હાથનાં સાધનને સક્રિય કરો +pdfjs-cursor-hand-tool-button-label = હેન્ડ ટૂલ +pdfjs-scroll-vertical-button = + .title = ઊભી સ્ક્રોલિંગનો ઉપયોગ કરો +pdfjs-scroll-vertical-button-label = ઊભી સ્ક્રોલિંગ +pdfjs-scroll-horizontal-button = + .title = આડી સ્ક્રોલિંગનો ઉપયોગ કરો +pdfjs-scroll-horizontal-button-label = આડી સ્ક્રોલિંગ +pdfjs-scroll-wrapped-button = + .title = આવરિત સ્ક્રોલિંગનો ઉપયોગ કરો +pdfjs-scroll-wrapped-button-label = આવરિત સ્ક્રોલિંગ +pdfjs-spread-none-button = + .title = પૃષ્ઠ સ્પ્રેડમાં જોડાવશો નહીં +pdfjs-spread-none-button-label = કોઈ સ્પ્રેડ નથી +pdfjs-spread-odd-button = + .title = એકી-ક્રમાંકિત પૃષ્ઠો સાથે પ્રારંભ થતાં પૃષ્ઠ સ્પ્રેડમાં જોડાઓ +pdfjs-spread-odd-button-label = એકી સ્પ્રેડ્સ +pdfjs-spread-even-button = + .title = નંબર-ક્રમાંકિત પૃષ્ઠોથી શરૂ થતાં પૃષ્ઠ સ્પ્રેડમાં જોડાઓ +pdfjs-spread-even-button-label = સરખું ફેલાવવું + +## Document properties dialog + +pdfjs-document-properties-button = + .title = દસ્તાવેજ ગુણધર્મો… +pdfjs-document-properties-button-label = દસ્તાવેજ ગુણધર્મો… +pdfjs-document-properties-file-name = ફાઇલ નામ: +pdfjs-document-properties-file-size = ફાઇલ માપ: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } બાઇટ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } બાઇટ) +pdfjs-document-properties-title = શીર્ષક: +pdfjs-document-properties-author = લેખક: +pdfjs-document-properties-subject = વિષય: +pdfjs-document-properties-keywords = કિવર્ડ: +pdfjs-document-properties-creation-date = નિર્માણ તારીખ: +pdfjs-document-properties-modification-date = ફેરફાર તારીખ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = નિર્માતા: +pdfjs-document-properties-producer = PDF નિર્માતા: +pdfjs-document-properties-version = PDF આવૃત્તિ: +pdfjs-document-properties-page-count = પાનાં ગણતરી: +pdfjs-document-properties-page-size = પૃષ્ઠનું કદ: +pdfjs-document-properties-page-size-unit-inches = ઇંચ +pdfjs-document-properties-page-size-unit-millimeters = મીમી +pdfjs-document-properties-page-size-orientation-portrait = ઉભું +pdfjs-document-properties-page-size-orientation-landscape = આડુ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = પત્ર +pdfjs-document-properties-page-size-name-legal = કાયદાકીય + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = ઝડપી વૅબ દૃશ્ય: +pdfjs-document-properties-linearized-yes = હા +pdfjs-document-properties-linearized-no = ના +pdfjs-document-properties-close-button = બંધ કરો + +## Print + +pdfjs-print-progress-message = છાપકામ માટે દસ્તાવેજ તૈયાર કરી રહ્યા છે… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = રદ કરો +pdfjs-printing-not-supported = ચેતવણી: છાપવાનું આ બ્રાઉઝર દ્દારા સંપૂર્ણપણે આધારભૂત નથી. +pdfjs-printing-not-ready = Warning: PDF એ છાપવા માટે સંપૂર્ણપણે લાવેલ છે. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ટૉગલ બાજુપટ્ટી +pdfjs-toggle-sidebar-button-label = ટૉગલ બાજુપટ્ટી +pdfjs-document-outline-button = + .title = દસ્તાવેજની રૂપરેખા બતાવો(બધી આઇટમ્સને વિસ્તૃત/સંકુચિત કરવા માટે ડબલ-ક્લિક કરો) +pdfjs-document-outline-button-label = દસ્તાવેજ રૂપરેખા +pdfjs-attachments-button = + .title = જોડાણોને બતાવો +pdfjs-attachments-button-label = જોડાણો +pdfjs-thumbs-button = + .title = થંબનેલ્સ બતાવો +pdfjs-thumbs-button-label = થંબનેલ્સ +pdfjs-findbar-button = + .title = દસ્તાવેજમાં શોધો +pdfjs-findbar-button-label = શોધો + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = પાનું { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = પાનાં { $page } નું થંબનેલ્સ + +## Find panel button title and messages + +pdfjs-find-input = + .title = શોધો + .placeholder = દસ્તાવેજમાં શોધો… +pdfjs-find-previous-button = + .title = શબ્દસમૂહની પાછલી ઘટનાને શોધો +pdfjs-find-previous-button-label = પહેલાંનુ +pdfjs-find-next-button = + .title = શબ્દસમૂહની આગળની ઘટનાને શોધો +pdfjs-find-next-button-label = આગળનું +pdfjs-find-highlight-checkbox = બધુ પ્રકાશિત કરો +pdfjs-find-match-case-checkbox-label = કેસ બંધબેસાડો +pdfjs-find-entire-word-checkbox-label = સંપૂર્ણ શબ્દો +pdfjs-find-reached-top = દસ્તાવેજનાં ટોચે પહોંચી ગયા, તળિયેથી ચાલુ કરેલ હતુ +pdfjs-find-reached-bottom = દસ્તાવેજનાં અંતે પહોંચી ગયા, ઉપરથી ચાલુ કરેલ હતુ +pdfjs-find-not-found = શબ્દસમૂહ મળ્યુ નથી + +## Predefined zoom values + +pdfjs-page-scale-width = પાનાની પહોળાઇ +pdfjs-page-scale-fit = પાનું બંધબેસતુ +pdfjs-page-scale-auto = આપમેળે નાનુંમોટુ કરો +pdfjs-page-scale-actual = ચોક્કસ માપ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = ભૂલ ઉદ્ભવી જ્યારે PDF ને લાવી રહ્યા હોય. +pdfjs-invalid-file-error = અયોગ્ય અથવા ભાંગેલ PDF ફાઇલ. +pdfjs-missing-file-error = ગુમ થયેલ PDF ફાઇલ. +pdfjs-unexpected-response-error = અનપેક્ષિત સર્વર પ્રતિસાદ. +pdfjs-rendering-error = ભૂલ ઉદ્ભવી જ્યારે પાનાંનુ રેન્ડ કરી રહ્યા હોય. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = આ PDF ફાઇલને ખોલવા પાસવર્ડને દાખલ કરો. +pdfjs-password-invalid = અયોગ્ય પાસવર્ડ. મહેરબાની કરીને ફરી પ્રયત્ન કરો. +pdfjs-password-ok-button = બરાબર +pdfjs-password-cancel-button = રદ કરો +pdfjs-web-fonts-disabled = વેબ ફોન્ટ નિષ્ક્રિય થયેલ છે: ઍમ્બેડ થયેલ PDF ફોન્ટને વાપરવાનું અસમર્થ. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/he/viewer.ftl b/public/assets/pdfjs/locale/he/viewer.ftl new file mode 100644 index 0000000..08308c0 --- /dev/null +++ b/public/assets/pdfjs/locale/he/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = דף קודם +pdfjs-previous-button-label = קודם +pdfjs-next-button = + .title = דף הבא +pdfjs-next-button-label = הבא +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = דף +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = מתוך { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } מתוך { $pagesCount }) +pdfjs-zoom-out-button = + .title = התרחקות +pdfjs-zoom-out-button-label = התרחקות +pdfjs-zoom-in-button = + .title = התקרבות +pdfjs-zoom-in-button-label = התקרבות +pdfjs-zoom-select = + .title = מרחק מתצוגה +pdfjs-presentation-mode-button = + .title = מעבר למצב מצגת +pdfjs-presentation-mode-button-label = מצב מצגת +pdfjs-open-file-button = + .title = פתיחת קובץ +pdfjs-open-file-button-label = פתיחה +pdfjs-print-button = + .title = הדפסה +pdfjs-print-button-label = הדפסה +pdfjs-save-button = + .title = שמירה +pdfjs-save-button-label = שמירה +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = הורדה +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = הורדה +pdfjs-bookmark-button = + .title = עמוד נוכחי (הצגת כתובת האתר מהעמוד הנוכחי) +pdfjs-bookmark-button-label = עמוד נוכחי + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = כלים +pdfjs-tools-button-label = כלים +pdfjs-first-page-button = + .title = מעבר לעמוד הראשון +pdfjs-first-page-button-label = מעבר לעמוד הראשון +pdfjs-last-page-button = + .title = מעבר לעמוד האחרון +pdfjs-last-page-button-label = מעבר לעמוד האחרון +pdfjs-page-rotate-cw-button = + .title = הטיה עם כיוון השעון +pdfjs-page-rotate-cw-button-label = הטיה עם כיוון השעון +pdfjs-page-rotate-ccw-button = + .title = הטיה כנגד כיוון השעון +pdfjs-page-rotate-ccw-button-label = הטיה כנגד כיוון השעון +pdfjs-cursor-text-select-tool-button = + .title = הפעלת כלי בחירת טקסט +pdfjs-cursor-text-select-tool-button-label = כלי בחירת טקסט +pdfjs-cursor-hand-tool-button = + .title = הפעלת כלי היד +pdfjs-cursor-hand-tool-button-label = כלי יד +pdfjs-scroll-page-button = + .title = שימוש בגלילת עמוד +pdfjs-scroll-page-button-label = גלילת עמוד +pdfjs-scroll-vertical-button = + .title = שימוש בגלילה אנכית +pdfjs-scroll-vertical-button-label = גלילה אנכית +pdfjs-scroll-horizontal-button = + .title = שימוש בגלילה אופקית +pdfjs-scroll-horizontal-button-label = גלילה אופקית +pdfjs-scroll-wrapped-button = + .title = שימוש בגלילה רציפה +pdfjs-scroll-wrapped-button-label = גלילה רציפה +pdfjs-spread-none-button = + .title = לא לצרף מפתחי עמודים +pdfjs-spread-none-button-label = ללא מפתחים +pdfjs-spread-odd-button = + .title = צירוף מפתחי עמודים שמתחילים בדפים עם מספרים אי־זוגיים +pdfjs-spread-odd-button-label = מפתחים אי־זוגיים +pdfjs-spread-even-button = + .title = צירוף מפתחי עמודים שמתחילים בדפים עם מספרים זוגיים +pdfjs-spread-even-button-label = מפתחים זוגיים + +## Document properties dialog + +pdfjs-document-properties-button = + .title = מאפייני מסמך… +pdfjs-document-properties-button-label = מאפייני מסמך… +pdfjs-document-properties-file-name = שם קובץ: +pdfjs-document-properties-file-size = גודל הקובץ: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } ק״ב ({ $b } בתים) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } מ״ב ({ $b } בתים) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } ק״ב ({ $size_b } בתים) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } מ״ב ({ $size_b } בתים) +pdfjs-document-properties-title = כותרת: +pdfjs-document-properties-author = מחבר: +pdfjs-document-properties-subject = נושא: +pdfjs-document-properties-keywords = מילות מפתח: +pdfjs-document-properties-creation-date = תאריך יצירה: +pdfjs-document-properties-modification-date = תאריך שינוי: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = יוצר: +pdfjs-document-properties-producer = יצרן PDF: +pdfjs-document-properties-version = גרסת PDF: +pdfjs-document-properties-page-count = מספר דפים: +pdfjs-document-properties-page-size = גודל העמוד: +pdfjs-document-properties-page-size-unit-inches = אינ׳ +pdfjs-document-properties-page-size-unit-millimeters = מ״מ +pdfjs-document-properties-page-size-orientation-portrait = לאורך +pdfjs-document-properties-page-size-orientation-landscape = לרוחב +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = מכתב +pdfjs-document-properties-page-size-name-legal = דף משפטי + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = תצוגת דף מהירה: +pdfjs-document-properties-linearized-yes = כן +pdfjs-document-properties-linearized-no = לא +pdfjs-document-properties-close-button = סגירה + +## Print + +pdfjs-print-progress-message = מסמך בהכנה להדפסה… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ביטול +pdfjs-printing-not-supported = אזהרה: הדפסה אינה נתמכת במלואה בדפדפן זה. +pdfjs-printing-not-ready = אזהרה: מסמך ה־PDF לא נטען לחלוטין עד מצב שמאפשר הדפסה. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = הצגה/הסתרה של סרגל הצד +pdfjs-toggle-sidebar-notification-button = + .title = החלפת תצוגת סרגל צד (מסמך שמכיל תוכן עניינים/קבצים מצורפים/שכבות) +pdfjs-toggle-sidebar-button-label = הצגה/הסתרה של סרגל הצד +pdfjs-document-outline-button = + .title = הצגת תוכן העניינים של המסמך (לחיצה כפולה כדי להרחיב או לצמצם את כל הפריטים) +pdfjs-document-outline-button-label = תוכן העניינים של המסמך +pdfjs-attachments-button = + .title = הצגת צרופות +pdfjs-attachments-button-label = צרופות +pdfjs-layers-button = + .title = הצגת שכבות (יש ללחוץ לחיצה כפולה כדי לאפס את כל השכבות למצב ברירת המחדל) +pdfjs-layers-button-label = שכבות +pdfjs-thumbs-button = + .title = הצגת תצוגה מקדימה +pdfjs-thumbs-button-label = תצוגה מקדימה +pdfjs-current-outline-item-button = + .title = מציאת פריט תוכן העניינים הנוכחי +pdfjs-current-outline-item-button-label = פריט תוכן העניינים הנוכחי +pdfjs-findbar-button = + .title = חיפוש במסמך +pdfjs-findbar-button-label = חיפוש +pdfjs-additional-layers = שכבות נוספות + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = עמוד { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = תצוגה מקדימה של עמוד { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = חיפוש + .placeholder = חיפוש במסמך… +pdfjs-find-previous-button = + .title = מציאת המופע הקודם של הביטוי +pdfjs-find-previous-button-label = קודם +pdfjs-find-next-button = + .title = מציאת המופע הבא של הביטוי +pdfjs-find-next-button-label = הבא +pdfjs-find-highlight-checkbox = הדגשת הכול +pdfjs-find-match-case-checkbox-label = התאמת אותיות +pdfjs-find-match-diacritics-checkbox-label = התאמה דיאקריטית +pdfjs-find-entire-word-checkbox-label = מילים שלמות +pdfjs-find-reached-top = הגיע לראש הדף, ממשיך מלמטה +pdfjs-find-reached-bottom = הגיע לסוף הדף, ממשיך מלמעלה +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } מתוך { $total } תוצאות + *[other] { $current } מתוך { $total } תוצאות + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] יותר מתוצאה אחת + *[other] יותר מ־{ $limit } תוצאות + } +pdfjs-find-not-found = הביטוי לא נמצא + +## Predefined zoom values + +pdfjs-page-scale-width = רוחב העמוד +pdfjs-page-scale-fit = התאמה לעמוד +pdfjs-page-scale-auto = מרחק מתצוגה אוטומטי +pdfjs-page-scale-actual = גודל אמיתי +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = עמוד { $page } + +## Loading indicator messages + +pdfjs-loading-error = אירעה שגיאה בעת טעינת ה־PDF. +pdfjs-invalid-file-error = קובץ PDF פגום או לא תקין. +pdfjs-missing-file-error = קובץ PDF חסר. +pdfjs-unexpected-response-error = תגובת שרת לא צפויה. +pdfjs-rendering-error = אירעה שגיאה בעת עיבוד הדף. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [הערת { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = נא להכניס את הססמה לפתיחת קובץ PDF זה. +pdfjs-password-invalid = ססמה שגויה. נא לנסות שנית. +pdfjs-password-ok-button = אישור +pdfjs-password-cancel-button = ביטול +pdfjs-web-fonts-disabled = גופני רשת מנוטרלים: לא ניתן להשתמש בגופני PDF מוטבעים. + +## Editing + +pdfjs-editor-free-text-button = + .title = טקסט +pdfjs-editor-free-text-button-label = טקסט +pdfjs-editor-ink-button = + .title = ציור +pdfjs-editor-ink-button-label = ציור +pdfjs-editor-stamp-button = + .title = הוספה או עריכת תמונות +pdfjs-editor-stamp-button-label = הוספה או עריכת תמונות +pdfjs-editor-highlight-button = + .title = סימון +pdfjs-editor-highlight-button-label = סימון +pdfjs-highlight-floating-button1 = + .title = סימון + .aria-label = סימון +pdfjs-highlight-floating-button-label = סימון + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = הסרת ציור +pdfjs-editor-remove-freetext-button = + .title = הסרת טקסט +pdfjs-editor-remove-stamp-button = + .title = הסרת תמונה +pdfjs-editor-remove-highlight-button = + .title = הסרת סימון + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = צבע +pdfjs-editor-free-text-size-input = גודל +pdfjs-editor-ink-color-input = צבע +pdfjs-editor-ink-thickness-input = עובי +pdfjs-editor-ink-opacity-input = אטימות +pdfjs-editor-stamp-add-image-button = + .title = הוספת תמונה +pdfjs-editor-stamp-add-image-button-label = הוספת תמונה +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = עובי +pdfjs-editor-free-highlight-thickness-title = + .title = שינוי עובי בעת סימון פריטים שאינם טקסט +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = עורך טקסט + .default-content = נא להתחיל להקליד… +pdfjs-free-text = + .aria-label = עורך טקסט +pdfjs-free-text-default-content = להתחיל להקליד… +pdfjs-ink = + .aria-label = עורך ציור +pdfjs-ink-canvas = + .aria-label = תמונה שנוצרה על־ידי משתמש + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = טקסט חלופי +pdfjs-editor-alt-text-edit-button = + .aria-label = עריכת טקסט חלופי +pdfjs-editor-alt-text-edit-button-label = עריכת טקסט חלופי +pdfjs-editor-alt-text-dialog-label = בחירת אפשרות +pdfjs-editor-alt-text-dialog-description = טקסט חלופי עוזר כשאנשים לא יכולים לראות את התמונה או כשהיא לא נטענת. +pdfjs-editor-alt-text-add-description-label = הוספת תיאור +pdfjs-editor-alt-text-add-description-description = כדאי לתאר במשפט אחד או שניים את הנושא, התפאורה או הפעולות. +pdfjs-editor-alt-text-mark-decorative-label = סימון כדקורטיבי +pdfjs-editor-alt-text-mark-decorative-description = זה משמש לתמונות נוי, כמו גבולות או סימני מים. +pdfjs-editor-alt-text-cancel-button = ביטול +pdfjs-editor-alt-text-save-button = שמירה +pdfjs-editor-alt-text-decorative-tooltip = מסומן כדקורטיבי +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = לדוגמה, ״גבר צעיר מתיישב ליד שולחן לאכול ארוחה״ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = טקסט חלופי + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = פינה שמאלית עליונה - שינוי גודל +pdfjs-editor-resizer-label-top-middle = למעלה באמצע - שינוי גודל +pdfjs-editor-resizer-label-top-right = פינה ימנית עליונה - שינוי גודל +pdfjs-editor-resizer-label-middle-right = ימינה באמצע - שינוי גודל +pdfjs-editor-resizer-label-bottom-right = פינה ימנית תחתונה - שינוי גודל +pdfjs-editor-resizer-label-bottom-middle = למטה באמצע - שינוי גודל +pdfjs-editor-resizer-label-bottom-left = פינה שמאלית תחתונה - שינוי גודל +pdfjs-editor-resizer-label-middle-left = שמאלה באמצע - שינוי גודל +pdfjs-editor-resizer-top-left = + .aria-label = פינה שמאלית עליונה - שינוי גודל +pdfjs-editor-resizer-top-middle = + .aria-label = למעלה באמצע - שינוי גודל +pdfjs-editor-resizer-top-right = + .aria-label = פינה ימנית עליונה - שינוי גודל +pdfjs-editor-resizer-middle-right = + .aria-label = ימינה באמצע - שינוי גודל +pdfjs-editor-resizer-bottom-right = + .aria-label = פינה ימנית תחתונה - שינוי גודל +pdfjs-editor-resizer-bottom-middle = + .aria-label = למטה באמצע - שינוי גודל +pdfjs-editor-resizer-bottom-left = + .aria-label = פינה שמאלית תחתונה - שינוי גודל +pdfjs-editor-resizer-middle-left = + .aria-label = שמאלה באמצע - שינוי גודל + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = צבע סימון +pdfjs-editor-colorpicker-button = + .title = שינוי צבע +pdfjs-editor-colorpicker-dropdown = + .aria-label = בחירת צבע +pdfjs-editor-colorpicker-yellow = + .title = צהוב +pdfjs-editor-colorpicker-green = + .title = ירוק +pdfjs-editor-colorpicker-blue = + .title = כחול +pdfjs-editor-colorpicker-pink = + .title = ורוד +pdfjs-editor-colorpicker-red = + .title = אדום + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = הצגת הכול +pdfjs-editor-highlight-show-all-button = + .title = הצגת הכול + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = עריכת טקסט חלופי (תיאור תמונה) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = הוספת טקסט חלופי (תיאור תמונה) +pdfjs-editor-new-alt-text-textarea = + .placeholder = נא לכתוב את התיאור שלך כאן… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = תיאור קצר לאנשים שאינם יכולים לראות את התמונה או כאשר התמונה אינה נטענת. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = טקסט חלופי זה נוצר באופן אוטומטי ועשוי להיות לא מדויק. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = מידע נוסף +pdfjs-editor-new-alt-text-create-automatically-button-label = יצירת טקסט חלופי באופן אוטומטי +pdfjs-editor-new-alt-text-not-now-button = לא כעת +pdfjs-editor-new-alt-text-error-title = לא ניתן היה ליצור טקסט חלופי באופן אוטומטי +pdfjs-editor-new-alt-text-error-description = נא לכתוב טקסט חלופי משלך או לנסות שוב מאוחר יותר. +pdfjs-editor-new-alt-text-error-close-button = סגירה +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = בתהליך הורדת מודל AI של טקסט חלופי ({ $downloadedSize } מתוך { $totalSize } מ״ב) + .aria-valuetext = בתהליך הורדת מודל AI של טקסט חלופי ({ $downloadedSize } מתוך { $totalSize } מ״ב) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = נוסף טקסט חלופי +pdfjs-editor-new-alt-text-added-button-label = נוסף טקסט חלופי +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = חסר טקסט חלופי +pdfjs-editor-new-alt-text-missing-button-label = חסר טקסט חלופי +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = סקירת טקסט חלופי +pdfjs-editor-new-alt-text-to-review-button-label = סקירת טקסט חלופי +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = נוצר באופן אוטומטי: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = הגדרות טקסט חלופי של תמונה +pdfjs-image-alt-text-settings-button-label = הגדרות טקסט חלופי של תמונה +pdfjs-editor-alt-text-settings-dialog-label = הגדרות טקסט חלופי של תמונה +pdfjs-editor-alt-text-settings-automatic-title = טקסט חלופי אוטומטי +pdfjs-editor-alt-text-settings-create-model-button-label = יצירת טקסט חלופי באופן אוטומטי +pdfjs-editor-alt-text-settings-create-model-description = הצעת תיאורים כדי לסייע לאנשים שאינם יכולים לראות את התמונה או כאשר התמונה אינה נטענת. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = מודל AI לטקסט חלופי ({ $totalSize } מ״ב) +pdfjs-editor-alt-text-settings-ai-model-description = פועל באופן מקומי במכשיר שלך כך שהנתונים שלך נשארים פרטיים. נדרש עבור טקסט חלופי אוטומטי. +pdfjs-editor-alt-text-settings-delete-model-button = מחיקה +pdfjs-editor-alt-text-settings-download-model-button = הורדה +pdfjs-editor-alt-text-settings-downloading-model-button = בהורדה… +pdfjs-editor-alt-text-settings-editor-title = עורך טקסט חלופי +pdfjs-editor-alt-text-settings-show-dialog-button-label = הצגת עורך טקסט חלופי מיד בעת הוספת תמונה +pdfjs-editor-alt-text-settings-show-dialog-description = מסייע לך לוודא שלכל התמונות שלך יש טקסט חלופי. +pdfjs-editor-alt-text-settings-close-button = סגירה + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = הסימון הוסר +pdfjs-editor-undo-bar-message-freetext = הטקסט הוסר +pdfjs-editor-undo-bar-message-ink = הציור הוסר +pdfjs-editor-undo-bar-message-stamp = התמונה הוסרה +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] הערה אחת הוסרה + *[other] { $count } הערות הוסרו + } +pdfjs-editor-undo-bar-undo-button = + .title = ביטול פעולה +pdfjs-editor-undo-bar-undo-button-label = ביטול פעלה +pdfjs-editor-undo-bar-close-button = + .title = סגירה +pdfjs-editor-undo-bar-close-button-label = סגירה diff --git a/public/assets/pdfjs/locale/hi-IN/viewer.ftl b/public/assets/pdfjs/locale/hi-IN/viewer.ftl new file mode 100644 index 0000000..b6f378f --- /dev/null +++ b/public/assets/pdfjs/locale/hi-IN/viewer.ftl @@ -0,0 +1,267 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = पिछला पृष्ठ +pdfjs-previous-button-label = पिछला +pdfjs-next-button = + .title = अगला पृष्ठ +pdfjs-next-button-label = आगे +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = पृष्ठ: +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } का +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = छोटा करें +pdfjs-zoom-out-button-label = छोटा करें +pdfjs-zoom-in-button = + .title = बड़ा करें +pdfjs-zoom-in-button-label = बड़ा करें +pdfjs-zoom-select = + .title = बड़ा-छोटा करें +pdfjs-presentation-mode-button = + .title = प्रस्तुति अवस्था में जाएँ +pdfjs-presentation-mode-button-label = प्रस्तुति अवस्था +pdfjs-open-file-button = + .title = फ़ाइल खोलें +pdfjs-open-file-button-label = खोलें +pdfjs-print-button = + .title = छापें +pdfjs-print-button-label = छापें + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = औज़ार +pdfjs-tools-button-label = औज़ार +pdfjs-first-page-button = + .title = प्रथम पृष्ठ पर जाएँ +pdfjs-first-page-button-label = प्रथम पृष्ठ पर जाएँ +pdfjs-last-page-button = + .title = अंतिम पृष्ठ पर जाएँ +pdfjs-last-page-button-label = अंतिम पृष्ठ पर जाएँ +pdfjs-page-rotate-cw-button = + .title = घड़ी की दिशा में घुमाएँ +pdfjs-page-rotate-cw-button-label = घड़ी की दिशा में घुमाएँ +pdfjs-page-rotate-ccw-button = + .title = घड़ी की दिशा से उल्टा घुमाएँ +pdfjs-page-rotate-ccw-button-label = घड़ी की दिशा से उल्टा घुमाएँ +pdfjs-cursor-text-select-tool-button = + .title = पाठ चयन उपकरण सक्षम करें +pdfjs-cursor-text-select-tool-button-label = पाठ चयन उपकरण +pdfjs-cursor-hand-tool-button = + .title = हस्त उपकरण सक्षम करें +pdfjs-cursor-hand-tool-button-label = हस्त उपकरण +pdfjs-scroll-vertical-button = + .title = लंबवत स्क्रॉलिंग का उपयोग करें +pdfjs-scroll-vertical-button-label = लंबवत स्क्रॉलिंग +pdfjs-scroll-horizontal-button = + .title = क्षितिजिय स्क्रॉलिंग का उपयोग करें +pdfjs-scroll-horizontal-button-label = क्षितिजिय स्क्रॉलिंग +pdfjs-scroll-wrapped-button = + .title = व्राप्पेड स्क्रॉलिंग का उपयोग करें +pdfjs-spread-none-button-label = कोई स्प्रेड उपलब्ध नहीं +pdfjs-spread-odd-button = + .title = विषम-क्रमांकित पृष्ठों से प्रारंभ होने वाले पृष्ठ स्प्रेड में शामिल हों +pdfjs-spread-odd-button-label = विषम फैलाव + +## Document properties dialog + +pdfjs-document-properties-button = + .title = दस्तावेज़ विशेषता... +pdfjs-document-properties-button-label = दस्तावेज़ विशेषता... +pdfjs-document-properties-file-name = फ़ाइल नाम: +pdfjs-document-properties-file-size = फाइल आकारः +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = शीर्षक: +pdfjs-document-properties-author = लेखकः +pdfjs-document-properties-subject = विषय: +pdfjs-document-properties-keywords = कुंजी-शब्द: +pdfjs-document-properties-creation-date = निर्माण दिनांक: +pdfjs-document-properties-modification-date = संशोधन दिनांक: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = निर्माता: +pdfjs-document-properties-producer = PDF उत्पादक: +pdfjs-document-properties-version = PDF संस्करण: +pdfjs-document-properties-page-count = पृष्ठ गिनती: +pdfjs-document-properties-page-size = पृष्ठ आकार: +pdfjs-document-properties-page-size-unit-inches = इंच +pdfjs-document-properties-page-size-unit-millimeters = मिमी +pdfjs-document-properties-page-size-orientation-portrait = पोर्ट्रेट +pdfjs-document-properties-page-size-orientation-landscape = लैंडस्केप +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = पत्र +pdfjs-document-properties-page-size-name-legal = क़ानूनी + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = तीव्र वेब व्यू: +pdfjs-document-properties-linearized-yes = हाँ +pdfjs-document-properties-linearized-no = नहीं +pdfjs-document-properties-close-button = बंद करें + +## Print + +pdfjs-print-progress-message = छपाई के लिए दस्तावेज़ को तैयार किया जा रहा है... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = रद्द करें +pdfjs-printing-not-supported = चेतावनी: इस ब्राउज़र पर छपाई पूरी तरह से समर्थित नहीं है. +pdfjs-printing-not-ready = चेतावनी: PDF छपाई के लिए पूरी तरह से लोड नहीं है. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = स्लाइडर टॉगल करें +pdfjs-toggle-sidebar-button-label = स्लाइडर टॉगल करें +pdfjs-document-outline-button = + .title = दस्तावेज़ की रूपरेखा दिखाइए (सारी वस्तुओं को फलने अथवा समेटने के लिए दो बार क्लिक करें) +pdfjs-document-outline-button-label = दस्तावेज़ आउटलाइन +pdfjs-attachments-button = + .title = संलग्नक दिखायें +pdfjs-attachments-button-label = संलग्नक +pdfjs-thumbs-button = + .title = लघुछवियाँ दिखाएँ +pdfjs-thumbs-button-label = लघु छवि +pdfjs-findbar-button = + .title = दस्तावेज़ में ढूँढ़ें +pdfjs-findbar-button-label = ढूँढें + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = पृष्ठ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = पृष्ठ { $page } की लघु-छवि + +## Find panel button title and messages + +pdfjs-find-input = + .title = ढूँढें + .placeholder = दस्तावेज़ में खोजें... +pdfjs-find-previous-button = + .title = वाक्यांश की पिछली उपस्थिति ढूँढ़ें +pdfjs-find-previous-button-label = पिछला +pdfjs-find-next-button = + .title = वाक्यांश की अगली उपस्थिति ढूँढ़ें +pdfjs-find-next-button-label = अगला +pdfjs-find-highlight-checkbox = सभी आलोकित करें +pdfjs-find-match-case-checkbox-label = मिलान स्थिति +pdfjs-find-entire-word-checkbox-label = संपूर्ण शब्द +pdfjs-find-reached-top = पृष्ठ के ऊपर पहुंच गया, नीचे से जारी रखें +pdfjs-find-reached-bottom = पृष्ठ के नीचे में जा पहुँचा, ऊपर से जारी +pdfjs-find-not-found = वाक्यांश नहीं मिला + +## Predefined zoom values + +pdfjs-page-scale-width = पृष्ठ चौड़ाई +pdfjs-page-scale-fit = पृष्ठ फिट +pdfjs-page-scale-auto = स्वचालित जूम +pdfjs-page-scale-actual = वास्तविक आकार +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF लोड करते समय एक त्रुटि हुई. +pdfjs-invalid-file-error = अमान्य या भ्रष्ट PDF फ़ाइल. +pdfjs-missing-file-error = अनुपस्थित PDF फ़ाइल. +pdfjs-unexpected-response-error = अप्रत्याशित सर्वर प्रतिक्रिया. +pdfjs-rendering-error = पृष्ठ रेंडरिंग के दौरान त्रुटि आई. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = इस PDF फ़ाइल को खोलने के लिए कृपया कूटशब्द भरें. +pdfjs-password-invalid = अवैध कूटशब्द, कृपया फिर कोशिश करें. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = रद्द करें +pdfjs-web-fonts-disabled = वेब फॉन्ट्स निष्क्रिय हैं: अंतःस्थापित PDF फॉन्टस के उपयोग में असमर्थ. + +## Editing + + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = रंग + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/hr/viewer.ftl b/public/assets/pdfjs/locale/hr/viewer.ftl new file mode 100644 index 0000000..c081c6f --- /dev/null +++ b/public/assets/pdfjs/locale/hr/viewer.ftl @@ -0,0 +1,473 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Prethodna stranica +pdfjs-previous-button-label = Prethodna +pdfjs-next-button = + .title = Sljedeća stranica +pdfjs-next-button-label = Sljedeća +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Stranica +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = od { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } od { $pagesCount }) +pdfjs-zoom-out-button = + .title = Umanji +pdfjs-zoom-out-button-label = Umanji +pdfjs-zoom-in-button = + .title = Uvećaj +pdfjs-zoom-in-button-label = Uvećaj +pdfjs-zoom-select = + .title = Zumiranje +pdfjs-presentation-mode-button = + .title = Prebaci u modus prezentacija +pdfjs-presentation-mode-button-label = Modus prezentacija +pdfjs-open-file-button = + .title = Otvori datoteku +pdfjs-open-file-button-label = Otvori +pdfjs-print-button = + .title = Ispiši +pdfjs-print-button-label = Ispiši +pdfjs-save-button = + .title = Spremi +pdfjs-save-button-label = Spremi +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Preuzimanja +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Preuzimanja +pdfjs-bookmark-button = + .title = Trenutna stranica (pogledajte URL s trenutne stranice) +pdfjs-bookmark-button-label = Trenutna stranica + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Alati +pdfjs-tools-button-label = Alati +pdfjs-first-page-button = + .title = Idi na prvu stranicu +pdfjs-first-page-button-label = Idi na prvu stranicu +pdfjs-last-page-button = + .title = Idi na posljednju stranicu +pdfjs-last-page-button-label = Idi na posljednju stranicu +pdfjs-page-rotate-cw-button = + .title = Rotiraj u smjeru kazaljke na satu +pdfjs-page-rotate-cw-button-label = Rotiraj u smjeru kazaljke na satu +pdfjs-page-rotate-ccw-button = + .title = Rotiraj obrnutno od smjera kazaljke na satu +pdfjs-page-rotate-ccw-button-label = Rotiraj obrnutno od smjera kazaljke na satu +pdfjs-cursor-text-select-tool-button = + .title = Aktiviraj alat za biranje teksta +pdfjs-cursor-text-select-tool-button-label = Alat za označavanje teksta +pdfjs-cursor-hand-tool-button = + .title = Aktiviraj ručni alat +pdfjs-cursor-hand-tool-button-label = Ručni alat +pdfjs-scroll-page-button = + .title = Koristi klizanje stranice +pdfjs-scroll-page-button-label = Klizanje stranice +pdfjs-scroll-vertical-button = + .title = Koristi okomito pomicanje +pdfjs-scroll-vertical-button-label = Okomito pomicanje +pdfjs-scroll-horizontal-button = + .title = Koristi vodoravno pomicanje +pdfjs-scroll-horizontal-button-label = Vodoravno pomicanje +pdfjs-scroll-wrapped-button = + .title = Koristi kontinuirani raspored stranica +pdfjs-scroll-wrapped-button-label = Kontinuirani raspored stranica +pdfjs-spread-none-button = + .title = Ne izrađuj duplerice +pdfjs-spread-none-button-label = Pojedinačne stranice +pdfjs-spread-odd-button = + .title = Izradi duplerice koje počinju s neparnim stranicama +pdfjs-spread-odd-button-label = Neparne duplerice +pdfjs-spread-even-button = + .title = Izradi duplerice koje počinju s parnim stranicama +pdfjs-spread-even-button-label = Parne duplerice + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Svojstva dokumenta … +pdfjs-document-properties-button-label = Svojstva dokumenta … +pdfjs-document-properties-file-name = Ime datoteke: +pdfjs-document-properties-file-size = Veličina datoteke: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bajtova) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtova) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajtova) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtova) +pdfjs-document-properties-title = Naslov: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Predmet: +pdfjs-document-properties-keywords = Ključne riječi: +pdfjs-document-properties-creation-date = Datum stvaranja: +pdfjs-document-properties-modification-date = Datum promjene: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Stvaratelj: +pdfjs-document-properties-producer = PDF stvaratelj: +pdfjs-document-properties-version = PDF verzija: +pdfjs-document-properties-page-count = Broj stranica: +pdfjs-document-properties-page-size = Dimenzije stranice: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = uspravno +pdfjs-document-properties-page-size-orientation-landscape = položeno +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Brzi web pregled: +pdfjs-document-properties-linearized-yes = Da +pdfjs-document-properties-linearized-no = Ne +pdfjs-document-properties-close-button = Zatvori + +## Print + +pdfjs-print-progress-message = Pripremanje dokumenta za ispis… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Odustani +pdfjs-printing-not-supported = Upozorenje: Ovaj preglednik ne podržava u potpunosti ispisivanje. +pdfjs-printing-not-ready = Upozorenje: PDF nije u potpunosti učitan za ispis. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Prikaži/sakrij bočnu traku +pdfjs-toggle-sidebar-notification-button = + .title = Prikazivanje i sklanjanje bočne trake (dokument sadrži strukturu/privitke/slojeve) +pdfjs-toggle-sidebar-button-label = Prikaži/sakrij bočnu traku +pdfjs-document-outline-button = + .title = Prikaži strukturu dokumenta (dvostruki klik za rasklapanje/sklapanje svih stavki) +pdfjs-document-outline-button-label = Struktura dokumenta +pdfjs-attachments-button = + .title = Prikaži privitke +pdfjs-attachments-button-label = Privitci +pdfjs-layers-button = + .title = Prikaži slojeve (dvoklik za vraćanje svih slojeva u standardno stanje) +pdfjs-layers-button-label = Slojevi +pdfjs-thumbs-button = + .title = Prikaži minijature +pdfjs-thumbs-button-label = Minijature +pdfjs-current-outline-item-button = + .title = Pronađi trenutačni element strukture +pdfjs-current-outline-item-button-label = Trenutačni element strukture +pdfjs-findbar-button = + .title = Pronađi u dokumentu +pdfjs-findbar-button-label = Pronađi +pdfjs-additional-layers = Dodatni slojevi + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Stranica { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Minijatura stranice { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Pronađi + .placeholder = Pronađi u dokumentu … +pdfjs-find-previous-button = + .title = Pronađi prethodno pojavljivanje ovog izraza +pdfjs-find-previous-button-label = Prethodno +pdfjs-find-next-button = + .title = Pronađi sljedeće pojavljivanje ovog izraza +pdfjs-find-next-button-label = Dalje +pdfjs-find-highlight-checkbox = Istankni sve +pdfjs-find-match-case-checkbox-label = Razlikovanje velikih i malih slova +pdfjs-find-match-diacritics-checkbox-label = Razlikuj dijakritičke znakove +pdfjs-find-entire-word-checkbox-label = Cijele riječi +pdfjs-find-reached-top = Dosegnut početak dokumenta, nastavak s kraja +pdfjs-find-reached-bottom = Dosegnut kraj dokumenta, nastavak s početka +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } od { $total } rezultata + [few] { $current } od { $total } rezultata + *[other] { $current } od { $total } rezultata + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Više od { $limit } rezultat + [few] Više od { $limit } rezultata + *[other] Više od { $limit } rezultata + } +pdfjs-find-not-found = Izraz nije pronađen + +## Predefined zoom values + +pdfjs-page-scale-width = Prilagodi širini prozora +pdfjs-page-scale-fit = Prilagodi veličini prozora +pdfjs-page-scale-auto = Automatsko zumiranje +pdfjs-page-scale-actual = Stvarna veličina +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Stranica { $page } + +## Loading indicator messages + +pdfjs-loading-error = Došlo je do greške pri učitavanju PDF-a. +pdfjs-invalid-file-error = Neispravna ili oštećena PDF datoteka. +pdfjs-missing-file-error = Nedostaje PDF datoteka. +pdfjs-unexpected-response-error = Neočekivani odgovor servera. +pdfjs-rendering-error = Došlo je do greške prilikom iscrtavanja stranice. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Bilješka] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Za otvoranje ove PDF datoteku upiši lozinku. +pdfjs-password-invalid = Neispravna lozinka. Pokušaj ponovo. +pdfjs-password-ok-button = U redu +pdfjs-password-cancel-button = Odustani +pdfjs-web-fonts-disabled = Web fontovi su deaktivirani: nije moguće koristiti ugrađene PDF fontove. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Crtanje +pdfjs-editor-ink-button-label = Crtanje +pdfjs-editor-stamp-button = + .title = Dodajte ili uredite slike +pdfjs-editor-stamp-button-label = Dodajte ili uredite slike +pdfjs-editor-highlight-button = + .title = Istakni +pdfjs-editor-highlight-button-label = Istakni +pdfjs-highlight-floating-button1 = + .title = Istakni + .aria-label = Istakni +pdfjs-highlight-floating-button-label = Istakni + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Ukloni crtež +pdfjs-editor-remove-freetext-button = + .title = Ukloni tekst +pdfjs-editor-remove-stamp-button = + .title = Ukloni sliku +pdfjs-editor-remove-highlight-button = + .title = Ukloni isticanje + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Boja +pdfjs-editor-free-text-size-input = Veličina +pdfjs-editor-ink-color-input = Boja +pdfjs-editor-ink-thickness-input = Debljina +pdfjs-editor-ink-opacity-input = Neprozirnost +pdfjs-editor-stamp-add-image-button = + .title = Dodaj sliku +pdfjs-editor-stamp-add-image-button-label = Dodaj sliku +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Debljina +pdfjs-editor-free-highlight-thickness-title = + .title = Promjeni debljinu pri isticanju drugih stavki osim teksta +pdfjs-free-text = + .aria-label = Uređivač teksta +pdfjs-free-text-default-content = Počni tipkati … +pdfjs-ink = + .aria-label = Uređivač crteža +pdfjs-ink-canvas = + .aria-label = Slika koju je izradio korisnik + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternativni tekst +pdfjs-editor-alt-text-edit-button-label = Uredi alternativni tekst +pdfjs-editor-alt-text-dialog-label = Odaberi jednu opciju +pdfjs-editor-alt-text-dialog-description = Alternativni tekst pomaže slijepim osobama ili kada se slika ne učita. +pdfjs-editor-alt-text-add-description-label = Dodaj opis +pdfjs-editor-alt-text-add-description-description = Sažmi sadržaj predmeta, okruženje ili radnje u jednoj ili dvije rečenice. +pdfjs-editor-alt-text-mark-decorative-label = Označi kao ukrasno +pdfjs-editor-alt-text-mark-decorative-description = Ovo se koristi za ukrasne slike, poput rubova ili vodenih žigova. +pdfjs-editor-alt-text-cancel-button = Odustani +pdfjs-editor-alt-text-save-button = Spremi +pdfjs-editor-alt-text-decorative-tooltip = Označeno kao ukrasno +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Na primjer, „Mladić sjeda za stol kako bi jeo” + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Gornji lijevi kut – promijeni veličinu +pdfjs-editor-resizer-label-top-middle = Sredina gore – promijeni veličinu +pdfjs-editor-resizer-label-top-right = Gornji desni kut – promijeni veličinu +pdfjs-editor-resizer-label-middle-right = Sredina desno – promijeni veličinu +pdfjs-editor-resizer-label-bottom-right = Donji desni kut – promijeni veličinu +pdfjs-editor-resizer-label-bottom-middle = Sredina dolje – promjeni veličinu +pdfjs-editor-resizer-label-bottom-left = Donji lijevi kut – promijeni veličinu +pdfjs-editor-resizer-label-middle-left = Sredina lijevo – promijeni veličinu +pdfjs-editor-resizer-top-left = + .aria-label = Gornji lijevi kut – promijeni veličinu +pdfjs-editor-resizer-top-middle = + .aria-label = Sredina gore – promijeni veličinu +pdfjs-editor-resizer-top-right = + .aria-label = Gornji desni kut – promijeni veličinu +pdfjs-editor-resizer-middle-right = + .aria-label = Sredina desno – promijeni veličinu +pdfjs-editor-resizer-bottom-right = + .aria-label = Donji desni kut – promijeni veličinu +pdfjs-editor-resizer-bottom-middle = + .aria-label = Sredina dolje – promjeni veličinu +pdfjs-editor-resizer-bottom-left = + .aria-label = Donji lijevi kut – promijeni veličinu +pdfjs-editor-resizer-middle-left = + .aria-label = Sredina lijevo – promijeni veličinu + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Boja isticanja +pdfjs-editor-colorpicker-button = + .title = Promjeni boju +pdfjs-editor-colorpicker-dropdown = + .aria-label = Izbor boja +pdfjs-editor-colorpicker-yellow = + .title = Žuta +pdfjs-editor-colorpicker-green = + .title = Zelena +pdfjs-editor-colorpicker-blue = + .title = Plava +pdfjs-editor-colorpicker-pink = + .title = Ružičasta +pdfjs-editor-colorpicker-red = + .title = Crvena + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Prikaži sve +pdfjs-editor-highlight-show-all-button = + .title = Prikaži sve + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +pdfjs-editor-new-alt-text-textarea = + .placeholder = Ovdje upiši tvoj opis … +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ovaj je alternativni tekst stvoren automatski i može biti netočan. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Saznaj više +pdfjs-editor-new-alt-text-create-automatically-button-label = Automatski stvori alternativni tekst +pdfjs-editor-new-alt-text-error-title = Nije bilo moguće automatski izraditi alternativni tekst +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Preuzimanje alternativnog teksta UI modela ({ $downloadedSize } od { $totalSize } MB) + .aria-valuetext = Preuzimanje alternativnog teksta UI modela ({ $downloadedSize } od { $totalSize } MB) +pdfjs-editor-new-alt-text-added-button-label = Alternativni tekst je dodan +pdfjs-editor-new-alt-text-missing-button-label = Nedostaje alternativni tekst +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Pregledaj alternativni tekst +pdfjs-editor-new-alt-text-to-review-button-label = Pregledaj alternativni tekst +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Stvoreno automatski: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Postavke alternativnog teksta slike +pdfjs-image-alt-text-settings-button-label = Postavke alternativnog teksta slike +pdfjs-editor-alt-text-settings-dialog-label = Postavke alternativnog teksta slike +pdfjs-editor-alt-text-settings-automatic-title = Automatski alternativni tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Stvori alternativni tekst automatski +pdfjs-editor-alt-text-settings-create-model-description = Predlaže opise koji pomažu osobama koji ne mogu vidjeti sliku ili kada se slika ne učita. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alternativni tekst UI modela ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Radi lokalno na tvom uređaju kako bi tvoji podaci ostali privatni. Potrebno za automatski alternativni tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Izbriši +pdfjs-editor-alt-text-settings-download-model-button = Preuzmi +pdfjs-editor-alt-text-settings-downloading-model-button = Preuzimanje … +pdfjs-editor-alt-text-settings-editor-title = Uređivač alternativnog teksta +pdfjs-editor-alt-text-settings-show-dialog-button-label = Prikaži uređivač alternativnog teksta odmah pri dodavanju slike +pdfjs-editor-alt-text-settings-show-dialog-description = Pomaže osigurati da sve tvoje slike imaju alternativni tekst. +pdfjs-editor-alt-text-settings-close-button = Zatvori diff --git a/public/assets/pdfjs/locale/hsb/viewer.ftl b/public/assets/pdfjs/locale/hsb/viewer.ftl new file mode 100644 index 0000000..065ab8d --- /dev/null +++ b/public/assets/pdfjs/locale/hsb/viewer.ftl @@ -0,0 +1,521 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Předchadna strona +pdfjs-previous-button-label = Wróćo +pdfjs-next-button = + .title = Přichodna strona +pdfjs-next-button-label = Dale +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Strona +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = z { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } z { $pagesCount }) +pdfjs-zoom-out-button = + .title = Pomjeńšić +pdfjs-zoom-out-button-label = Pomjeńšić +pdfjs-zoom-in-button = + .title = Powjetšić +pdfjs-zoom-in-button-label = Powjetšić +pdfjs-zoom-select = + .title = Skalowanje +pdfjs-presentation-mode-button = + .title = Do prezentaciskeho modusa přeńć +pdfjs-presentation-mode-button-label = Prezentaciski modus +pdfjs-open-file-button = + .title = Dataju wočinić +pdfjs-open-file-button-label = Wočinić +pdfjs-print-button = + .title = Ćišćeć +pdfjs-print-button-label = Ćišćeć +pdfjs-save-button = + .title = Składować +pdfjs-save-button-label = Składować +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Sćahnyć +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Sćahnyć +pdfjs-bookmark-button = + .title = Aktualna strona (URL z aktualneje strony pokazać) +pdfjs-bookmark-button-label = Aktualna strona + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Nastroje +pdfjs-tools-button-label = Nastroje +pdfjs-first-page-button = + .title = K prěnjej stronje +pdfjs-first-page-button-label = K prěnjej stronje +pdfjs-last-page-button = + .title = K poslednjej stronje +pdfjs-last-page-button-label = K poslednjej stronje +pdfjs-page-rotate-cw-button = + .title = K směrej časnika wjerćeć +pdfjs-page-rotate-cw-button-label = K směrej časnika wjerćeć +pdfjs-page-rotate-ccw-button = + .title = Přećiwo směrej časnika wjerćeć +pdfjs-page-rotate-ccw-button-label = Přećiwo směrej časnika wjerćeć +pdfjs-cursor-text-select-tool-button = + .title = Nastroj za wuběranje teksta zmóžnić +pdfjs-cursor-text-select-tool-button-label = Nastroj za wuběranje teksta +pdfjs-cursor-hand-tool-button = + .title = Ručny nastroj zmóžnić +pdfjs-cursor-hand-tool-button-label = Ručny nastroj +pdfjs-scroll-page-button = + .title = Kulenje strony wužiwać +pdfjs-scroll-page-button-label = Kulenje strony +pdfjs-scroll-vertical-button = + .title = Wertikalne suwanje wužiwać +pdfjs-scroll-vertical-button-label = Wertikalne suwanje +pdfjs-scroll-horizontal-button = + .title = Horicontalne suwanje wužiwać +pdfjs-scroll-horizontal-button-label = Horicontalne suwanje +pdfjs-scroll-wrapped-button = + .title = Postupne suwanje wužiwać +pdfjs-scroll-wrapped-button-label = Postupne suwanje +pdfjs-spread-none-button = + .title = Strony njezwjazać +pdfjs-spread-none-button-label = Žana dwójna strona +pdfjs-spread-odd-button = + .title = Strony započinajo z njerunymi stronami zwjazać +pdfjs-spread-odd-button-label = Njerune strony +pdfjs-spread-even-button = + .title = Strony započinajo z runymi stronami zwjazać +pdfjs-spread-even-button-label = Rune strony + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentowe kajkosće… +pdfjs-document-properties-button-label = Dokumentowe kajkosće… +pdfjs-document-properties-file-name = Mjeno dataje: +pdfjs-document-properties-file-size = Wulkosć dataje: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bajtow) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtow) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajtow) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtow) +pdfjs-document-properties-title = Titul: +pdfjs-document-properties-author = Awtor: +pdfjs-document-properties-subject = Předmjet: +pdfjs-document-properties-keywords = Klučowe słowa: +pdfjs-document-properties-creation-date = Datum wutworjenja: +pdfjs-document-properties-modification-date = Datum změny: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Awtor: +pdfjs-document-properties-producer = PDF-zhotowjer: +pdfjs-document-properties-version = PDF-wersija: +pdfjs-document-properties-page-count = Ličba stronow: +pdfjs-document-properties-page-size = Wulkosć strony: +pdfjs-document-properties-page-size-unit-inches = cól +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = wysoki format +pdfjs-document-properties-page-size-orientation-landscape = prěčny format +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Haj +pdfjs-document-properties-linearized-no = Ně +pdfjs-document-properties-close-button = Začinić + +## Print + +pdfjs-print-progress-message = Dokument so za ćišćenje přihotuje… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Přetorhnyć +pdfjs-printing-not-supported = Warnowanje: Ćišćenje so přez tutón wobhladowak połnje njepodpěruje. +pdfjs-printing-not-ready = Warnowanje: PDF njeje so za ćišćenje dospołnje začitał. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Bóčnicu pokazać/schować +pdfjs-toggle-sidebar-notification-button = + .title = Bóčnicu přepinać (dokument rozrjad/přiwěški/woršty wobsahuje) +pdfjs-toggle-sidebar-button-label = Bóčnicu pokazać/schować +pdfjs-document-outline-button = + .title = Dokumentowy naćisk pokazać (dwójne kliknjenje, zo bychu so wšě zapiski pokazali/schowali) +pdfjs-document-outline-button-label = Dokumentowa struktura +pdfjs-attachments-button = + .title = Přiwěški pokazać +pdfjs-attachments-button-label = Přiwěški +pdfjs-layers-button = + .title = Woršty pokazać (klikńće dwójce, zo byšće wšě woršty na standardny staw wróćo stajił) +pdfjs-layers-button-label = Woršty +pdfjs-thumbs-button = + .title = Miniatury pokazać +pdfjs-thumbs-button-label = Miniatury +pdfjs-current-outline-item-button = + .title = Aktualny rozrjadowy zapisk pytać +pdfjs-current-outline-item-button-label = Aktualny rozrjadowy zapisk +pdfjs-findbar-button = + .title = W dokumenće pytać +pdfjs-findbar-button-label = Pytać +pdfjs-additional-layers = Dalše woršty + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Strona { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura strony { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Pytać + .placeholder = W dokumenće pytać… +pdfjs-find-previous-button = + .title = Předchadne wustupowanje pytanskeho wuraza pytać +pdfjs-find-previous-button-label = Wróćo +pdfjs-find-next-button = + .title = Přichodne wustupowanje pytanskeho wuraza pytać +pdfjs-find-next-button-label = Dale +pdfjs-find-highlight-checkbox = Wšě wuzběhnyć +pdfjs-find-match-case-checkbox-label = Wulkopisanje wobkedźbować +pdfjs-find-match-diacritics-checkbox-label = Diakritiske znamješka wužiwać +pdfjs-find-entire-word-checkbox-label = Cyłe słowa +pdfjs-find-reached-top = Spočatk dokumenta docpěty, pokročuje so z kóncom +pdfjs-find-reached-bottom = Kónc dokument docpěty, pokročuje so ze spočatkom +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } z { $total } wotpowědnika + [two] { $current } z { $total } wotpowědnikow + [few] { $current } z { $total } wotpowědnikow + *[other] { $current } z { $total } wotpowědnikow + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Wyše { $limit } wotpowědnik + [two] Wyše { $limit } wotpowědnikaj + [few] Wyše { $limit } wotpowědniki + *[other] Wyše { $limit } wotpowědnikow + } +pdfjs-find-not-found = Pytanski wuraz njeje so namakał + +## Predefined zoom values + +pdfjs-page-scale-width = Šěrokosć strony +pdfjs-page-scale-fit = Wulkosć strony +pdfjs-page-scale-auto = Awtomatiske skalowanje +pdfjs-page-scale-actual = Aktualna wulkosć +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Strona { $page } + +## Loading indicator messages + +pdfjs-loading-error = Při začitowanju PDF je zmylk wustupił. +pdfjs-invalid-file-error = Njepłaćiwa abo wobškodźena PDF-dataja. +pdfjs-missing-file-error = Falowaca PDF-dataja. +pdfjs-unexpected-response-error = Njewočakowana serwerowa wotmołwa. +pdfjs-rendering-error = Při zwobraznjenju strony je zmylk wustupił. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Typ přispomnjenki: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Zapodajće hesło, zo byšće PDF-dataju wočinił. +pdfjs-password-invalid = Njepłaćiwe hesło. Prošu spytajće hišće raz. +pdfjs-password-ok-button = W porjadku +pdfjs-password-cancel-button = Přetorhnyć +pdfjs-web-fonts-disabled = Webpisma su znjemóžnjene: njeje móžno, zasadźene PDF-pisma wužiwać. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Rysować +pdfjs-editor-ink-button-label = Rysować +pdfjs-editor-stamp-button = + .title = Wobrazy přidać abo wobdźěłać +pdfjs-editor-stamp-button-label = Wobrazy přidać abo wobdźěłać +pdfjs-editor-highlight-button = + .title = Wuzběhnyć +pdfjs-editor-highlight-button-label = Wuzběhnyć +pdfjs-highlight-floating-button1 = + .title = Wuzběhnjenje + .aria-label = Wuzběhnjenje +pdfjs-highlight-floating-button-label = Wuzběhnjenje + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Rysowanku wotstronić +pdfjs-editor-remove-freetext-button = + .title = Tekst wotstronić +pdfjs-editor-remove-stamp-button = + .title = Wobraz wotstronić +pdfjs-editor-remove-highlight-button = + .title = Wuzběhnjenje wotstronić + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Barba +pdfjs-editor-free-text-size-input = Wulkosć +pdfjs-editor-ink-color-input = Barba +pdfjs-editor-ink-thickness-input = Tołstosć +pdfjs-editor-ink-opacity-input = Opacita +pdfjs-editor-stamp-add-image-button = + .title = Wobraz přidać +pdfjs-editor-stamp-add-image-button-label = Wobraz přidać +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tołstosć +pdfjs-editor-free-highlight-thickness-title = + .title = Tołstosć změnić, hdyž so zapiski wuzběhuja, kotrež tekst njejsu +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstowy editor + .default-content = Započńće pisać … +pdfjs-free-text = + .aria-label = Tekstowy editor +pdfjs-free-text-default-content = Započńće pisać… +pdfjs-ink = + .aria-label = Rysowanski editor +pdfjs-ink-canvas = + .aria-label = Wobraz wutworjeny wot wužiwarja + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatiwny tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternatiwny tekst wobdźěłać +pdfjs-editor-alt-text-edit-button-label = Alternatiwny tekst wobdźěłać +pdfjs-editor-alt-text-dialog-label = Nastajenje wubrać +pdfjs-editor-alt-text-dialog-description = Alternatiwny tekst pomha, hdyž ludźo njemóža wobraz widźeć abo hdyž so wobraz njezačita. +pdfjs-editor-alt-text-add-description-label = Wopisanje přidać +pdfjs-editor-alt-text-add-description-description = Pisajće 1 sadu abo 2 sadźe, kotrejž temu, nastajenje abo akcije wopisujetej. +pdfjs-editor-alt-text-mark-decorative-label = Jako dekoratiwny markěrować +pdfjs-editor-alt-text-mark-decorative-description = To so za pyšace wobrazy wužiwa, na přikład ramiki abo wodowe znamjenja. +pdfjs-editor-alt-text-cancel-button = Přetorhnyć +pdfjs-editor-alt-text-save-button = Składować +pdfjs-editor-alt-text-decorative-tooltip = Jako dekoratiwny markěrowany +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Na přikład, „Młody muž za blidom sedźi, zo by jědź jědł“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatiwny tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Horjeka nalěwo – wulkosć změnić +pdfjs-editor-resizer-label-top-middle = Horjeka wosrjedź – wulkosć změnić +pdfjs-editor-resizer-label-top-right = Horjeka naprawo – wulkosć změnić +pdfjs-editor-resizer-label-middle-right = Wosrjedź naprawo – wulkosć změnić +pdfjs-editor-resizer-label-bottom-right = Deleka naprawo – wulkosć změnić +pdfjs-editor-resizer-label-bottom-middle = Deleka wosrjedź – wulkosć změnić +pdfjs-editor-resizer-label-bottom-left = Deleka nalěwo – wulkosć změnić +pdfjs-editor-resizer-label-middle-left = Wosrjedź nalěwo – wulkosć změnić +pdfjs-editor-resizer-top-left = + .aria-label = Horjeka nalěwo – wulkosć změnić +pdfjs-editor-resizer-top-middle = + .aria-label = Horjeka wosrjedź – wulkosć změnić +pdfjs-editor-resizer-top-right = + .aria-label = Horjeka naprawo – wulkosć změnić +pdfjs-editor-resizer-middle-right = + .aria-label = Wosrjedź naprawo – wulkosć změnić +pdfjs-editor-resizer-bottom-right = + .aria-label = Deleka naprawo – wulkosć změnić +pdfjs-editor-resizer-bottom-middle = + .aria-label = Deleka wosrjedź – wulkosć změnić +pdfjs-editor-resizer-bottom-left = + .aria-label = Deleka nalěwo – wulkosć změnić +pdfjs-editor-resizer-middle-left = + .aria-label = Wosrjedź nalěwo – wulkosć změnić + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Barba wuzběhnjenja +pdfjs-editor-colorpicker-button = + .title = Barbu změnić +pdfjs-editor-colorpicker-dropdown = + .aria-label = Wuběr barbow +pdfjs-editor-colorpicker-yellow = + .title = Žołty +pdfjs-editor-colorpicker-green = + .title = Zeleny +pdfjs-editor-colorpicker-blue = + .title = Módry +pdfjs-editor-colorpicker-pink = + .title = Pink +pdfjs-editor-colorpicker-red = + .title = Čerwjeny + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Wšě pokazać +pdfjs-editor-highlight-show-all-button = + .title = Wšě pokazać + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternatiwny tekst wobdźěłać (wobrazowe wopisanje) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternatiwny tekst přidać (wobrazowe wopisanje) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Pisajće tu swoje wopisanje… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Krótke wopisanje za ludźi, kotřiž njemóžeće wobraz widźeć abo hdyž so wobraz njezačita. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Tutón alternatiwny tekst je so awtomatisce wutworił a je snano njedokładny. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Dalše informacije +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternatiwny tekst awtomatisce wutworić +pdfjs-editor-new-alt-text-not-now-button = Nic nětko +pdfjs-editor-new-alt-text-error-title = Alternatiwny tekst njeda so awtomatisce wutworić +pdfjs-editor-new-alt-text-error-description = Prošu pisajće swój alternatiwny tekst abo spytajće pozdźišo hišće raz. +pdfjs-editor-new-alt-text-error-close-button = Začinić +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Model KI za alternatiwny tekst so sćahuje ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Model KI za alternatiwny tekst so sćahuje ({ $downloadedSize } z { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatiwny tekst je so přidał +pdfjs-editor-new-alt-text-added-button-label = Alternatiwny tekst je so přidał +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Alternatiwny tekst faluje +pdfjs-editor-new-alt-text-missing-button-label = Alternatiwny tekst faluje +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternatiwny tekst přepruwować +pdfjs-editor-new-alt-text-to-review-button-label = Alternatiwny tekst přepruwować +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Awtomatisce wutworjeny: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Nastajenja alternatiwneho wobrazoweho teksta +pdfjs-image-alt-text-settings-button-label = Nastajenja alternatiwneho wobrazoweho teksta +pdfjs-editor-alt-text-settings-dialog-label = Nastajenja alternatiwneho wobrazoweho teksta +pdfjs-editor-alt-text-settings-automatic-title = Awtomatiski alternatiwny tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Alternatiwny tekst awtomatisce wutworić +pdfjs-editor-alt-text-settings-create-model-description = Namjetuje wopisanja, zo by ludźom pomhał, kotřiž njemóžeće wobraz widźeć abo hdyž so wobraz njezačita. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model KI alternatiwneho teksta ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Běži lokalnje na wašim graće, zo bychu waše daty priwatne wostali. Za awtomatiski alternatiwny tekst trěbny. +pdfjs-editor-alt-text-settings-delete-model-button = Zhašeć +pdfjs-editor-alt-text-settings-download-model-button = Sćahnyć +pdfjs-editor-alt-text-settings-downloading-model-button = Sćahuje so… +pdfjs-editor-alt-text-settings-editor-title = Editor za alternatiwny tekst +pdfjs-editor-alt-text-settings-show-dialog-button-label = Editor alternatiwneho teksta hnydom pokazać, hdyž so wobraz přidawa +pdfjs-editor-alt-text-settings-show-dialog-description = Pomha, wam wšěm swojim wobrazam alternatiwny tekst přidać. +pdfjs-editor-alt-text-settings-close-button = Začinić + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Wotstronjene wuzběhnyć +pdfjs-editor-undo-bar-message-freetext = Tekst je so wotstronił +pdfjs-editor-undo-bar-message-ink = Rysowanka je so wotstroniła +pdfjs-editor-undo-bar-message-stamp = Wobraz je so wotstronił +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } přispomnjenka je so wotstroniła + [two] { $count } přispomnjence stej so wotstroniłoj + [few] { $count } přispomnjenki su so wotstronili + *[other] { $count } přispomnjenkow je so wotstroniło + } +pdfjs-editor-undo-bar-undo-button = + .title = Cofnyć +pdfjs-editor-undo-bar-undo-button-label = Cofnyć +pdfjs-editor-undo-bar-close-button = + .title = Začinić +pdfjs-editor-undo-bar-close-button-label = Začinić diff --git a/public/assets/pdfjs/locale/hu/viewer.ftl b/public/assets/pdfjs/locale/hu/viewer.ftl new file mode 100644 index 0000000..76307a2 --- /dev/null +++ b/public/assets/pdfjs/locale/hu/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Előző oldal +pdfjs-previous-button-label = Előző +pdfjs-next-button = + .title = Következő oldal +pdfjs-next-button-label = Tovább +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Oldal +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = összesen: { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = Kicsinyítés +pdfjs-zoom-out-button-label = Kicsinyítés +pdfjs-zoom-in-button = + .title = Nagyítás +pdfjs-zoom-in-button-label = Nagyítás +pdfjs-zoom-select = + .title = Nagyítás +pdfjs-presentation-mode-button = + .title = Váltás bemutató módba +pdfjs-presentation-mode-button-label = Bemutató mód +pdfjs-open-file-button = + .title = Fájl megnyitása +pdfjs-open-file-button-label = Megnyitás +pdfjs-print-button = + .title = Nyomtatás +pdfjs-print-button-label = Nyomtatás +pdfjs-save-button = + .title = Mentés +pdfjs-save-button-label = Mentés +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Letöltés +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Letöltés +pdfjs-bookmark-button = + .title = Jelenlegi oldal (webcím megtekintése a jelenlegi oldalról) +pdfjs-bookmark-button-label = Jelenlegi oldal + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Eszközök +pdfjs-tools-button-label = Eszközök +pdfjs-first-page-button = + .title = Ugrás az első oldalra +pdfjs-first-page-button-label = Ugrás az első oldalra +pdfjs-last-page-button = + .title = Ugrás az utolsó oldalra +pdfjs-last-page-button-label = Ugrás az utolsó oldalra +pdfjs-page-rotate-cw-button = + .title = Forgatás az óramutató járásával egyezően +pdfjs-page-rotate-cw-button-label = Forgatás az óramutató járásával egyezően +pdfjs-page-rotate-ccw-button = + .title = Forgatás az óramutató járásával ellentétesen +pdfjs-page-rotate-ccw-button-label = Forgatás az óramutató járásával ellentétesen +pdfjs-cursor-text-select-tool-button = + .title = Szövegkijelölő eszköz bekapcsolása +pdfjs-cursor-text-select-tool-button-label = Szövegkijelölő eszköz +pdfjs-cursor-hand-tool-button = + .title = Kéz eszköz bekapcsolása +pdfjs-cursor-hand-tool-button-label = Kéz eszköz +pdfjs-scroll-page-button = + .title = Oldalgörgetés használata +pdfjs-scroll-page-button-label = Oldalgörgetés +pdfjs-scroll-vertical-button = + .title = Függőleges görgetés használata +pdfjs-scroll-vertical-button-label = Függőleges görgetés +pdfjs-scroll-horizontal-button = + .title = Vízszintes görgetés használata +pdfjs-scroll-horizontal-button-label = Vízszintes görgetés +pdfjs-scroll-wrapped-button = + .title = Rácsos elrendezés használata +pdfjs-scroll-wrapped-button-label = Rácsos elrendezés +pdfjs-spread-none-button = + .title = Ne tapassza össze az oldalakat +pdfjs-spread-none-button-label = Nincs összetapasztás +pdfjs-spread-odd-button = + .title = Lapok összetapasztása, a páratlan számú oldalakkal kezdve +pdfjs-spread-odd-button-label = Összetapasztás: páratlan +pdfjs-spread-even-button = + .title = Lapok összetapasztása, a páros számú oldalakkal kezdve +pdfjs-spread-even-button-label = Összetapasztás: páros + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentum tulajdonságai… +pdfjs-document-properties-button-label = Dokumentum tulajdonságai… +pdfjs-document-properties-file-name = Fájlnév: +pdfjs-document-properties-file-size = Fájlméret: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bájt) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bájt) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bájt) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bájt) +pdfjs-document-properties-title = Cím: +pdfjs-document-properties-author = Szerző: +pdfjs-document-properties-subject = Tárgy: +pdfjs-document-properties-keywords = Kulcsszavak: +pdfjs-document-properties-creation-date = Létrehozás dátuma: +pdfjs-document-properties-modification-date = Módosítás dátuma: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Létrehozta: +pdfjs-document-properties-producer = PDF előállító: +pdfjs-document-properties-version = PDF verzió: +pdfjs-document-properties-page-count = Oldalszám: +pdfjs-document-properties-page-size = Lapméret: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = álló +pdfjs-document-properties-page-size-orientation-landscape = fekvő +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Jogi információk + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Gyors webes nézet: +pdfjs-document-properties-linearized-yes = Igen +pdfjs-document-properties-linearized-no = Nem +pdfjs-document-properties-close-button = Bezárás + +## Print + +pdfjs-print-progress-message = Dokumentum előkészítése nyomtatáshoz… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Mégse +pdfjs-printing-not-supported = Figyelmeztetés: Ez a böngésző nem teljesen támogatja a nyomtatást. +pdfjs-printing-not-ready = Figyelmeztetés: A PDF nincs teljesen betöltve a nyomtatáshoz. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Oldalsáv be/ki +pdfjs-toggle-sidebar-notification-button = + .title = Oldalsáv be/ki (a dokumentum vázlatot/mellékleteket/rétegeket tartalmaz) +pdfjs-toggle-sidebar-button-label = Oldalsáv be/ki +pdfjs-document-outline-button = + .title = Dokumentum megjelenítése online (dupla kattintás minden elem kinyitásához/összecsukásához) +pdfjs-document-outline-button-label = Dokumentumvázlat +pdfjs-attachments-button = + .title = Mellékletek megjelenítése +pdfjs-attachments-button-label = Van melléklet +pdfjs-layers-button = + .title = Rétegek megjelenítése (dupla kattintás az összes réteg alapértelmezett állapotra visszaállításához) +pdfjs-layers-button-label = Rétegek +pdfjs-thumbs-button = + .title = Bélyegképek megjelenítése +pdfjs-thumbs-button-label = Bélyegképek +pdfjs-current-outline-item-button = + .title = Jelenlegi vázlatelem megkeresése +pdfjs-current-outline-item-button-label = Jelenlegi vázlatelem +pdfjs-findbar-button = + .title = Keresés a dokumentumban +pdfjs-findbar-button-label = Keresés +pdfjs-additional-layers = További rétegek + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page }. oldal +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page }. oldal bélyegképe + +## Find panel button title and messages + +pdfjs-find-input = + .title = Keresés + .placeholder = Keresés a dokumentumban… +pdfjs-find-previous-button = + .title = A kifejezés előző előfordulásának keresése +pdfjs-find-previous-button-label = Előző +pdfjs-find-next-button = + .title = A kifejezés következő előfordulásának keresése +pdfjs-find-next-button-label = Tovább +pdfjs-find-highlight-checkbox = Összes kiemelése +pdfjs-find-match-case-checkbox-label = Kis- és nagybetűk megkülönböztetése +pdfjs-find-match-diacritics-checkbox-label = Diakritikus jelek +pdfjs-find-entire-word-checkbox-label = Teljes szavak +pdfjs-find-reached-top = A dokumentum eleje elérve, folytatás a végétől +pdfjs-find-reached-bottom = A dokumentum vége elérve, folytatás az elejétől +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } / { $total } találat + *[other] { $current } / { $total } találat + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Több mint { $limit } találat + *[other] Több mint { $limit } találat + } +pdfjs-find-not-found = A kifejezés nem található + +## Predefined zoom values + +pdfjs-page-scale-width = Oldalszélesség +pdfjs-page-scale-fit = Teljes oldal +pdfjs-page-scale-auto = Automatikus nagyítás +pdfjs-page-scale-actual = Valódi méret +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page }. oldal + +## Loading indicator messages + +pdfjs-loading-error = Hiba történt a PDF betöltésekor. +pdfjs-invalid-file-error = Érvénytelen vagy sérült PDF fájl. +pdfjs-missing-file-error = Hiányzó PDF fájl. +pdfjs-unexpected-response-error = Váratlan kiszolgálóválasz. +pdfjs-rendering-error = Hiba történt az oldal feldolgozása közben. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } megjegyzés] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Adja meg a jelszót a PDF fájl megnyitásához. +pdfjs-password-invalid = Helytelen jelszó. Próbálja újra. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Mégse +pdfjs-web-fonts-disabled = Webes betűkészletek letiltva: nem használhatók a beágyazott PDF betűkészletek. + +## Editing + +pdfjs-editor-free-text-button = + .title = Szöveg +pdfjs-editor-free-text-button-label = Szöveg +pdfjs-editor-ink-button = + .title = Rajzolás +pdfjs-editor-ink-button-label = Rajzolás +pdfjs-editor-stamp-button = + .title = Képek hozzáadása vagy szerkesztése +pdfjs-editor-stamp-button-label = Képek hozzáadása vagy szerkesztése +pdfjs-editor-highlight-button = + .title = Kiemelés +pdfjs-editor-highlight-button-label = Kiemelés +pdfjs-highlight-floating-button1 = + .title = Kiemelés + .aria-label = Kiemelés +pdfjs-highlight-floating-button-label = Kiemelés + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Rajz eltávolítása +pdfjs-editor-remove-freetext-button = + .title = Szöveg eltávolítása +pdfjs-editor-remove-stamp-button = + .title = Kép eltávolítása +pdfjs-editor-remove-highlight-button = + .title = Kiemelés eltávolítása + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Szín +pdfjs-editor-free-text-size-input = Méret +pdfjs-editor-ink-color-input = Szín +pdfjs-editor-ink-thickness-input = Vastagság +pdfjs-editor-ink-opacity-input = Átlátszatlanság +pdfjs-editor-stamp-add-image-button = + .title = Kép hozzáadása +pdfjs-editor-stamp-add-image-button-label = Kép hozzáadása +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Vastagság +pdfjs-editor-free-highlight-thickness-title = + .title = Vastagság módosítása, ha nem szöveges elemeket emel ki +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Szövegszerkesztő + .default-content = Kezdjen gépelni… +pdfjs-free-text = + .aria-label = Szövegszerkesztő +pdfjs-free-text-default-content = Kezdjen el gépelni… +pdfjs-ink = + .aria-label = Rajzszerkesztő +pdfjs-ink-canvas = + .aria-label = Felhasználó által készített kép + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatív szöveg +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternatív szöveg szerkesztése +pdfjs-editor-alt-text-edit-button-label = Alternatív szöveg szerkesztése +pdfjs-editor-alt-text-dialog-label = Válasszon egy lehetőséget +pdfjs-editor-alt-text-dialog-description = Az alternatív szöveg segít, ha az emberek nem látják a képet, vagy ha az nem töltődik be. +pdfjs-editor-alt-text-add-description-label = Leírás hozzáadása +pdfjs-editor-alt-text-add-description-description = Törekedjen 1-2 mondatra, amely jellemzi a témát, környezetet vagy cselekvést. +pdfjs-editor-alt-text-mark-decorative-label = Megjelölés dekoratívként +pdfjs-editor-alt-text-mark-decorative-description = Ez a díszítőképeknél használatos, mint a szegélyek vagy a vízjelek. +pdfjs-editor-alt-text-cancel-button = Mégse +pdfjs-editor-alt-text-save-button = Mentés +pdfjs-editor-alt-text-decorative-tooltip = Megjelölve dekoratívként +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Például: „Egy fiatal férfi leül enni egy asztalhoz” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatív szöveg + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Bal felső sarok – átméretezés +pdfjs-editor-resizer-label-top-middle = Felül középen – átméretezés +pdfjs-editor-resizer-label-top-right = Jobb felső sarok – átméretezés +pdfjs-editor-resizer-label-middle-right = Jobbra középen – átméretezés +pdfjs-editor-resizer-label-bottom-right = Jobb alsó sarok – átméretezés +pdfjs-editor-resizer-label-bottom-middle = Alul középen – átméretezés +pdfjs-editor-resizer-label-bottom-left = Bal alsó sarok – átméretezés +pdfjs-editor-resizer-label-middle-left = Balra középen – átméretezés +pdfjs-editor-resizer-top-left = + .aria-label = Bal felső sarok – átméretezés +pdfjs-editor-resizer-top-middle = + .aria-label = Felül középen – átméretezés +pdfjs-editor-resizer-top-right = + .aria-label = Jobb felső sarok – átméretezés +pdfjs-editor-resizer-middle-right = + .aria-label = Jobbra középen – átméretezés +pdfjs-editor-resizer-bottom-right = + .aria-label = Jobb alsó sarok – átméretezés +pdfjs-editor-resizer-bottom-middle = + .aria-label = Alul középen – átméretezés +pdfjs-editor-resizer-bottom-left = + .aria-label = Bal alsó sarok – átméretezés +pdfjs-editor-resizer-middle-left = + .aria-label = Balra középen – átméretezés + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Kiemelés színe +pdfjs-editor-colorpicker-button = + .title = Szín módosítása +pdfjs-editor-colorpicker-dropdown = + .aria-label = Színválasztások +pdfjs-editor-colorpicker-yellow = + .title = Sárga +pdfjs-editor-colorpicker-green = + .title = Zöld +pdfjs-editor-colorpicker-blue = + .title = Kék +pdfjs-editor-colorpicker-pink = + .title = Rózsaszín +pdfjs-editor-colorpicker-red = + .title = Vörös + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Összes megjelenítése +pdfjs-editor-highlight-show-all-button = + .title = Összes megjelenítése + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternatív szöveg szerkesztése (képleírás) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternatív szöveg hozzáadása (képleírás) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Írja ide a leírását… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Rövid leírás azoknak, akik nem látják a képet, vagy arra az esetre, ha a kép nem tölt be. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ez az alternatív szöveg automatikusan lett létrehozva, és pontatlan lehet. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = További tudnivalók +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternatív szöveg automatikus létrehozása +pdfjs-editor-new-alt-text-not-now-button = Most nem +pdfjs-editor-new-alt-text-error-title = Az alternatív szöveg automatikus létrehozása nem sikerült +pdfjs-editor-new-alt-text-error-description = Írja meg a saját alternatív szövegét, vagy próbálja újra később. +pdfjs-editor-new-alt-text-error-close-button = Bezárás +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Alternatív szöveg MI modell letöltése ({ $downloadedSize } / { $totalSize } MB) + .aria-valuetext = Alternatív szöveg MI modell letöltése ({ $downloadedSize } / { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatív szöveg hozzáadva +pdfjs-editor-new-alt-text-added-button-label = Alternatív szöveg hozzáadva +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Hiányzó alternatív szöveg +pdfjs-editor-new-alt-text-missing-button-label = Hiányzó alternatív szöveg +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternatív szöveg áttekintése +pdfjs-editor-new-alt-text-to-review-button-label = Alternatív szöveg szerkesztése +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatikusan létrehozva: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Kép alternatív szövegének beállításai +pdfjs-image-alt-text-settings-button-label = Kép alternatív szövegének beállításai +pdfjs-editor-alt-text-settings-dialog-label = Kép alternatív szövegének beállításai +pdfjs-editor-alt-text-settings-automatic-title = Automatikus alternatív szöveg +pdfjs-editor-alt-text-settings-create-model-button-label = Alternatív szöveg automatikus létrehozása +pdfjs-editor-alt-text-settings-create-model-description = Leírásokat javasol, hogy segítsen azoknak, akik nem látják a képet, vagy arra az esetre, ha a kép nem tölt be. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alternatív szöveg MI modellje ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Helyben fut az eszközén, így az adatai privátok maradnak. Az automatikus alternatív szövegekhez szükséges. +pdfjs-editor-alt-text-settings-delete-model-button = Törlés +pdfjs-editor-alt-text-settings-download-model-button = Letöltés +pdfjs-editor-alt-text-settings-downloading-model-button = Letöltés… +pdfjs-editor-alt-text-settings-editor-title = Alternatív szöveg szerkesztője +pdfjs-editor-alt-text-settings-show-dialog-button-label = Az alternatív szöveg szerkesztőjének azonnali megjelenítése egy kép hozzáadásakor +pdfjs-editor-alt-text-settings-show-dialog-description = Segít elérni, hogy az összes képén legyen alternatív szöveg. +pdfjs-editor-alt-text-settings-close-button = Bezárás + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Kiemelés eltávolítva +pdfjs-editor-undo-bar-message-freetext = Szöveg eltávolítva +pdfjs-editor-undo-bar-message-ink = Rajz eltávolítva +pdfjs-editor-undo-bar-message-stamp = Kép eltávolítva +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } kommentár eltávolítva + *[other] { $count } kommentár eltávolítva + } +pdfjs-editor-undo-bar-undo-button = + .title = Visszavonás +pdfjs-editor-undo-bar-undo-button-label = Visszavonás +pdfjs-editor-undo-bar-close-button = + .title = Bezárás +pdfjs-editor-undo-bar-close-button-label = Bezárás diff --git a/public/assets/pdfjs/locale/hy-AM/viewer.ftl b/public/assets/pdfjs/locale/hy-AM/viewer.ftl new file mode 100644 index 0000000..5c9dd27 --- /dev/null +++ b/public/assets/pdfjs/locale/hy-AM/viewer.ftl @@ -0,0 +1,272 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Նախորդ էջը +pdfjs-previous-button-label = Նախորդը +pdfjs-next-button = + .title = Հաջորդ էջը +pdfjs-next-button-label = Հաջորդը +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Էջ. +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = -ը՝ { $pagesCount }-ից +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber }-ը { $pagesCount })-ից +pdfjs-zoom-out-button = + .title = Փոքրացնել +pdfjs-zoom-out-button-label = Փոքրացնել +pdfjs-zoom-in-button = + .title = Խոշորացնել +pdfjs-zoom-in-button-label = Խոշորացնել +pdfjs-zoom-select = + .title = Դիտափոխում +pdfjs-presentation-mode-button = + .title = Անցնել Ներկայացման եղանակին +pdfjs-presentation-mode-button-label = Ներկայացման եղանակ +pdfjs-open-file-button = + .title = Բացել նիշք +pdfjs-open-file-button-label = Բացել +pdfjs-print-button = + .title = Տպել +pdfjs-print-button-label = Տպել +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Ներբեռնել +pdfjs-bookmark-button-label = Ընթացիկ էջ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Գործիքներ +pdfjs-tools-button-label = Գործիքներ +pdfjs-first-page-button = + .title = Անցնել առաջին էջին +pdfjs-first-page-button-label = Անցնել առաջին էջին +pdfjs-last-page-button = + .title = Անցնել վերջին էջին +pdfjs-last-page-button-label = Անցնել վերջին էջին +pdfjs-page-rotate-cw-button = + .title = Պտտել ըստ ժամացույցի սլաքի +pdfjs-page-rotate-cw-button-label = Պտտել ըստ ժամացույցի սլաքի +pdfjs-page-rotate-ccw-button = + .title = Պտտել հակառակ ժամացույցի սլաքի +pdfjs-page-rotate-ccw-button-label = Պտտել հակառակ ժամացույցի սլաքի +pdfjs-cursor-text-select-tool-button = + .title = Միացնել գրույթ ընտրելու գործիքը +pdfjs-cursor-text-select-tool-button-label = Գրույթը ընտրելու գործիք +pdfjs-cursor-hand-tool-button = + .title = Միացնել Ձեռքի գործիքը +pdfjs-cursor-hand-tool-button-label = Ձեռքի գործիք +pdfjs-scroll-vertical-button = + .title = Օգտագործել ուղղահայաց ոլորում +pdfjs-scroll-vertical-button-label = Ուղղահայաց ոլորում +pdfjs-scroll-horizontal-button = + .title = Օգտագործել հորիզոնական ոլորում +pdfjs-scroll-horizontal-button-label = Հորիզոնական ոլորում +pdfjs-scroll-wrapped-button = + .title = Օգտագործել փաթաթված ոլորում +pdfjs-scroll-wrapped-button-label = Փաթաթված ոլորում +pdfjs-spread-none-button = + .title = Մի միացեք էջի վերածածկերին +pdfjs-spread-none-button-label = Չկա վերածածկեր +pdfjs-spread-odd-button = + .title = Միացեք էջի վերածածկերին սկսելով՝ կենտ համարակալված էջերով +pdfjs-spread-odd-button-label = Կենտ վերածածկեր +pdfjs-spread-even-button = + .title = Միացեք էջի վերածածկերին սկսելով՝ զույգ համարակալված էջերով +pdfjs-spread-even-button-label = Զույգ վերածածկեր + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Փաստաթղթի հատկությունները… +pdfjs-document-properties-button-label = Փաստաթղթի հատկությունները… +pdfjs-document-properties-file-name = Նիշքի անունը. +pdfjs-document-properties-file-size = Նիշք չափը. +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } ԿԲ ({ $size_b } բայթ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } ՄԲ ({ $size_b } բայթ) +pdfjs-document-properties-title = Վերնագիր. +pdfjs-document-properties-author = Հեղինակ․ +pdfjs-document-properties-subject = Վերնագիր. +pdfjs-document-properties-keywords = Հիմնաբառ. +pdfjs-document-properties-creation-date = Ստեղծելու ամսաթիվը. +pdfjs-document-properties-modification-date = Փոփոխելու ամսաթիվը. +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Ստեղծող. +pdfjs-document-properties-producer = PDF-ի հեղինակը. +pdfjs-document-properties-version = PDF-ի տարբերակը. +pdfjs-document-properties-page-count = Էջերի քանակը. +pdfjs-document-properties-page-size = Էջի չափը. +pdfjs-document-properties-page-size-unit-inches = ում +pdfjs-document-properties-page-size-unit-millimeters = մմ +pdfjs-document-properties-page-size-orientation-portrait = ուղղաձիգ +pdfjs-document-properties-page-size-orientation-landscape = հորիզոնական +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Նամակ +pdfjs-document-properties-page-size-name-legal = Օրինական + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Արագ վեբ դիտում․ +pdfjs-document-properties-linearized-yes = Այո +pdfjs-document-properties-linearized-no = Ոչ +pdfjs-document-properties-close-button = Փակել + +## Print + +pdfjs-print-progress-message = Նախապատրաստում է փաստաթուղթը տպելուն... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Չեղարկել +pdfjs-printing-not-supported = Զգուշացում. Տպելը ամբողջությամբ չի աջակցվում դիտարկիչի կողմից։ +pdfjs-printing-not-ready = Զգուշացում. PDF-ը ամբողջությամբ չի բեռնավորվել տպելու համար: + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Բացել/Փակել Կողային վահանակը +pdfjs-toggle-sidebar-button-label = Բացել/Փակել Կողային վահանակը +pdfjs-document-outline-button = + .title = Ցուցադրել փաստաթղթի ուրվագիծը (կրկնակի սեղմեք՝ միավորները ընդարձակելու/կոծկելու համար) +pdfjs-document-outline-button-label = Փաստաթղթի բովանդակությունը +pdfjs-attachments-button = + .title = Ցուցադրել կցորդները +pdfjs-attachments-button-label = Կցորդներ +pdfjs-thumbs-button = + .title = Ցուցադրել Մանրապատկերը +pdfjs-thumbs-button-label = Մանրապատկերը +pdfjs-findbar-button = + .title = Գտնել փաստաթղթում +pdfjs-findbar-button-label = Որոնում + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Էջը { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Էջի մանրապատկերը { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Որոնում + .placeholder = Գտնել փաստաթղթում... +pdfjs-find-previous-button = + .title = Գտնել անրահայտության նախորդ հանդիպումը +pdfjs-find-previous-button-label = Նախորդը +pdfjs-find-next-button = + .title = Գտիր արտահայտության հաջորդ հանդիպումը +pdfjs-find-next-button-label = Հաջորդը +pdfjs-find-highlight-checkbox = Գունանշել բոլորը +pdfjs-find-match-case-checkbox-label = Մեծ(փոքր)ատառ հաշվի առնել +pdfjs-find-entire-word-checkbox-label = Ամբողջ բառերը +pdfjs-find-reached-top = Հասել եք փաստաթղթի վերևին, կշարունակվի ներքևից +pdfjs-find-reached-bottom = Հասել եք փաստաթղթի վերջին, կշարունակվի վերևից +pdfjs-find-not-found = Արտահայտությունը չգտնվեց + +## Predefined zoom values + +pdfjs-page-scale-width = Էջի լայնքը +pdfjs-page-scale-fit = Ձգել էջը +pdfjs-page-scale-auto = Ինքնաշխատ +pdfjs-page-scale-actual = Իրական չափը +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Սխալ՝ PDF ֆայլը բացելիս։ +pdfjs-invalid-file-error = Սխալ կամ վնասված PDF ֆայլ: +pdfjs-missing-file-error = PDF ֆայլը բացակայում է: +pdfjs-unexpected-response-error = Սպասարկիչի անսպասելի պատասխան: +pdfjs-rendering-error = Սխալ՝ էջը ստեղծելիս: + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Ծանոթություն] + +## Password + +pdfjs-password-label = Մուտքագրեք PDF-ի գաղտնաբառը: +pdfjs-password-invalid = Գաղտնաբառը սխալ է: Կրկին փորձեք: +pdfjs-password-ok-button = Լավ +pdfjs-password-cancel-button = Չեղարկել +pdfjs-web-fonts-disabled = Վեբ-տառատեսակները անջատված են. հնարավոր չէ օգտագործել ներկառուցված PDF տառատեսակները: + +## Editing + + +## Remove button for the various kind of editor. + + +## + +pdfjs-free-text-default-content = Սկսել մուտքագրումը… + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Ցուցադրել բոլորը +pdfjs-editor-highlight-show-all-button = + .title = Ցուցադրել բոլորը diff --git a/public/assets/pdfjs/locale/hye/viewer.ftl b/public/assets/pdfjs/locale/hye/viewer.ftl new file mode 100644 index 0000000..75cdc06 --- /dev/null +++ b/public/assets/pdfjs/locale/hye/viewer.ftl @@ -0,0 +1,268 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Նախորդ էջ +pdfjs-previous-button-label = Նախորդը +pdfjs-next-button = + .title = Յաջորդ էջ +pdfjs-next-button-label = Յաջորդը +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = էջ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount }-ից +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber }-ը { $pagesCount })-ից +pdfjs-zoom-out-button = + .title = Փոքրացնել +pdfjs-zoom-out-button-label = Փոքրացնել +pdfjs-zoom-in-button = + .title = Խոշորացնել +pdfjs-zoom-in-button-label = Խոշորացնել +pdfjs-zoom-select = + .title = Խոշորացում +pdfjs-presentation-mode-button = + .title = Անցնել ներկայացման եղանակին +pdfjs-presentation-mode-button-label = Ներկայացման եղանակ +pdfjs-open-file-button = + .title = Բացել նիշքը +pdfjs-open-file-button-label = Բացել +pdfjs-print-button = + .title = Տպել +pdfjs-print-button-label = Տպել + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Գործիքներ +pdfjs-tools-button-label = Գործիքներ +pdfjs-first-page-button = + .title = Գնալ դէպի առաջին էջ +pdfjs-first-page-button-label = Գնալ դէպի առաջին էջ +pdfjs-last-page-button = + .title = Գնալ դէպի վերջին էջ +pdfjs-last-page-button-label = Գնալ դէպի վերջին էջ +pdfjs-page-rotate-cw-button = + .title = Պտտել ժամացոյցի սլաքի ուղղութեամբ +pdfjs-page-rotate-cw-button-label = Պտտել ժամացոյցի սլաքի ուղղութեամբ +pdfjs-page-rotate-ccw-button = + .title = Պտտել ժամացոյցի սլաքի հակառակ ուղղութեամբ +pdfjs-page-rotate-ccw-button-label = Պտտել ժամացոյցի սլաքի հակառակ ուղղութեամբ +pdfjs-cursor-text-select-tool-button = + .title = Միացնել գրոյթ ընտրելու գործիքը +pdfjs-cursor-text-select-tool-button-label = Գրուածք ընտրելու գործիք +pdfjs-cursor-hand-tool-button = + .title = Միացնել ձեռքի գործիքը +pdfjs-cursor-hand-tool-button-label = Ձեռքի գործիք +pdfjs-scroll-page-button = + .title = Աւգտագործել էջի ոլորում +pdfjs-scroll-page-button-label = Էջի ոլորում +pdfjs-scroll-vertical-button = + .title = Աւգտագործել ուղղահայեաց ոլորում +pdfjs-scroll-vertical-button-label = Ուղղահայեաց ոլորում +pdfjs-scroll-horizontal-button = + .title = Աւգտագործել հորիզոնական ոլորում +pdfjs-scroll-horizontal-button-label = Հորիզոնական ոլորում +pdfjs-scroll-wrapped-button = + .title = Աւգտագործել փաթաթուած ոլորում +pdfjs-scroll-wrapped-button-label = Փաթաթուած ոլորում +pdfjs-spread-none-button = + .title = Մի միացէք էջի կոնտեքստում +pdfjs-spread-none-button-label = Չկայ կոնտեքստ +pdfjs-spread-odd-button = + .title = Միացէք էջի կոնտեքստին սկսելով՝ կենտ համարակալուած էջերով +pdfjs-spread-odd-button-label = Տարաւրինակ կոնտեքստ +pdfjs-spread-even-button = + .title = Միացէք էջի կոնտեքստին սկսելով՝ զոյգ համարակալուած էջերով +pdfjs-spread-even-button-label = Հաւասար վերածածկեր + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Փաստաթղթի հատկութիւնները… +pdfjs-document-properties-button-label = Փաստաթղթի յատկութիւնները… +pdfjs-document-properties-file-name = Նիշքի անունը․ +pdfjs-document-properties-file-size = Նիշք չափը. +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } ԿԲ ({ $size_b } բայթ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } ՄԲ ({ $size_b } բայթ) +pdfjs-document-properties-title = Վերնագիր +pdfjs-document-properties-author = Հեղինակ․ +pdfjs-document-properties-subject = առարկայ +pdfjs-document-properties-keywords = Հիմնաբառեր +pdfjs-document-properties-creation-date = Ստեղծման ամսաթիւ +pdfjs-document-properties-modification-date = Փոփոխութեան ամսաթիւ. +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Ստեղծող +pdfjs-document-properties-producer = PDF-ի Արտադրողը. +pdfjs-document-properties-version = PDF-ի տարբերակը. +pdfjs-document-properties-page-count = Էջերի քանակը. +pdfjs-document-properties-page-size = Էջի չափը. +pdfjs-document-properties-page-size-unit-inches = ում +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = ուղղաձիգ +pdfjs-document-properties-page-size-orientation-landscape = հորիզոնական +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Նամակ +pdfjs-document-properties-page-size-name-legal = Աւրինական + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Արագ վեբ դիտում․ +pdfjs-document-properties-linearized-yes = Այո +pdfjs-document-properties-linearized-no = Ոչ +pdfjs-document-properties-close-button = Փակել + +## Print + +pdfjs-print-progress-message = Նախապատրաստում է փաստաթուղթը տպելուն… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Չեղարկել +pdfjs-printing-not-supported = Զգուշացում. Տպելը ամբողջութեամբ չի աջակցուում զննարկիչի կողմից։ +pdfjs-printing-not-ready = Զգուշացում. PDF֊ը ամբողջութեամբ չի բեռնաւորուել տպելու համար։ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Փոխարկել կողային վահանակը +pdfjs-toggle-sidebar-notification-button = + .title = Փոխանջատել կողմնասիւնը (փաստաթուղթը պարունակում է ուրուագիծ/կցորդներ/շերտեր) +pdfjs-toggle-sidebar-button-label = Փոխարկել կողային վահանակը +pdfjs-document-outline-button = + .title = Ցուցադրել փաստաթղթի ուրուագիծը (կրկնակի սեղմէք՝ միաւորները ընդարձակելու/կոծկելու համար) +pdfjs-document-outline-button-label = Փաստաթղթի ուրուագիծ +pdfjs-attachments-button = + .title = Ցուցադրել կցորդները +pdfjs-attachments-button-label = Կցորդներ +pdfjs-layers-button = + .title = Ցուցադրել շերտերը (կրկնահպել վերակայելու բոլոր շերտերը սկզբնադիր վիճակի) +pdfjs-layers-button-label = Շերտեր +pdfjs-thumbs-button = + .title = Ցուցադրել մանրապատկերը +pdfjs-thumbs-button-label = Մանրապատկեր +pdfjs-current-outline-item-button = + .title = Գտէք ընթացիկ գծագրման տարրը +pdfjs-current-outline-item-button-label = Ընթացիկ գծագրման տարր +pdfjs-findbar-button = + .title = Գտնել փաստաթղթում +pdfjs-findbar-button-label = Որոնում +pdfjs-additional-layers = Լրացուցիչ շերտեր + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Էջը { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Էջի մանրապատկերը { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Որոնում + .placeholder = Գտնել փաստաթղթում… +pdfjs-find-previous-button = + .title = Գտնել արտայայտութեան նախորդ արտայայտութիւնը +pdfjs-find-previous-button-label = Նախորդը +pdfjs-find-next-button = + .title = Գտիր արտայայտութեան յաջորդ արտայայտութիւնը +pdfjs-find-next-button-label = Հաջորդը +pdfjs-find-highlight-checkbox = Գունանշել բոլորը +pdfjs-find-match-case-checkbox-label = Հաշուի առնել հանգամանքը +pdfjs-find-match-diacritics-checkbox-label = Հնչիւնատարբերիչ նշանների համապատասխանեցում +pdfjs-find-entire-word-checkbox-label = Ամբողջ բառերը +pdfjs-find-reached-top = Հասել եք փաստաթղթի վերեւին,շարունակել ներքեւից +pdfjs-find-reached-bottom = Հասել էք փաստաթղթի վերջին, շարունակել վերեւից +pdfjs-find-not-found = Արտայայտութիւնը չգտնուեց + +## Predefined zoom values + +pdfjs-page-scale-width = Էջի լայնութիւն +pdfjs-page-scale-fit = Հարմարեցնել էջը +pdfjs-page-scale-auto = Ինքնաշխատ խոշորացում +pdfjs-page-scale-actual = Իրական չափը +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Էջ { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF նիշքը բացելիս սխալ է տեղի ունեցել։ +pdfjs-invalid-file-error = Սխալ կամ վնասուած PDF նիշք։ +pdfjs-missing-file-error = PDF նիշքը բացակաիւմ է։ +pdfjs-unexpected-response-error = Սպասարկիչի անսպասելի պատասխան։ +pdfjs-rendering-error = Սխալ է տեղի ունեցել էջի մեկնաբանման ժամանակ + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Ծանոթութիւն] + +## Password + +pdfjs-password-label = Մուտքագրէք գաղտնաբառը այս PDF նիշքը բացելու համար +pdfjs-password-invalid = Գաղտնաբառը սխալ է: Կրկին փորձէք: +pdfjs-password-ok-button = Լաւ +pdfjs-password-cancel-button = Չեղարկել +pdfjs-web-fonts-disabled = Վեբ-տառատեսակները անջատուած են. հնարաւոր չէ աւգտագործել ներկառուցուած PDF տառատեսակները։ + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ia/viewer.ftl b/public/assets/pdfjs/locale/ia/viewer.ftl new file mode 100644 index 0000000..91fbaf9 --- /dev/null +++ b/public/assets/pdfjs/locale/ia/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina previe +pdfjs-previous-button-label = Previe +pdfjs-next-button = + .title = Pagina sequente +pdfjs-next-button-label = Sequente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Distantiar +pdfjs-zoom-out-button-label = Distantiar +pdfjs-zoom-in-button = + .title = Approximar +pdfjs-zoom-in-button-label = Approximar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Excambiar a modo presentation +pdfjs-presentation-mode-button-label = Modo presentation +pdfjs-open-file-button = + .title = Aperir le file +pdfjs-open-file-button-label = Aperir +pdfjs-print-button = + .title = Imprimer +pdfjs-print-button-label = Imprimer +pdfjs-save-button = + .title = Salvar +pdfjs-save-button-label = Salvar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Discargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Discargar +pdfjs-bookmark-button = + .title = Pagina actual (vide le URL del pagina actual) +pdfjs-bookmark-button-label = Pagina actual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Instrumentos +pdfjs-tools-button-label = Instrumentos +pdfjs-first-page-button = + .title = Ir al prime pagina +pdfjs-first-page-button-label = Ir al prime pagina +pdfjs-last-page-button = + .title = Ir al ultime pagina +pdfjs-last-page-button-label = Ir al ultime pagina +pdfjs-page-rotate-cw-button = + .title = Rotar in senso horari +pdfjs-page-rotate-cw-button-label = Rotar in senso horari +pdfjs-page-rotate-ccw-button = + .title = Rotar in senso antihorari +pdfjs-page-rotate-ccw-button-label = Rotar in senso antihorari +pdfjs-cursor-text-select-tool-button = + .title = Activar le instrumento de selection de texto +pdfjs-cursor-text-select-tool-button-label = Instrumento de selection de texto +pdfjs-cursor-hand-tool-button = + .title = Activar le instrumento mano +pdfjs-cursor-hand-tool-button-label = Instrumento mano +pdfjs-scroll-page-button = + .title = Usar rolamento de pagina +pdfjs-scroll-page-button-label = Rolamento de pagina +pdfjs-scroll-vertical-button = + .title = Usar rolamento vertical +pdfjs-scroll-vertical-button-label = Rolamento vertical +pdfjs-scroll-horizontal-button = + .title = Usar rolamento horizontal +pdfjs-scroll-horizontal-button-label = Rolamento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar rolamento incapsulate +pdfjs-scroll-wrapped-button-label = Rolamento incapsulate +pdfjs-spread-none-button = + .title = Non junger paginas dual +pdfjs-spread-none-button-label = Sin paginas dual +pdfjs-spread-odd-button = + .title = Junger paginas dual a partir de paginas con numeros impar +pdfjs-spread-odd-button-label = Paginas dual impar +pdfjs-spread-even-button = + .title = Junger paginas dual a partir de paginas con numeros par +pdfjs-spread-even-button-label = Paginas dual par + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Proprietates del documento… +pdfjs-document-properties-button-label = Proprietates del documento… +pdfjs-document-properties-file-name = Nomine del file: +pdfjs-document-properties-file-size = Dimension de file: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titulo: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Subjecto: +pdfjs-document-properties-keywords = Parolas clave: +pdfjs-document-properties-creation-date = Data de creation: +pdfjs-document-properties-modification-date = Data de modification: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creator: +pdfjs-document-properties-producer = Productor PDF: +pdfjs-document-properties-version = Version PDF: +pdfjs-document-properties-page-count = Numero de paginas: +pdfjs-document-properties-page-size = Dimension del pagina: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = horizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Littera +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web rapide: +pdfjs-document-properties-linearized-yes = Si +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Clauder + +## Print + +pdfjs-print-progress-message = Preparation del documento pro le impression… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancellar +pdfjs-printing-not-supported = Attention : le impression non es totalmente supportate per ce navigator. +pdfjs-printing-not-ready = Attention: le file PDF non es integremente cargate pro lo poter imprimer. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Monstrar/celar le barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Monstrar/celar le barra lateral (le documento contine structura/attachamentos/stratos) +pdfjs-toggle-sidebar-button-label = Monstrar/celar le barra lateral +pdfjs-document-outline-button = + .title = Monstrar le schema del documento (clic duple pro expander/contraher tote le elementos) +pdfjs-document-outline-button-label = Schema del documento +pdfjs-attachments-button = + .title = Monstrar le annexos +pdfjs-attachments-button-label = Annexos +pdfjs-layers-button = + .title = Monstrar stratos (clicca duple pro remontar tote le stratos al stato predefinite) +pdfjs-layers-button-label = Stratos +pdfjs-thumbs-button = + .title = Monstrar le vignettes +pdfjs-thumbs-button-label = Vignettes +pdfjs-current-outline-item-button = + .title = Trovar le elemento de structura actual +pdfjs-current-outline-item-button-label = Elemento de structura actual +pdfjs-findbar-button = + .title = Cercar in le documento +pdfjs-findbar-button-label = Cercar +pdfjs-additional-layers = Altere stratos + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Vignette del pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Cercar + .placeholder = Cercar in le documento… +pdfjs-find-previous-button = + .title = Trovar le previe occurrentia del phrase +pdfjs-find-previous-button-label = Previe +pdfjs-find-next-button = + .title = Trovar le successive occurrentia del phrase +pdfjs-find-next-button-label = Sequente +pdfjs-find-highlight-checkbox = Evidentiar toto +pdfjs-find-match-case-checkbox-label = Distinguer majusculas/minusculas +pdfjs-find-match-diacritics-checkbox-label = Differentiar diacriticos +pdfjs-find-entire-word-checkbox-label = Parolas integre +pdfjs-find-reached-top = Initio del documento attingite, continuation ab fin +pdfjs-find-reached-bottom = Fin del documento attingite, continuation ab initio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } correspondentia + *[other] { $current } de { $total } correspondentias + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Plus de { $limit } correspondentia + *[other] Plus de { $limit } correspondentias + } +pdfjs-find-not-found = Phrase non trovate + +## Predefined zoom values + +pdfjs-page-scale-width = Plen largor del pagina +pdfjs-page-scale-fit = Pagina integre +pdfjs-page-scale-auto = Zoom automatic +pdfjs-page-scale-actual = Dimension real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Un error occurreva durante que on cargava le file PDF. +pdfjs-invalid-file-error = File PDF corrumpite o non valide. +pdfjs-missing-file-error = File PDF mancante. +pdfjs-unexpected-response-error = Responsa del servitor inexpectate. +pdfjs-rendering-error = Un error occurreva durante que on processava le pagina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Insere le contrasigno pro aperir iste file PDF. +pdfjs-password-invalid = Contrasigno invalide. Per favor retenta. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancellar +pdfjs-web-fonts-disabled = Le typos de litteras web es disactivate: impossibile usar le typos de litteras PDF incorporate. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Designar +pdfjs-editor-ink-button-label = Designar +pdfjs-editor-stamp-button = + .title = Adder o rediger imagines +pdfjs-editor-stamp-button-label = Adder o rediger imagines +pdfjs-editor-highlight-button = + .title = Evidentia +pdfjs-editor-highlight-button-label = Evidentia +pdfjs-highlight-floating-button1 = + .title = Evidentiar + .aria-label = Evidentiar +pdfjs-highlight-floating-button-label = Evidentiar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remover le designo +pdfjs-editor-remove-freetext-button = + .title = Remover texto +pdfjs-editor-remove-stamp-button = + .title = Remover imagine +pdfjs-editor-remove-highlight-button = + .title = Remover evidentia + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Dimension +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Spissor +pdfjs-editor-ink-opacity-input = Opacitate +pdfjs-editor-stamp-add-image-button = + .title = Adder imagine +pdfjs-editor-stamp-add-image-button-label = Adder imagine +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Spissor +pdfjs-editor-free-highlight-thickness-title = + .title = Cambiar spissor evidentiante elementos differente de texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Initiar a inserer… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Comenciar a scriber… +pdfjs-ink = + .aria-label = Editor de designos +pdfjs-ink-canvas = + .aria-label = Imagine create per le usator + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternative +pdfjs-editor-alt-text-edit-button = + .aria-label = Rediger texto alternative +pdfjs-editor-alt-text-edit-button-label = Rediger texto alternative +pdfjs-editor-alt-text-dialog-label = Elige un option +pdfjs-editor-alt-text-dialog-description = Le texto alternative (alt text) adjuta quando le personas non pote vider le imagine o quando illo non carga. +pdfjs-editor-alt-text-add-description-label = Adder un description +pdfjs-editor-alt-text-add-description-description = Mira a 1-2 phrases que describe le subjecto, parametro, o actiones. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorative +pdfjs-editor-alt-text-mark-decorative-description = Isto es usate pro imagines ornamental, como bordaturas o filigranas. +pdfjs-editor-alt-text-cancel-button = Cancellar +pdfjs-editor-alt-text-save-button = Salvar +pdfjs-editor-alt-text-decorative-tooltip = Marcate como decorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Per exemplo, “Un juvene sede a un tabula pro mangiar un repasto” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternative + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Angulo superior sinistre — redimensionar +pdfjs-editor-resizer-label-top-middle = Medio superior — redimensionar +pdfjs-editor-resizer-label-top-right = Angulo superior dextre — redimensionar +pdfjs-editor-resizer-label-middle-right = Medio dextre — redimensionar +pdfjs-editor-resizer-label-bottom-right = Angulo inferior dextre — redimensionar +pdfjs-editor-resizer-label-bottom-middle = Medio inferior — redimensionar +pdfjs-editor-resizer-label-bottom-left = Angulo inferior sinistre — redimensionar +pdfjs-editor-resizer-label-middle-left = Medio sinistre — redimensionar +pdfjs-editor-resizer-top-left = + .aria-label = Angulo superior sinistre — redimensionar +pdfjs-editor-resizer-top-middle = + .aria-label = Medio superior — redimensionar +pdfjs-editor-resizer-top-right = + .aria-label = Angulo superior dextre — redimensionar +pdfjs-editor-resizer-middle-right = + .aria-label = Medio dextre — redimensionar +pdfjs-editor-resizer-bottom-right = + .aria-label = Angulo inferior dextre — redimensionar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Medio inferior — redimensionar +pdfjs-editor-resizer-bottom-left = + .aria-label = Angulo inferior sinistre — redimensionar +pdfjs-editor-resizer-middle-left = + .aria-label = Medio sinistre — redimensionar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color pro evidentiar +pdfjs-editor-colorpicker-button = + .title = Cambiar color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Electiones del color +pdfjs-editor-colorpicker-yellow = + .title = Jalne +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Rosate +pdfjs-editor-colorpicker-red = + .title = Rubie + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Monstrar toto +pdfjs-editor-highlight-show-all-button = + .title = Monstrar toto + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Rediger texto alternative (description del imagine) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Adder texto alternative (description del imagine) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Scribe tu description ci… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Breve description pro personas qui non pote vider le imagine o quando le imagine non se carga. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Iste texto alternative ha essite create automaticamente e pote esser inexacte. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Pro saper plus +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear texto alternative automaticamente +pdfjs-editor-new-alt-text-not-now-button = Non ora +pdfjs-editor-new-alt-text-error-title = Impossibile crear texto alternative automaticamente +pdfjs-editor-new-alt-text-error-description = Scribe tu proprie texto alternative o retenta plus tarde. +pdfjs-editor-new-alt-text-error-close-button = Clauder +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Discargante modello de intelligentia artificial del texto alternative ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Discargante modello de intelligentia artificial del texto alternative ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Texto alternative addite +pdfjs-editor-new-alt-text-added-button-label = Texto alternative addite +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Texto alternative mancante +pdfjs-editor-new-alt-text-missing-button-label = Texto alternative mancante +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Revider texto alternative +pdfjs-editor-new-alt-text-to-review-button-label = Revider texto alternative +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automaticamente create: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Parametros del texto alternative del imagine +pdfjs-image-alt-text-settings-button-label = Parametros del texto alternative del imagine +pdfjs-editor-alt-text-settings-dialog-label = Parametros del texto alternative del imagine +pdfjs-editor-alt-text-settings-automatic-title = Texto alternative automatic +pdfjs-editor-alt-text-settings-create-model-button-label = Crear texto alternative automaticamente +pdfjs-editor-alt-text-settings-create-model-description = Suggere descriptiones pro adjutar le personas qui non pote vider le imagine o quando le imagine non carga. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modello de intelligentia artificial del texto alternative ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Flue localmente sur tu apparato assi tu datos remane private. Necessari pro texto alternative automatic. +pdfjs-editor-alt-text-settings-delete-model-button = Deler +pdfjs-editor-alt-text-settings-download-model-button = Discargar +pdfjs-editor-alt-text-settings-downloading-model-button = Discargante… +pdfjs-editor-alt-text-settings-editor-title = Rediger texto alternative +pdfjs-editor-alt-text-settings-show-dialog-button-label = Monstrar le redactor de texto alternative a pena on adde un imagine +pdfjs-editor-alt-text-settings-show-dialog-description = Te adjuta a verifica que tote tu imagines ha un texto alternative. +pdfjs-editor-alt-text-settings-close-button = Clauder + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Evidentiation removite +pdfjs-editor-undo-bar-message-freetext = Texto removite +pdfjs-editor-undo-bar-message-ink = Designo removite +pdfjs-editor-undo-bar-message-stamp = Imagine removite +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotation removite + *[other] { $count } annotationes removite + } +pdfjs-editor-undo-bar-undo-button = + .title = Disfacer +pdfjs-editor-undo-bar-undo-button-label = Disfacer +pdfjs-editor-undo-bar-close-button = + .title = Clauder +pdfjs-editor-undo-bar-close-button-label = Clauder diff --git a/public/assets/pdfjs/locale/id/viewer.ftl b/public/assets/pdfjs/locale/id/viewer.ftl new file mode 100644 index 0000000..c985a33 --- /dev/null +++ b/public/assets/pdfjs/locale/id/viewer.ftl @@ -0,0 +1,374 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Laman Sebelumnya +pdfjs-previous-button-label = Sebelumnya +pdfjs-next-button = + .title = Laman Selanjutnya +pdfjs-next-button-label = Selanjutnya +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Halaman +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = dari { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } dari { $pagesCount }) +pdfjs-zoom-out-button = + .title = Perkecil +pdfjs-zoom-out-button-label = Perkecil +pdfjs-zoom-in-button = + .title = Perbesar +pdfjs-zoom-in-button-label = Perbesar +pdfjs-zoom-select = + .title = Perbesaran +pdfjs-presentation-mode-button = + .title = Ganti ke Mode Presentasi +pdfjs-presentation-mode-button-label = Mode Presentasi +pdfjs-open-file-button = + .title = Buka Berkas +pdfjs-open-file-button-label = Buka +pdfjs-print-button = + .title = Cetak +pdfjs-print-button-label = Cetak +pdfjs-save-button = + .title = Simpan +pdfjs-save-button-label = Simpan +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Unduh +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Unduh +pdfjs-bookmark-button = + .title = Laman Saat Ini (Lihat URL dari Laman Sekarang) +pdfjs-bookmark-button-label = Laman Saat Ini + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Alat +pdfjs-tools-button-label = Alat +pdfjs-first-page-button = + .title = Buka Halaman Pertama +pdfjs-first-page-button-label = Buka Halaman Pertama +pdfjs-last-page-button = + .title = Buka Halaman Terakhir +pdfjs-last-page-button-label = Buka Halaman Terakhir +pdfjs-page-rotate-cw-button = + .title = Putar Searah Jarum Jam +pdfjs-page-rotate-cw-button-label = Putar Searah Jarum Jam +pdfjs-page-rotate-ccw-button = + .title = Putar Berlawanan Arah Jarum Jam +pdfjs-page-rotate-ccw-button-label = Putar Berlawanan Arah Jarum Jam +pdfjs-cursor-text-select-tool-button = + .title = Aktifkan Alat Seleksi Teks +pdfjs-cursor-text-select-tool-button-label = Alat Seleksi Teks +pdfjs-cursor-hand-tool-button = + .title = Aktifkan Alat Tangan +pdfjs-cursor-hand-tool-button-label = Alat Tangan +pdfjs-scroll-page-button = + .title = Gunakan Pengguliran Laman +pdfjs-scroll-page-button-label = Pengguliran Laman +pdfjs-scroll-vertical-button = + .title = Gunakan Penggeseran Vertikal +pdfjs-scroll-vertical-button-label = Penggeseran Vertikal +pdfjs-scroll-horizontal-button = + .title = Gunakan Penggeseran Horizontal +pdfjs-scroll-horizontal-button-label = Penggeseran Horizontal +pdfjs-scroll-wrapped-button = + .title = Gunakan Penggeseran Terapit +pdfjs-scroll-wrapped-button-label = Penggeseran Terapit +pdfjs-spread-none-button = + .title = Jangan gabungkan lembar halaman +pdfjs-spread-none-button-label = Tidak Ada Lembaran +pdfjs-spread-odd-button = + .title = Gabungkan lembar lamanan mulai dengan halaman ganjil +pdfjs-spread-odd-button-label = Lembaran Ganjil +pdfjs-spread-even-button = + .title = Gabungkan lembar halaman dimulai dengan halaman genap +pdfjs-spread-even-button-label = Lembaran Genap + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Properti Dokumen… +pdfjs-document-properties-button-label = Properti Dokumen… +pdfjs-document-properties-file-name = Nama berkas: +pdfjs-document-properties-file-size = Ukuran berkas: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Judul: +pdfjs-document-properties-author = Penyusun: +pdfjs-document-properties-subject = Subjek: +pdfjs-document-properties-keywords = Kata Kunci: +pdfjs-document-properties-creation-date = Tanggal Dibuat: +pdfjs-document-properties-modification-date = Tanggal Dimodifikasi: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Pembuat: +pdfjs-document-properties-producer = Pemroduksi PDF: +pdfjs-document-properties-version = Versi PDF: +pdfjs-document-properties-page-count = Jumlah Halaman: +pdfjs-document-properties-page-size = Ukuran Laman: +pdfjs-document-properties-page-size-unit-inches = inci +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = tegak +pdfjs-document-properties-page-size-orientation-landscape = mendatar +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Tampilan Web Kilat: +pdfjs-document-properties-linearized-yes = Ya +pdfjs-document-properties-linearized-no = Tidak +pdfjs-document-properties-close-button = Tutup + +## Print + +pdfjs-print-progress-message = Menyiapkan dokumen untuk pencetakan… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Batalkan +pdfjs-printing-not-supported = Peringatan: Pencetakan tidak didukung secara lengkap pada peramban ini. +pdfjs-printing-not-ready = Peringatan: Berkas PDF masih belum dimuat secara lengkap untuk dapat dicetak. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Aktif/Nonaktifkan Bilah Samping +pdfjs-toggle-sidebar-notification-button = + .title = Aktif/Nonaktifkan Bilah Samping (dokumen berisi kerangka/lampiran/lapisan) +pdfjs-toggle-sidebar-button-label = Aktif/Nonaktifkan Bilah Samping +pdfjs-document-outline-button = + .title = Tampilkan Kerangka Dokumen (klik ganda untuk membentangkan/menciutkan semua item) +pdfjs-document-outline-button-label = Kerangka Dokumen +pdfjs-attachments-button = + .title = Tampilkan Lampiran +pdfjs-attachments-button-label = Lampiran +pdfjs-layers-button = + .title = Tampilkan Lapisan (klik ganda untuk mengatur ulang semua lapisan ke keadaan baku) +pdfjs-layers-button-label = Lapisan +pdfjs-thumbs-button = + .title = Tampilkan Miniatur +pdfjs-thumbs-button-label = Miniatur +pdfjs-current-outline-item-button = + .title = Cari Butir Ikhtisar Saat Ini +pdfjs-current-outline-item-button-label = Butir Ikhtisar Saat Ini +pdfjs-findbar-button = + .title = Temukan di Dokumen +pdfjs-findbar-button-label = Temukan +pdfjs-additional-layers = Lapisan Tambahan + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Laman { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatur Laman { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Temukan + .placeholder = Temukan di dokumen… +pdfjs-find-previous-button = + .title = Temukan kata sebelumnya +pdfjs-find-previous-button-label = Sebelumnya +pdfjs-find-next-button = + .title = Temukan lebih lanjut +pdfjs-find-next-button-label = Selanjutnya +pdfjs-find-highlight-checkbox = Sorot semuanya +pdfjs-find-match-case-checkbox-label = Cocokkan BESAR/kecil +pdfjs-find-match-diacritics-checkbox-label = Pencocokan Diakritik +pdfjs-find-entire-word-checkbox-label = Seluruh teks +pdfjs-find-reached-top = Sampai di awal dokumen, dilanjutkan dari bawah +pdfjs-find-reached-bottom = Sampai di akhir dokumen, dilanjutkan dari atas +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = { $current } dari { $total } yang cocok +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = Lebih dari { $limit } kecocokan +pdfjs-find-not-found = Frasa tidak ditemukan + +## Predefined zoom values + +pdfjs-page-scale-width = Lebar Laman +pdfjs-page-scale-fit = Muat Laman +pdfjs-page-scale-auto = Perbesaran Otomatis +pdfjs-page-scale-actual = Ukuran Asli +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Halaman { $page } + +## Loading indicator messages + +pdfjs-loading-error = Galat terjadi saat memuat PDF. +pdfjs-invalid-file-error = Berkas PDF tidak valid atau rusak. +pdfjs-missing-file-error = Berkas PDF tidak ada. +pdfjs-unexpected-response-error = Balasan server yang tidak diharapkan. +pdfjs-rendering-error = Galat terjadi saat merender laman. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotasi { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Masukkan sandi untuk membuka berkas PDF ini. +pdfjs-password-invalid = Sandi tidak valid. Silakan coba lagi. +pdfjs-password-ok-button = Oke +pdfjs-password-cancel-button = Batal +pdfjs-web-fonts-disabled = Font web dinonaktifkan: tidak dapat menggunakan font PDF yang tersemat. + +## Editing + +pdfjs-editor-free-text-button = + .title = Teks +pdfjs-editor-free-text-button-label = Teks +pdfjs-editor-ink-button = + .title = Gambar +pdfjs-editor-ink-button-label = Gambar +pdfjs-editor-stamp-button = + .title = Tambah atau edit gambar +pdfjs-editor-stamp-button-label = Tambah atau edit gambar +pdfjs-editor-highlight-button = + .title = Sorot +pdfjs-editor-highlight-button-label = Sorot +pdfjs-highlight-floating-button1 = + .title = Sorot + .aria-label = Sorot +pdfjs-highlight-floating-button-label = Sorot + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Hapus gambar +pdfjs-editor-remove-freetext-button = + .title = Hapus teks +pdfjs-editor-remove-stamp-button = + .title = Hapus gambar +pdfjs-editor-remove-highlight-button = + .title = Hapus sorotan + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Warna +pdfjs-editor-free-text-size-input = Ukuran +pdfjs-editor-ink-color-input = Warna +pdfjs-editor-ink-thickness-input = Ketebalan +pdfjs-editor-ink-opacity-input = Opasitas +pdfjs-editor-stamp-add-image-button = + .title = Tambahkan gambar +pdfjs-editor-stamp-add-image-button-label = Tambahkan gambar +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Ketebalan +pdfjs-editor-free-highlight-thickness-title = + .title = Ubah ketebalan saat menyorot item selain teks +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor Teks + .default-content = Mulai mengetik… +pdfjs-free-text = + .aria-label = Editor Teks +pdfjs-free-text-default-content = Mulai mengetik… +pdfjs-ink = + .aria-label = Editor Gambar +pdfjs-ink-canvas = + .aria-label = Gambar yang dibuat pengguna + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Teks alternatif +pdfjs-editor-alt-text-edit-button = + .aria-label = Edit teks alternatif +pdfjs-editor-alt-text-edit-button-label = Edit teks alternatif +pdfjs-editor-alt-text-dialog-label = Pilih opsi + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/is/viewer.ftl b/public/assets/pdfjs/locale/is/viewer.ftl new file mode 100644 index 0000000..deda510 --- /dev/null +++ b/public/assets/pdfjs/locale/is/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Fyrri síða +pdfjs-previous-button-label = Fyrri +pdfjs-next-button = + .title = Næsta síða +pdfjs-next-button-label = Næsti +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Síða +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = af { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } af { $pagesCount }) +pdfjs-zoom-out-button = + .title = Minnka aðdrátt +pdfjs-zoom-out-button-label = Minnka aðdrátt +pdfjs-zoom-in-button = + .title = Auka aðdrátt +pdfjs-zoom-in-button-label = Auka aðdrátt +pdfjs-zoom-select = + .title = Aðdráttur +pdfjs-presentation-mode-button = + .title = Skipta yfir á kynningarham +pdfjs-presentation-mode-button-label = Kynningarhamur +pdfjs-open-file-button = + .title = Opna skrá +pdfjs-open-file-button-label = Opna +pdfjs-print-button = + .title = Prenta +pdfjs-print-button-label = Prenta +pdfjs-save-button = + .title = Vista +pdfjs-save-button-label = Vista +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Sækja +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Sækja +pdfjs-bookmark-button = + .title = Núverandi síða (Skoða vefslóð frá núverandi síðu) +pdfjs-bookmark-button-label = Núverandi síða + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Verkfæri +pdfjs-tools-button-label = Verkfæri +pdfjs-first-page-button = + .title = Fara á fyrstu síðu +pdfjs-first-page-button-label = Fara á fyrstu síðu +pdfjs-last-page-button = + .title = Fara á síðustu síðu +pdfjs-last-page-button-label = Fara á síðustu síðu +pdfjs-page-rotate-cw-button = + .title = Snúa réttsælis +pdfjs-page-rotate-cw-button-label = Snúa réttsælis +pdfjs-page-rotate-ccw-button = + .title = Snúa rangsælis +pdfjs-page-rotate-ccw-button-label = Snúa rangsælis +pdfjs-cursor-text-select-tool-button = + .title = Virkja textavalsáhald +pdfjs-cursor-text-select-tool-button-label = Textavalsáhald +pdfjs-cursor-hand-tool-button = + .title = Virkja handarverkfæri +pdfjs-cursor-hand-tool-button-label = Handarverkfæri +pdfjs-scroll-page-button = + .title = Nota síðuskrun +pdfjs-scroll-page-button-label = Síðuskrun +pdfjs-scroll-vertical-button = + .title = Nota lóðrétt skrun +pdfjs-scroll-vertical-button-label = Lóðrétt skrun +pdfjs-scroll-horizontal-button = + .title = Nota lárétt skrun +pdfjs-scroll-horizontal-button-label = Lárétt skrun +pdfjs-scroll-wrapped-button = + .title = Nota línuskipt síðuskrun +pdfjs-scroll-wrapped-button-label = Línuskipt síðuskrun +pdfjs-spread-none-button = + .title = Ekki taka þátt í dreifingu síðna +pdfjs-spread-none-button-label = Engin dreifing +pdfjs-spread-odd-button = + .title = Taka þátt í dreifingu síðna með oddatölum +pdfjs-spread-odd-button-label = Oddatöludreifing +pdfjs-spread-even-button = + .title = Taktu þátt í dreifingu síðna með jöfnuntölum +pdfjs-spread-even-button-label = Jafnatöludreifing + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Eiginleikar skjals… +pdfjs-document-properties-button-label = Eiginleikar skjals… +pdfjs-document-properties-file-name = Skráarnafn: +pdfjs-document-properties-file-size = Skrárstærð: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bæti) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bæti) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titill: +pdfjs-document-properties-author = Hönnuður: +pdfjs-document-properties-subject = Efni: +pdfjs-document-properties-keywords = Stikkorð: +pdfjs-document-properties-creation-date = Búið til: +pdfjs-document-properties-modification-date = Dags breytingar: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Höfundur: +pdfjs-document-properties-producer = PDF framleiðandi: +pdfjs-document-properties-version = PDF útgáfa: +pdfjs-document-properties-page-count = Blaðsíðufjöldi: +pdfjs-document-properties-page-size = Stærð síðu: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = skammsnið +pdfjs-document-properties-page-size-orientation-landscape = langsnið +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fljótleg vefskoðun: +pdfjs-document-properties-linearized-yes = Já +pdfjs-document-properties-linearized-no = Nei +pdfjs-document-properties-close-button = Loka + +## Print + +pdfjs-print-progress-message = Undirbý skjal fyrir prentun… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Hætta við +pdfjs-printing-not-supported = Aðvörun: Prentun er ekki með fyllilegan stuðning á þessum vafra. +pdfjs-printing-not-ready = Aðvörun: Ekki er búið að hlaða inn allri PDF skránni fyrir prentun. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Víxla hliðarspjaldi af/á +pdfjs-toggle-sidebar-notification-button = + .title = Víxla hliðarslá (skjal inniheldur yfirlit/viðhengi/lög) +pdfjs-toggle-sidebar-button-label = Víxla hliðarspjaldi af/á +pdfjs-document-outline-button = + .title = Sýna yfirlit skjals (tvísmelltu til að opna/loka öllum hlutum) +pdfjs-document-outline-button-label = Efnisskipan skjals +pdfjs-attachments-button = + .title = Sýna viðhengi +pdfjs-attachments-button-label = Viðhengi +pdfjs-layers-button = + .title = Birta lög (tvísmelltu til að endurstilla öll lög í sjálfgefna stöðu) +pdfjs-layers-button-label = Lög +pdfjs-thumbs-button = + .title = Sýna smámyndir +pdfjs-thumbs-button-label = Smámyndir +pdfjs-current-outline-item-button = + .title = Finna núverandi atriði efnisskipunar +pdfjs-current-outline-item-button-label = Núverandi atriði efnisskipunar +pdfjs-findbar-button = + .title = Leita í skjali +pdfjs-findbar-button-label = Leita +pdfjs-additional-layers = Viðbótarlög + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Síða { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Smámynd af síðu { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Leita + .placeholder = Leita í skjali… +pdfjs-find-previous-button = + .title = Leita að fyrra tilfelli þessara orða +pdfjs-find-previous-button-label = Fyrri +pdfjs-find-next-button = + .title = Leita að næsta tilfelli þessara orða +pdfjs-find-next-button-label = Næsti +pdfjs-find-highlight-checkbox = Lita allt +pdfjs-find-match-case-checkbox-label = Passa við stafstöðu +pdfjs-find-match-diacritics-checkbox-label = Passa við broddstafi +pdfjs-find-entire-word-checkbox-label = Heil orð +pdfjs-find-reached-top = Náði efst í skjal, held áfram neðst +pdfjs-find-reached-bottom = Náði enda skjals, held áfram efst +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } af { $total } passar við + *[other] { $current } af { $total } passa við + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Fleiri en { $limit } passar við + *[other] Fleiri en { $limit } passa við + } +pdfjs-find-not-found = Fann ekki orðið + +## Predefined zoom values + +pdfjs-page-scale-width = Síðubreidd +pdfjs-page-scale-fit = Passa á síðu +pdfjs-page-scale-auto = Sjálfvirkur aðdráttur +pdfjs-page-scale-actual = Raunstærð +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Síða { $page } + +## Loading indicator messages + +pdfjs-loading-error = Villa kom upp við að hlaða inn PDF. +pdfjs-invalid-file-error = Ógild eða skemmd PDF skrá. +pdfjs-missing-file-error = Vantar PDF skrá. +pdfjs-unexpected-response-error = Óvænt svar frá netþjóni. +pdfjs-rendering-error = Upp kom villa við að birta síðuna. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Skýring] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Settu inn lykilorð til að opna þessa PDF-skrá. +pdfjs-password-invalid = Ógilt lykilorð. Reyndu aftur. +pdfjs-password-ok-button = Í lagi +pdfjs-password-cancel-button = Hætta við +pdfjs-web-fonts-disabled = Vef leturgerðir eru óvirkar: get ekki notað innbyggðar PDF leturgerðir. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texti +pdfjs-editor-free-text-button-label = Texti +pdfjs-editor-ink-button = + .title = Teikna +pdfjs-editor-ink-button-label = Teikna +pdfjs-editor-stamp-button = + .title = Bæta við eða breyta myndum +pdfjs-editor-stamp-button-label = Bæta við eða breyta myndum +pdfjs-editor-highlight-button = + .title = Áherslulita +pdfjs-editor-highlight-button-label = Áherslulita +pdfjs-highlight-floating-button1 = + .title = Áherslulita + .aria-label = Áherslulita +pdfjs-highlight-floating-button-label = Áherslulita + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Fjarlægja teikningu +pdfjs-editor-remove-freetext-button = + .title = Fjarlægja texta +pdfjs-editor-remove-stamp-button = + .title = Fjarlægja mynd +pdfjs-editor-remove-highlight-button = + .title = Fjarlægja áherslulit + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Litur +pdfjs-editor-free-text-size-input = Stærð +pdfjs-editor-ink-color-input = Litur +pdfjs-editor-ink-thickness-input = Þykkt +pdfjs-editor-ink-opacity-input = Ógegnsæi +pdfjs-editor-stamp-add-image-button = + .title = Bæta við mynd +pdfjs-editor-stamp-add-image-button-label = Bæta við mynd +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Þykkt +pdfjs-editor-free-highlight-thickness-title = + .title = Breyta þykkt við áherslulitun annarra atriða en texta +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Textaritill + .default-content = Byrjaðu að skrifa… +pdfjs-free-text = + .aria-label = Textaritill +pdfjs-free-text-default-content = Byrjaðu að skrifa… +pdfjs-ink = + .aria-label = Teikniritill +pdfjs-ink-canvas = + .aria-label = Mynd gerð af notanda + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt-varatexti +pdfjs-editor-alt-text-edit-button = + .aria-label = Breyta alt-myndatexta +pdfjs-editor-alt-text-edit-button-label = Breyta alt-varatexta +pdfjs-editor-alt-text-dialog-label = Veldu valkost +pdfjs-editor-alt-text-dialog-description = Alt-varatexti (auka-myndatexti) hjálpar þegar fólk getur ekki séð myndina eða þegar hún hleðst ekki inn. +pdfjs-editor-alt-text-add-description-label = Bættu við lýsingu +pdfjs-editor-alt-text-add-description-description = Reyndu að takmarka þetta við 1-2 setningar sem lýsa efninu, umhverfi eða aðgerðum. +pdfjs-editor-alt-text-mark-decorative-label = Merkja sem skraut +pdfjs-editor-alt-text-mark-decorative-description = Þetta er notað fyrir skrautmyndir, eins og borða eða vatnsmerki. +pdfjs-editor-alt-text-cancel-button = Hætta við +pdfjs-editor-alt-text-save-button = Vista +pdfjs-editor-alt-text-decorative-tooltip = Merkt sem skraut +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Til dæmis: „Ungur maður sest við borð til að snæða máltíð“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt-myndatexti + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Efst í vinstra horni - breyta stærð +pdfjs-editor-resizer-label-top-middle = Efst á miðju - breyta stærð +pdfjs-editor-resizer-label-top-right = Efst í hægra horni - breyta stærð +pdfjs-editor-resizer-label-middle-right = Miðja til hægri - breyta stærð +pdfjs-editor-resizer-label-bottom-right = Neðst í hægra horni - breyta stærð +pdfjs-editor-resizer-label-bottom-middle = Neðst á miðju - breyta stærð +pdfjs-editor-resizer-label-bottom-left = Neðst í vinstra horni - breyta stærð +pdfjs-editor-resizer-label-middle-left = Miðja til vinstri - breyta stærð +pdfjs-editor-resizer-top-left = + .aria-label = Efst í vinstra horni - breyta stærð +pdfjs-editor-resizer-top-middle = + .aria-label = Efst á miðju - breyta stærð +pdfjs-editor-resizer-top-right = + .aria-label = Efst í hægra horni - breyta stærð +pdfjs-editor-resizer-middle-right = + .aria-label = Miðja til hægri - breyta stærð +pdfjs-editor-resizer-bottom-right = + .aria-label = Neðst í hægra horni - breyta stærð +pdfjs-editor-resizer-bottom-middle = + .aria-label = Neðst á miðju - breyta stærð +pdfjs-editor-resizer-bottom-left = + .aria-label = Neðst í vinstra horni - breyta stærð +pdfjs-editor-resizer-middle-left = + .aria-label = Miðja til vinstri - breyta stærð + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Áherslulitur +pdfjs-editor-colorpicker-button = + .title = Skipta um lit +pdfjs-editor-colorpicker-dropdown = + .aria-label = Val lita +pdfjs-editor-colorpicker-yellow = + .title = Gult +pdfjs-editor-colorpicker-green = + .title = Grænt +pdfjs-editor-colorpicker-blue = + .title = Blátt +pdfjs-editor-colorpicker-pink = + .title = Bleikt +pdfjs-editor-colorpicker-red = + .title = Rautt + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Birta allt +pdfjs-editor-highlight-show-all-button = + .title = Birta allt + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Breyta alt-myndatexta (lýsingu á mynd) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Bæta við alt-myndatexta (lýsingu á mynd) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skrifaðu lýsinguna þína hér… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Stutt lýsing fyrir fólk sem getur ekki séð myndina eða þegar myndin hleðst ekki inn. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Þessi alt-myndatexti var búinn til sjálfvirkt og gæti verið ónákvæmur. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Kanna nánar +pdfjs-editor-new-alt-text-create-automatically-button-label = Útbúa alt-myndatexta sjálfvirkt +pdfjs-editor-new-alt-text-not-now-button = Ekki núna +pdfjs-editor-new-alt-text-error-title = Gat ekki búið til alt-myndatexta sjálfkrafa +pdfjs-editor-new-alt-text-error-description = Skrifaðu þinn eiginn alt-myndatexta eða reyndu aftur síðar. +pdfjs-editor-new-alt-text-error-close-button = Loka +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Sækir gervigreindarlíkan með alt-myndatextum ({ $downloadedSize } af { $totalSize } MB) + .aria-valuetext = Sækir gervigreindarlíkan með alt-myndatextum ({ $downloadedSize } af { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alt-myndatexta bætt við +pdfjs-editor-new-alt-text-added-button-label = Alt-myndatexta bætt við +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Vantar alt-myndatexta +pdfjs-editor-new-alt-text-missing-button-label = Vantar alt-myndatexta +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Yfirfara alt-myndatexta +pdfjs-editor-new-alt-text-to-review-button-label = Yfirfara myndatexta +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Útbúið sjálfvirkt: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Stillingar fyrir alt-texta myndar +pdfjs-image-alt-text-settings-button-label = Stillingar fyrir alt-texta myndar +pdfjs-editor-alt-text-settings-dialog-label = Stillingar fyrir alt-texta myndar +pdfjs-editor-alt-text-settings-automatic-title = Sjálfvirkur alt-myndatexti +pdfjs-editor-alt-text-settings-create-model-button-label = Útbúa alt-myndatexta sjálfvirkt +pdfjs-editor-alt-text-settings-create-model-description = Stingur upp á lýsingum til að hjálpa fólki sem getur ekki séð myndina eða þegar myndin hleðst ekki inn. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Gervigreindarlíkan alt-myndatexta ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Keyrir staðbundið á tækinu þínu svo gögnin þín haldast undir þinni stjórn. Nauðsynlegt fyrir sjálfvirka alt-myndatexta. +pdfjs-editor-alt-text-settings-delete-model-button = Eyða +pdfjs-editor-alt-text-settings-download-model-button = Sækja +pdfjs-editor-alt-text-settings-downloading-model-button = Sæki… +pdfjs-editor-alt-text-settings-editor-title = Ritill fyrir alt-myndatexta +pdfjs-editor-alt-text-settings-show-dialog-button-label = Sýna alt-myndatextaritil strax þegar mynd er bætt við +pdfjs-editor-alt-text-settings-show-dialog-description = Hjálpar þér að tryggja að allar myndirnar þínar séu með alt-myndatexta. +pdfjs-editor-alt-text-settings-close-button = Loka + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Áherslulitun fjarlægð +pdfjs-editor-undo-bar-message-freetext = Texti fjarlægður +pdfjs-editor-undo-bar-message-ink = Teikning fjarlægð +pdfjs-editor-undo-bar-message-stamp = Mynd fjarlægð +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } glósa fjarlægð + *[other] { $count } glósur fjarlægðar + } +pdfjs-editor-undo-bar-undo-button = + .title = Afturkalla +pdfjs-editor-undo-bar-undo-button-label = Afturkalla +pdfjs-editor-undo-bar-close-button = + .title = Loka +pdfjs-editor-undo-bar-close-button-label = Loka diff --git a/public/assets/pdfjs/locale/it/viewer.ftl b/public/assets/pdfjs/locale/it/viewer.ftl new file mode 100644 index 0000000..d1de7e1 --- /dev/null +++ b/public/assets/pdfjs/locale/it/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina precedente +pdfjs-previous-button-label = Precedente +pdfjs-next-button = + .title = Pagina successiva +pdfjs-next-button-label = Successiva +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = di { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } di { $pagesCount }) +pdfjs-zoom-out-button = + .title = Riduci zoom +pdfjs-zoom-out-button-label = Riduci zoom +pdfjs-zoom-in-button = + .title = Aumenta zoom +pdfjs-zoom-in-button-label = Aumenta zoom +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Passa alla modalità presentazione +pdfjs-presentation-mode-button-label = Modalità presentazione +pdfjs-open-file-button = + .title = Apri file +pdfjs-open-file-button-label = Apri +pdfjs-print-button = + .title = Stampa +pdfjs-print-button-label = Stampa +pdfjs-save-button = + .title = Salva +pdfjs-save-button-label = Salva +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Scarica +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Scarica +pdfjs-bookmark-button = + .title = Pagina corrente (mostra URL della pagina corrente) +pdfjs-bookmark-button-label = Pagina corrente + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Strumenti +pdfjs-tools-button-label = Strumenti +pdfjs-first-page-button = + .title = Vai alla prima pagina +pdfjs-first-page-button-label = Vai alla prima pagina +pdfjs-last-page-button = + .title = Vai all’ultima pagina +pdfjs-last-page-button-label = Vai all’ultima pagina +pdfjs-page-rotate-cw-button = + .title = Ruota in senso orario +pdfjs-page-rotate-cw-button-label = Ruota in senso orario +pdfjs-page-rotate-ccw-button = + .title = Ruota in senso antiorario +pdfjs-page-rotate-ccw-button-label = Ruota in senso antiorario +pdfjs-cursor-text-select-tool-button = + .title = Attiva strumento di selezione testo +pdfjs-cursor-text-select-tool-button-label = Strumento di selezione testo +pdfjs-cursor-hand-tool-button = + .title = Attiva strumento mano +pdfjs-cursor-hand-tool-button-label = Strumento mano +pdfjs-scroll-page-button = + .title = Utilizza scorrimento pagine +pdfjs-scroll-page-button-label = Scorrimento pagine +pdfjs-scroll-vertical-button = + .title = Scorri le pagine in verticale +pdfjs-scroll-vertical-button-label = Scorrimento verticale +pdfjs-scroll-horizontal-button = + .title = Scorri le pagine in orizzontale +pdfjs-scroll-horizontal-button-label = Scorrimento orizzontale +pdfjs-scroll-wrapped-button = + .title = Scorri le pagine in verticale, disponendole da sinistra a destra e andando a capo automaticamente +pdfjs-scroll-wrapped-button-label = Scorrimento con a capo automatico +pdfjs-spread-none-button = + .title = Non raggruppare pagine +pdfjs-spread-none-button-label = Nessun raggruppamento +pdfjs-spread-odd-button = + .title = Crea gruppi di pagine che iniziano con numeri di pagina dispari +pdfjs-spread-odd-button-label = Raggruppamento dispari +pdfjs-spread-even-button = + .title = Crea gruppi di pagine che iniziano con numeri di pagina pari +pdfjs-spread-even-button-label = Raggruppamento pari + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Proprietà del documento… +pdfjs-document-properties-button-label = Proprietà del documento… +pdfjs-document-properties-file-name = Nome file: +pdfjs-document-properties-file-size = Dimensione file: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Titolo: +pdfjs-document-properties-author = Autore: +pdfjs-document-properties-subject = Oggetto: +pdfjs-document-properties-keywords = Parole chiave: +pdfjs-document-properties-creation-date = Data creazione: +pdfjs-document-properties-modification-date = Data modifica: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Autore originale: +pdfjs-document-properties-producer = Produttore PDF: +pdfjs-document-properties-version = Versione PDF: +pdfjs-document-properties-page-count = Conteggio pagine: +pdfjs-document-properties-page-size = Dimensioni pagina: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = verticale +pdfjs-document-properties-page-size-orientation-landscape = orizzontale +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Lettera +pdfjs-document-properties-page-size-name-legal = Legale + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Visualizzazione web veloce: +pdfjs-document-properties-linearized-yes = Sì +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Chiudi + +## Print + +pdfjs-print-progress-message = Preparazione documento per la stampa… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Annulla +pdfjs-printing-not-supported = Attenzione: la stampa non è completamente supportata da questo browser. +pdfjs-printing-not-ready = Attenzione: il PDF non è ancora stato caricato completamente per la stampa. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Attiva/disattiva barra laterale +pdfjs-toggle-sidebar-notification-button = + .title = Attiva/disattiva barra laterale (il documento contiene struttura/allegati/livelli) +pdfjs-toggle-sidebar-button-label = Attiva/disattiva barra laterale +pdfjs-document-outline-button = + .title = Visualizza la struttura del documento (doppio clic per visualizzare/comprimere tutti gli elementi) +pdfjs-document-outline-button-label = Struttura documento +pdfjs-attachments-button = + .title = Visualizza allegati +pdfjs-attachments-button-label = Allegati +pdfjs-layers-button = + .title = Visualizza livelli (doppio clic per ripristinare tutti i livelli allo stato predefinito) +pdfjs-layers-button-label = Livelli +pdfjs-thumbs-button = + .title = Mostra le miniature +pdfjs-thumbs-button-label = Miniature +pdfjs-current-outline-item-button = + .title = Trova elemento struttura corrente +pdfjs-current-outline-item-button-label = Elemento struttura corrente +pdfjs-findbar-button = + .title = Trova nel documento +pdfjs-findbar-button-label = Trova +pdfjs-additional-layers = Livelli aggiuntivi + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura della pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Trova + .placeholder = Trova nel documento… +pdfjs-find-previous-button = + .title = Trova l’occorrenza precedente del testo da cercare +pdfjs-find-previous-button-label = Precedente +pdfjs-find-next-button = + .title = Trova l’occorrenza successiva del testo da cercare +pdfjs-find-next-button-label = Successivo +pdfjs-find-highlight-checkbox = Evidenzia +pdfjs-find-match-case-checkbox-label = Maiuscole/minuscole +pdfjs-find-match-diacritics-checkbox-label = Segni diacritici +pdfjs-find-entire-word-checkbox-label = Parole intere +pdfjs-find-reached-top = Raggiunto l’inizio della pagina, continua dalla fine +pdfjs-find-reached-bottom = Raggiunta la fine della pagina, continua dall’inizio +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } di { $total } corrispondenza + *[other] { $current } di { $total } corrispondenze + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Più di una { $limit } corrispondenza + *[other] Più di { $limit } corrispondenze + } +pdfjs-find-not-found = Testo non trovato + +## Predefined zoom values + +pdfjs-page-scale-width = Larghezza pagina +pdfjs-page-scale-fit = Adatta a una pagina +pdfjs-page-scale-auto = Zoom automatico +pdfjs-page-scale-actual = Dimensioni effettive +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Si è verificato un errore durante il caricamento del PDF. +pdfjs-invalid-file-error = File PDF non valido o danneggiato. +pdfjs-missing-file-error = File PDF non disponibile. +pdfjs-unexpected-response-error = Risposta imprevista del server +pdfjs-rendering-error = Si è verificato un errore durante il rendering della pagina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Annotazione: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Inserire la password per aprire questo file PDF. +pdfjs-password-invalid = Password non corretta. Riprovare. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Annulla +pdfjs-web-fonts-disabled = I web font risultano disattivati: impossibile utilizzare i caratteri incorporati nel PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Testo +pdfjs-editor-free-text-button-label = Testo +pdfjs-editor-ink-button = + .title = Disegno +pdfjs-editor-ink-button-label = Disegno +pdfjs-editor-stamp-button = + .title = Aggiungi o rimuovi immagine +pdfjs-editor-stamp-button-label = Aggiungi o rimuovi immagine +pdfjs-editor-highlight-button = + .title = Evidenzia +pdfjs-editor-highlight-button-label = Evidenzia +pdfjs-highlight-floating-button1 = + .title = Evidenzia + .aria-label = Evidenzia +pdfjs-highlight-floating-button-label = Evidenzia + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Rimuovi disegno +pdfjs-editor-remove-freetext-button = + .title = Rimuovi testo +pdfjs-editor-remove-stamp-button = + .title = Rimuovi immagine +pdfjs-editor-remove-highlight-button = + .title = Rimuovi evidenziazione + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colore +pdfjs-editor-free-text-size-input = Dimensione +pdfjs-editor-ink-color-input = Colore +pdfjs-editor-ink-thickness-input = Spessore +pdfjs-editor-ink-opacity-input = Opacità +pdfjs-editor-stamp-add-image-button = + .title = Aggiungi immagine +pdfjs-editor-stamp-add-image-button-label = Aggiungi immagine +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Spessore +pdfjs-editor-free-highlight-thickness-title = + .title = Modifica lo spessore della selezione per elementi non testuali +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor di testo + .default-content = Inizia a digitare… +pdfjs-free-text = + .aria-label = Editor di testo +pdfjs-free-text-default-content = Inizia a digitare… +pdfjs-ink = + .aria-label = Editor disegni +pdfjs-ink-canvas = + .aria-label = Immagine creata dall’utente + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Testo alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Modifica testo alternativo +pdfjs-editor-alt-text-edit-button-label = Modifica testo alternativo +pdfjs-editor-alt-text-dialog-label = Scegli un’opzione +pdfjs-editor-alt-text-dialog-description = Il testo alternativo (“alt text”) aiuta quando le persone non possono vedere l’immagine o quando l’immagine non viene caricata. +pdfjs-editor-alt-text-add-description-label = Aggiungi una descrizione +pdfjs-editor-alt-text-add-description-description = Punta a una o due frasi che descrivono l’argomento, l’ambientazione o le azioni. +pdfjs-editor-alt-text-mark-decorative-label = Contrassegna come decorativa +pdfjs-editor-alt-text-mark-decorative-description = Viene utilizzato per immagini ornamentali, come bordi o filigrane. +pdfjs-editor-alt-text-cancel-button = Annulla +pdfjs-editor-alt-text-save-button = Salva +pdfjs-editor-alt-text-decorative-tooltip = Contrassegnata come decorativa +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Ad esempio, “Un giovane si siede a tavola per mangiare” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Testo alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Angolo in alto a sinistra — ridimensiona +pdfjs-editor-resizer-label-top-middle = Lato superiore nel mezzo — ridimensiona +pdfjs-editor-resizer-label-top-right = Angolo in alto a destra — ridimensiona +pdfjs-editor-resizer-label-middle-right = Lato destro nel mezzo — ridimensiona +pdfjs-editor-resizer-label-bottom-right = Angolo in basso a destra — ridimensiona +pdfjs-editor-resizer-label-bottom-middle = Lato inferiore nel mezzo — ridimensiona +pdfjs-editor-resizer-label-bottom-left = Angolo in basso a sinistra — ridimensiona +pdfjs-editor-resizer-label-middle-left = Lato sinistro nel mezzo — ridimensiona +pdfjs-editor-resizer-top-left = + .aria-label = Angolo in alto a sinistra — ridimensiona +pdfjs-editor-resizer-top-middle = + .aria-label = Lato superiore nel mezzo — ridimensiona +pdfjs-editor-resizer-top-right = + .aria-label = Angolo in alto a destra — ridimensiona +pdfjs-editor-resizer-middle-right = + .aria-label = Lato destro nel mezzo — ridimensiona +pdfjs-editor-resizer-bottom-right = + .aria-label = Angolo in basso a destra — ridimensiona +pdfjs-editor-resizer-bottom-middle = + .aria-label = Lato inferiore nel mezzo — ridimensiona +pdfjs-editor-resizer-bottom-left = + .aria-label = Angolo in basso a sinistra — ridimensiona +pdfjs-editor-resizer-middle-left = + .aria-label = Lato sinistro nel mezzo — ridimensiona + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Colore evidenziatore +pdfjs-editor-colorpicker-button = + .title = Cambia colore +pdfjs-editor-colorpicker-dropdown = + .aria-label = Colori disponibili +pdfjs-editor-colorpicker-yellow = + .title = Giallo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Blu +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Rosso + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostra tutto +pdfjs-editor-highlight-show-all-button = + .title = Mostra tutto + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Modifica testo alternativo (descrizione dell’immagine) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Aggiungi testo alternativo (descrizione dell’immagine) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Scrivi qui la tua descrizione… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Breve descrizione per le persone che non possono vedere l’immagine, o mostrata quando l’immagine non si carica. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Questo testo alternativo è stato creato automaticamente e potrebbe non essere accurato. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Ulteriori informazioni +pdfjs-editor-new-alt-text-create-automatically-button-label = Crea automaticamente testo alternativo +pdfjs-editor-new-alt-text-not-now-button = Non adesso +pdfjs-editor-new-alt-text-error-title = Impossibile creare automaticamente il testo alternativo +pdfjs-editor-new-alt-text-error-description = Scrivi il testo alternativo o riprova più tardi. +pdfjs-editor-new-alt-text-error-close-button = Chiudi +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Download in corso del modello IA per il testo alternativo ({ $downloadedSize } di { $totalSize } MB) + .aria-valuetext = Download in corso del modello IA per il testo alternativo ({ $downloadedSize } di { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Aggiunto testo alternativo +pdfjs-editor-new-alt-text-added-button-label = Aggiunto testo alternativo +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Testo alternativo mancante +pdfjs-editor-new-alt-text-missing-button-label = Testo alternativo mancante +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Verifica testo alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Verifica testo alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creato automaticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Impostazioni testo alternativo per le immagini +pdfjs-image-alt-text-settings-button-label = Impostazioni testo alternativo per le immagini +pdfjs-editor-alt-text-settings-dialog-label = Impostazioni testo alternativo per le immagini +pdfjs-editor-alt-text-settings-automatic-title = Testo alternativo automatico +pdfjs-editor-alt-text-settings-create-model-button-label = Crea testo alternativo automaticamente +pdfjs-editor-alt-text-settings-create-model-description = Suggerisce una descrizione per le persone che non possono vedere l’immagine, o mostrata quando l’immagine non si carica. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modello IA per il testo alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Viene eseguito localmente sul tuo dispositivo in modo che i tuoi dati rimangano riservati. È richiesto per la generazione automatica del testo alternativo. +pdfjs-editor-alt-text-settings-delete-model-button = Elimina +pdfjs-editor-alt-text-settings-download-model-button = Scarica +pdfjs-editor-alt-text-settings-downloading-model-button = Download… +pdfjs-editor-alt-text-settings-editor-title = Modifica testo alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostra l’editor del testo alternativo non appena si aggiunge un’immagine +pdfjs-editor-alt-text-settings-show-dialog-description = Ti aiuta ad assicurarti che tutte le tue immagini abbiano il testo alternativo. +pdfjs-editor-alt-text-settings-close-button = Chiudi + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Evidenziazione rimossa +pdfjs-editor-undo-bar-message-freetext = Testo rimosso +pdfjs-editor-undo-bar-message-ink = Disegno rimosso +pdfjs-editor-undo-bar-message-stamp = Immagine rimossa +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotazione rimossa + *[other] { $count } annotazioni rimosse + } +pdfjs-editor-undo-bar-undo-button = + .title = Annulla +pdfjs-editor-undo-bar-undo-button-label = Annulla +pdfjs-editor-undo-bar-close-button = + .title = Chiudi +pdfjs-editor-undo-bar-close-button-label = Chiudi diff --git a/public/assets/pdfjs/locale/ja/viewer.ftl b/public/assets/pdfjs/locale/ja/viewer.ftl new file mode 100644 index 0000000..0f37f2a --- /dev/null +++ b/public/assets/pdfjs/locale/ja/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = 前のページへ戻ります +pdfjs-previous-button-label = 前へ +pdfjs-next-button = + .title = 次のページへ進みます +pdfjs-next-button-label = 次へ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ページ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = 表示を縮小します +pdfjs-zoom-out-button-label = 縮小 +pdfjs-zoom-in-button = + .title = 表示を拡大します +pdfjs-zoom-in-button-label = 拡大 +pdfjs-zoom-select = + .title = 拡大/縮小 +pdfjs-presentation-mode-button = + .title = プレゼンテーションモードに切り替えます +pdfjs-presentation-mode-button-label = プレゼンテーションモード +pdfjs-open-file-button = + .title = ファイルを開きます +pdfjs-open-file-button-label = 開く +pdfjs-print-button = + .title = 印刷します +pdfjs-print-button-label = 印刷 +pdfjs-save-button = + .title = 保存します +pdfjs-save-button-label = 保存 +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ダウンロードします +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ダウンロード +pdfjs-bookmark-button = + .title = 現在のページの URL です (現在のページを表示する URL) +pdfjs-bookmark-button-label = 現在のページ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ツール +pdfjs-tools-button-label = ツール +pdfjs-first-page-button = + .title = 最初のページへ移動します +pdfjs-first-page-button-label = 最初のページへ移動 +pdfjs-last-page-button = + .title = 最後のページへ移動します +pdfjs-last-page-button-label = 最後のページへ移動 +pdfjs-page-rotate-cw-button = + .title = ページを右へ回転します +pdfjs-page-rotate-cw-button-label = 右回転 +pdfjs-page-rotate-ccw-button = + .title = ページを左へ回転します +pdfjs-page-rotate-ccw-button-label = 左回転 +pdfjs-cursor-text-select-tool-button = + .title = テキスト選択ツールを有効にします +pdfjs-cursor-text-select-tool-button-label = テキスト選択ツール +pdfjs-cursor-hand-tool-button = + .title = 手のひらツールを有効にします +pdfjs-cursor-hand-tool-button-label = 手のひらツール +pdfjs-scroll-page-button = + .title = ページ単位でスクロールします +pdfjs-scroll-page-button-label = ページ単位でスクロール +pdfjs-scroll-vertical-button = + .title = 縦スクロールにします +pdfjs-scroll-vertical-button-label = 縦スクロール +pdfjs-scroll-horizontal-button = + .title = 横スクロールにします +pdfjs-scroll-horizontal-button-label = 横スクロール +pdfjs-scroll-wrapped-button = + .title = 折り返しスクロールにします +pdfjs-scroll-wrapped-button-label = 折り返しスクロール +pdfjs-spread-none-button = + .title = 見開きにしません +pdfjs-spread-none-button-label = 見開きにしない +pdfjs-spread-odd-button = + .title = 奇数ページ開始で見開きにします +pdfjs-spread-odd-button-label = 奇数ページ見開き +pdfjs-spread-even-button = + .title = 偶数ページ開始で見開きにします +pdfjs-spread-even-button-label = 偶数ページ見開き + +## Document properties dialog + +pdfjs-document-properties-button = + .title = 文書のプロパティ... +pdfjs-document-properties-button-label = 文書のプロパティ... +pdfjs-document-properties-file-name = ファイル名: +pdfjs-document-properties-file-size = ファイルサイズ: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } バイト) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } バイト) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } バイト) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } バイト) +pdfjs-document-properties-title = タイトル: +pdfjs-document-properties-author = 作成者: +pdfjs-document-properties-subject = 件名: +pdfjs-document-properties-keywords = キーワード: +pdfjs-document-properties-creation-date = 作成日: +pdfjs-document-properties-modification-date = 更新日: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = アプリケーション: +pdfjs-document-properties-producer = PDF 作成: +pdfjs-document-properties-version = PDF のバージョン: +pdfjs-document-properties-page-count = ページ数: +pdfjs-document-properties-page-size = ページサイズ: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = 縦 +pdfjs-document-properties-page-size-orientation-landscape = 横 +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = レター +pdfjs-document-properties-page-size-name-legal = リーガル + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = ウェブ表示用に最適化: +pdfjs-document-properties-linearized-yes = はい +pdfjs-document-properties-linearized-no = いいえ +pdfjs-document-properties-close-button = 閉じる + +## Print + +pdfjs-print-progress-message = 文書の印刷を準備しています... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = キャンセル +pdfjs-printing-not-supported = 警告: このブラウザーでは印刷が完全にサポートされていません。 +pdfjs-printing-not-ready = 警告: PDF を印刷するための読み込みが終了していません。 + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = サイドバー表示を切り替えます +pdfjs-toggle-sidebar-notification-button = + .title = サイドバー表示を切り替えます (文書に含まれるアウトライン / 添付 / レイヤー) +pdfjs-toggle-sidebar-button-label = サイドバーの切り替え +pdfjs-document-outline-button = + .title = 文書の目次を表示します (ダブルクリックで項目を開閉します) +pdfjs-document-outline-button-label = 文書の目次 +pdfjs-attachments-button = + .title = 添付ファイルを表示します +pdfjs-attachments-button-label = 添付ファイル +pdfjs-layers-button = + .title = レイヤーを表示します (ダブルクリックですべてのレイヤーが初期状態に戻ります) +pdfjs-layers-button-label = レイヤー +pdfjs-thumbs-button = + .title = 縮小版を表示します +pdfjs-thumbs-button-label = 縮小版 +pdfjs-current-outline-item-button = + .title = 現在のアウトライン項目を検索 +pdfjs-current-outline-item-button-label = 現在のアウトライン項目 +pdfjs-findbar-button = + .title = 文書内を検索します +pdfjs-findbar-button-label = 検索 +pdfjs-additional-layers = 追加レイヤー + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } ページ +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } ページの縮小版 + +## Find panel button title and messages + +pdfjs-find-input = + .title = 検索 + .placeholder = 文書内を検索... +pdfjs-find-previous-button = + .title = 現在より前の位置で指定文字列が現れる部分を検索します +pdfjs-find-previous-button-label = 前へ +pdfjs-find-next-button = + .title = 現在より後の位置で指定文字列が現れる部分を検索します +pdfjs-find-next-button-label = 次へ +pdfjs-find-highlight-checkbox = すべて強調表示 +pdfjs-find-match-case-checkbox-label = 大文字/小文字を区別 +pdfjs-find-match-diacritics-checkbox-label = 発音区別符号を区別 +pdfjs-find-entire-word-checkbox-label = 単語一致 +pdfjs-find-reached-top = 文書先頭に到達したので末尾から続けて検索します +pdfjs-find-reached-bottom = 文書末尾に到達したので先頭から続けて検索します +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = { $total } 件中 { $current } 件目 +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = { $limit } 件以上一致 +pdfjs-find-not-found = 見つかりませんでした + +## Predefined zoom values + +pdfjs-page-scale-width = 幅に合わせる +pdfjs-page-scale-fit = ページのサイズに合わせる +pdfjs-page-scale-auto = 自動ズーム +pdfjs-page-scale-actual = 実際のサイズ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page } ページ + +## Loading indicator messages + +pdfjs-loading-error = PDF の読み込み中にエラーが発生しました。 +pdfjs-invalid-file-error = 無効または破損した PDF ファイル。 +pdfjs-missing-file-error = PDF ファイルが見つかりません。 +pdfjs-unexpected-response-error = サーバーから予期せぬ応答がありました。 +pdfjs-rendering-error = ページのレンダリング中にエラーが発生しました。 + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } 注釈] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = この PDF ファイルを開くためのパスワードを入力してください。 +pdfjs-password-invalid = パスワードが正しくありません。もう一度試してください。 +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = キャンセル +pdfjs-web-fonts-disabled = ウェブフォントが無効になっています: 埋め込まれた PDF のフォントを使用できません。 + +## Editing + +pdfjs-editor-free-text-button = + .title = フリーテキスト注釈を追加します +pdfjs-editor-free-text-button-label = フリーテキスト注釈 +pdfjs-editor-ink-button = + .title = インク注釈を追加します +pdfjs-editor-ink-button-label = インク注釈 +pdfjs-editor-stamp-button = + .title = 画像を追加または編集します +pdfjs-editor-stamp-button-label = 画像を追加または編集 +pdfjs-editor-highlight-button = + .title = 強調します +pdfjs-editor-highlight-button-label = 強調 +pdfjs-highlight-floating-button1 = + .title = 強調 + .aria-label = 強調します +pdfjs-highlight-floating-button-label = 強調 + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = インク注釈を削除します +pdfjs-editor-remove-freetext-button = + .title = テキストを削除します +pdfjs-editor-remove-stamp-button = + .title = 画像を削除します +pdfjs-editor-remove-highlight-button = + .title = 強調を削除します + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = 色 +pdfjs-editor-free-text-size-input = サイズ +pdfjs-editor-ink-color-input = 色 +pdfjs-editor-ink-thickness-input = 太さ +pdfjs-editor-ink-opacity-input = 不透明度 +pdfjs-editor-stamp-add-image-button = + .title = 画像を追加します +pdfjs-editor-stamp-add-image-button-label = 画像を追加 +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = 太さ +pdfjs-editor-free-highlight-thickness-title = + .title = テキスト以外のアイテムを強調する時の太さを変更します +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = フリーテキスト注釈エディター + .default-content = テキストを入力してください... +pdfjs-free-text = + .aria-label = フリーテキスト注釈エディター +pdfjs-free-text-default-content = テキストを入力してください... +pdfjs-ink = + .aria-label = インク注釈エディター +pdfjs-ink-canvas = + .aria-label = ユーザー作成画像 + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = 代替テキスト +pdfjs-editor-alt-text-edit-button = + .aria-label = 代替テキストを編集 +pdfjs-editor-alt-text-edit-button-label = 代替テキストを編集 +pdfjs-editor-alt-text-dialog-label = オプションの選択 +pdfjs-editor-alt-text-dialog-description = 代替テキストは画像が表示されない場合や読み込まれない場合にユーザーの助けになります。 +pdfjs-editor-alt-text-add-description-label = 説明を追加 +pdfjs-editor-alt-text-add-description-description = 対象や設定、動作を説明する短い文章を記入してください。 +pdfjs-editor-alt-text-mark-decorative-label = 装飾マークを付ける +pdfjs-editor-alt-text-mark-decorative-description = これは区切り線やウォーターマークなどの装飾画像に使用されます。 +pdfjs-editor-alt-text-cancel-button = キャンセル +pdfjs-editor-alt-text-save-button = 保存 +pdfjs-editor-alt-text-decorative-tooltip = 装飾マークが付いています +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = 例:「若い人がテーブルの席について食事をしています」 +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = 代替テキスト + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = 左上隅 — サイズ変更 +pdfjs-editor-resizer-label-top-middle = 上中央 — サイズ変更 +pdfjs-editor-resizer-label-top-right = 右上隅 — サイズ変更 +pdfjs-editor-resizer-label-middle-right = 右中央 — サイズ変更 +pdfjs-editor-resizer-label-bottom-right = 右下隅 — サイズ変更 +pdfjs-editor-resizer-label-bottom-middle = 下中央 — サイズ変更 +pdfjs-editor-resizer-label-bottom-left = 左下隅 — サイズ変更 +pdfjs-editor-resizer-label-middle-left = 左中央 — サイズ変更 +pdfjs-editor-resizer-top-left = + .aria-label = 左上隅 — サイズ変更 +pdfjs-editor-resizer-top-middle = + .aria-label = 上中央 — サイズ変更 +pdfjs-editor-resizer-top-right = + .aria-label = 右上隅 — サイズ変更 +pdfjs-editor-resizer-middle-right = + .aria-label = 右中央 — サイズ変更 +pdfjs-editor-resizer-bottom-right = + .aria-label = 右下隅 — サイズ変更 +pdfjs-editor-resizer-bottom-middle = + .aria-label = 下中央 — サイズ変更 +pdfjs-editor-resizer-bottom-left = + .aria-label = 左下隅 — サイズ変更 +pdfjs-editor-resizer-middle-left = + .aria-label = 左中央 — サイズ変更 + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = 強調色 +pdfjs-editor-colorpicker-button = + .title = 色を変更します +pdfjs-editor-colorpicker-dropdown = + .aria-label = 色の選択 +pdfjs-editor-colorpicker-yellow = + .title = 黄色 +pdfjs-editor-colorpicker-green = + .title = 緑色 +pdfjs-editor-colorpicker-blue = + .title = 青色 +pdfjs-editor-colorpicker-pink = + .title = ピンク色 +pdfjs-editor-colorpicker-red = + .title = 赤色 + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = すべて表示 +# (^m^) en-US: .title = Show all +pdfjs-editor-highlight-show-all-button = + .title = 強調の表示を切り替えます + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = 代替テキストを編集 (画像の説明) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = 代替テキストを追加 (画像の説明) +pdfjs-editor-new-alt-text-textarea = + .placeholder = ここに説明を記入してください... +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = 画像が読み込まれない場合や見えない人のための短い説明です。 +pdfjs-editor-new-alt-text-disclaimer1 = この代替テキストは自動的に生成されたため正確でない可能性があります。 +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = 詳細情報 +pdfjs-editor-new-alt-text-create-automatically-button-label = 代替テキストを自動生成 +pdfjs-editor-new-alt-text-not-now-button = 後で +pdfjs-editor-new-alt-text-error-title = 代替テキストを自動生成できませんでした +pdfjs-editor-new-alt-text-error-description = ご自分で代替テキストを書くか後でもう一度試してください。 +pdfjs-editor-new-alt-text-error-close-button = 閉じる +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = 代替テキスト AI モデルをダウンロードしています ({ $downloadedSize } / { $totalSize } MB) + .aria-valuetext = 代替テキスト AI モデルをダウンロードしています ({ $downloadedSize } / { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = 代替テキストを追加しました +pdfjs-editor-new-alt-text-added-button-label = 代替テキストを追加しました +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = 代替テキストがありません +pdfjs-editor-new-alt-text-missing-button-label = 代替テキストがありません +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = 代替テキストをレビュー +pdfjs-editor-new-alt-text-to-review-button-label = 代替テキストをレビュー +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = 自動生成されました: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = 画像の代替テキスト設定 +pdfjs-image-alt-text-settings-button-label = 画像の代替テキスト設定 +pdfjs-editor-alt-text-settings-dialog-label = 画像の代替テキスト設定 +pdfjs-editor-alt-text-settings-automatic-title = 自動代替テキスト +pdfjs-editor-alt-text-settings-create-model-button-label = 代替テキストを自動生成 +pdfjs-editor-alt-text-settings-create-model-description = 画像が読み込まれない場合や見えない人のために説明を提案します。 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = 代替テキスト AI モデル ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = ローカルの端末上で実行されるためデータは非公開になります。代替テキストの自動生成に必要です。 +pdfjs-editor-alt-text-settings-delete-model-button = 削除 +pdfjs-editor-alt-text-settings-download-model-button = ダウンロード +pdfjs-editor-alt-text-settings-downloading-model-button = ダウンロード中... +pdfjs-editor-alt-text-settings-editor-title = 代替テキストエディター +pdfjs-editor-alt-text-settings-show-dialog-button-label = 画像の追加時に代替テキストエディターを表示する +pdfjs-editor-alt-text-settings-show-dialog-description = すべての画像に代替テキストを追加する助けになります。 +pdfjs-editor-alt-text-settings-close-button = 閉じる + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = 強調表示が削除されました +pdfjs-editor-undo-bar-message-freetext = フリーテキスト注釈が削除されました +pdfjs-editor-undo-bar-message-ink = インク注釈が削除されました +pdfjs-editor-undo-bar-message-stamp = 画像が削除されました +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = { $count } 個の注釈が削除されました +pdfjs-editor-undo-bar-undo-button = + .title = 元に戻す +pdfjs-editor-undo-bar-undo-button-label = 元に戻す +pdfjs-editor-undo-bar-close-button = + .title = 閉じる +pdfjs-editor-undo-bar-close-button-label = 閉じる diff --git a/public/assets/pdfjs/locale/ka/viewer.ftl b/public/assets/pdfjs/locale/ka/viewer.ftl new file mode 100644 index 0000000..d500f3e --- /dev/null +++ b/public/assets/pdfjs/locale/ka/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = წინა გვერდი +pdfjs-previous-button-label = წინა +pdfjs-next-button = + .title = შემდეგი გვერდი +pdfjs-next-button-label = შემდეგი +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = გვერდი +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount }-დან +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } { $pagesCount }-დან) +pdfjs-zoom-out-button = + .title = ზომის შემცირება +pdfjs-zoom-out-button-label = დაშორება +pdfjs-zoom-in-button = + .title = ზომის გაზრდა +pdfjs-zoom-in-button-label = მოახლოება +pdfjs-zoom-select = + .title = ზომა +pdfjs-presentation-mode-button = + .title = წარდგენის რეჟიმზე გადართვა +pdfjs-presentation-mode-button-label = წარდგენის რეჟიმი +pdfjs-open-file-button = + .title = ფაილის გახსნა +pdfjs-open-file-button-label = გახსნა +pdfjs-print-button = + .title = ამობეჭდვა +pdfjs-print-button-label = ამობეჭდვა +pdfjs-save-button = + .title = შენახვა +pdfjs-save-button-label = შენახვა +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ჩამოტვირთვა +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ჩამოტვირთვა +pdfjs-bookmark-button = + .title = მიმდინარე გვერდი (ბმული ამ გვერდისთვის) +pdfjs-bookmark-button-label = მიმდინარე გვერდი + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ხელსაწყოები +pdfjs-tools-button-label = ხელსაწყოები +pdfjs-first-page-button = + .title = პირველ გვერდზე გადასვლა +pdfjs-first-page-button-label = პირველ გვერდზე გადასვლა +pdfjs-last-page-button = + .title = ბოლო გვერდზე გადასვლა +pdfjs-last-page-button-label = ბოლო გვერდზე გადასვლა +pdfjs-page-rotate-cw-button = + .title = საათის ისრის მიმართულებით შებრუნება +pdfjs-page-rotate-cw-button-label = მარჯვნივ გადაბრუნება +pdfjs-page-rotate-ccw-button = + .title = საათის ისრის საპირისპიროდ შებრუნება +pdfjs-page-rotate-ccw-button-label = მარცხნივ გადაბრუნება +pdfjs-cursor-text-select-tool-button = + .title = მოსანიშნი მაჩვენებლის გამოყენება +pdfjs-cursor-text-select-tool-button-label = მოსანიშნი მაჩვენებელი +pdfjs-cursor-hand-tool-button = + .title = გადასაადგილებელი მაჩვენებლის გამოყენება +pdfjs-cursor-hand-tool-button-label = გადასაადგილებელი +pdfjs-scroll-page-button = + .title = გვერდზე გადაადგილების გამოყენება +pdfjs-scroll-page-button-label = გვერდშივე გადაადგილება +pdfjs-scroll-vertical-button = + .title = გვერდების შვეულად ჩვენება +pdfjs-scroll-vertical-button-label = შვეული გადაადგილება +pdfjs-scroll-horizontal-button = + .title = გვერდების თარაზულად ჩვენება +pdfjs-scroll-horizontal-button-label = განივი გადაადგილება +pdfjs-scroll-wrapped-button = + .title = გვერდების ცხრილურად ჩვენება +pdfjs-scroll-wrapped-button-label = ცხრილური გადაადგილება +pdfjs-spread-none-button = + .title = ორ გვერდზე გაშლის გარეშე +pdfjs-spread-none-button-label = ცალგვერდიანი ჩვენება +pdfjs-spread-odd-button = + .title = ორ გვერდზე გაშლა კენტი გვერდიდან +pdfjs-spread-odd-button-label = ორ გვერდზე კენტიდან +pdfjs-spread-even-button = + .title = ორ გვერდზე გაშლა ლუწი გვერდიდან +pdfjs-spread-even-button-label = ორ გვერდზე ლუწიდან + +## Document properties dialog + +pdfjs-document-properties-button = + .title = დოკუმენტის შესახებ… +pdfjs-document-properties-button-label = დოკუმენტის შესახებ… +pdfjs-document-properties-file-name = ფაილის სახელი: +pdfjs-document-properties-file-size = ფაილის მოცულობა: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } კბაიტი ({ $b } ბაიტი) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } მბაიტი ({ $b } ბაიტი) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } კბ ({ $size_b } ბაიტი) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } მბ ({ $size_b } ბაიტი) +pdfjs-document-properties-title = სათაური: +pdfjs-document-properties-author = შემქმნელი: +pdfjs-document-properties-subject = თემა: +pdfjs-document-properties-keywords = საკვანძო სიტყვები: +pdfjs-document-properties-creation-date = შექმნის დრო: +pdfjs-document-properties-modification-date = ჩასწორების დრო: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = შემდგენელი: +pdfjs-document-properties-producer = PDF-შემდგენელი: +pdfjs-document-properties-version = PDF-ვერსია: +pdfjs-document-properties-page-count = გვერდები: +pdfjs-document-properties-page-size = გვერდის ზომა: +pdfjs-document-properties-page-size-unit-inches = დუიმი +pdfjs-document-properties-page-size-unit-millimeters = მმ +pdfjs-document-properties-page-size-orientation-portrait = შვეულად +pdfjs-document-properties-page-size-orientation-landscape = თარაზულად +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = მსუბუქი ვებჩვენება: +pdfjs-document-properties-linearized-yes = დიახ +pdfjs-document-properties-linearized-no = არა +pdfjs-document-properties-close-button = დახურვა + +## Print + +pdfjs-print-progress-message = დოკუმენტი მზადდება ამოსაბეჭდად… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = გაუქმება +pdfjs-printing-not-supported = გაფრთხილება: ამობეჭდვა ამ ბრაუზერში არაა სრულად მხარდაჭერილი. +pdfjs-printing-not-ready = გაფრთხილება: PDF სრულად ჩატვირთული არაა, ამობეჭდვის დასაწყებად. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = გვერდითა ზოლის გამოჩენა/დამალვა +pdfjs-toggle-sidebar-notification-button = + .title = გვერდითი ზოლის გამოჩენა (შეიცავს სარჩევს/დანართს/შრეებს) +pdfjs-toggle-sidebar-button-label = გვერდითა ზოლის გამოჩენა/დამალვა +pdfjs-document-outline-button = + .title = დოკუმენტის სარჩევის ჩვენება (ორმაგი წკაპით თითოეულის ჩამოშლა/აკეცვა) +pdfjs-document-outline-button-label = დოკუმენტის სარჩევი +pdfjs-attachments-button = + .title = დანართების ჩვენება +pdfjs-attachments-button-label = დანართები +pdfjs-layers-button = + .title = შრეების გამოჩენა (ორმაგი წკაპით ყველა შრის ნაგულისხმევზე დაბრუნება) +pdfjs-layers-button-label = შრეები +pdfjs-thumbs-button = + .title = შეთვალიერება +pdfjs-thumbs-button-label = ესკიზები +pdfjs-current-outline-item-button = + .title = მიმდინარე გვერდის მონახვა სარჩევში +pdfjs-current-outline-item-button-label = მიმდინარე გვერდი სარჩევში +pdfjs-findbar-button = + .title = პოვნა დოკუმენტში +pdfjs-findbar-button-label = ძიება +pdfjs-additional-layers = დამატებითი შრეები + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = გვერდი { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = გვერდის შეთვალიერება { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ძიება + .placeholder = პოვნა დოკუმენტში… +pdfjs-find-previous-button = + .title = წინა დამთხვევის პოვნა +pdfjs-find-previous-button-label = წინა +pdfjs-find-next-button = + .title = მომდევნო დამთხვევის პოვნა +pdfjs-find-next-button-label = შემდეგი +pdfjs-find-highlight-checkbox = ყველაფრის მონიშვნა +pdfjs-find-match-case-checkbox-label = მთავრულით +pdfjs-find-match-diacritics-checkbox-label = ნიშნებით +pdfjs-find-entire-word-checkbox-label = მთლიანი სიტყვები +pdfjs-find-reached-top = მიღწეულია დოკუმენტის დასაწყისი, გრძელდება ბოლოდან +pdfjs-find-reached-bottom = მიღწეულია დოკუმენტის ბოლო, გრძელდება დასაწყისიდან +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] თანხვედრა { $current }, სულ { $total } + *[other] თანხვედრა { $current }, სულ { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] არანაკლებ { $limit } თანხვედრა + *[other] არანაკლებ { $limit } თანხვედრა + } +pdfjs-find-not-found = ფრაზა ვერ მოიძებნა + +## Predefined zoom values + +pdfjs-page-scale-width = გვერდის სიგანეზე +pdfjs-page-scale-fit = მთლიანი გვერდი +pdfjs-page-scale-auto = ავტომატური +pdfjs-page-scale-actual = საწყისი ზომა +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = გვერდი { $page } + +## Loading indicator messages + +pdfjs-loading-error = შეცდომა, PDF-ფაილის ჩატვირთვისას. +pdfjs-invalid-file-error = არამართებული ან დაზიანებული PDF-ფაილი. +pdfjs-missing-file-error = ნაკლული PDF-ფაილი. +pdfjs-unexpected-response-error = სერვერის მოულოდნელი პასუხი. +pdfjs-rendering-error = შეცდომა, გვერდის ჩვენებისას. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } შენიშვნა] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = შეიყვანეთ პაროლი PDF-ფაილის გასახსნელად. +pdfjs-password-invalid = არასწორი პაროლი. გთხოვთ, სცადოთ ხელახლა. +pdfjs-password-ok-button = კარგი +pdfjs-password-cancel-button = გაუქმება +pdfjs-web-fonts-disabled = ვებშრიფტები გამორთულია: ჩაშენებული PDF-შრიფტების გამოყენება ვერ ხერხდება. + +## Editing + +pdfjs-editor-free-text-button = + .title = წარწერა +pdfjs-editor-free-text-button-label = წარწერა +pdfjs-editor-ink-button = + .title = ხაზვა +pdfjs-editor-ink-button-label = ხაზვა +pdfjs-editor-stamp-button = + .title = სურათების დართვა ან ჩასწორება +pdfjs-editor-stamp-button-label = სურათების დართვა ან ჩასწორება +pdfjs-editor-highlight-button = + .title = მონიშვნა +pdfjs-editor-highlight-button-label = მონიშვნა +pdfjs-highlight-floating-button1 = + .title = მონიშვნა + .aria-label = მონიშვნა +pdfjs-highlight-floating-button-label = მონიშვნა + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = დახაზულის მოცილება +pdfjs-editor-remove-freetext-button = + .title = წარწერის მოცილება +pdfjs-editor-remove-stamp-button = + .title = სურათის მოცილება +pdfjs-editor-remove-highlight-button = + .title = მონიშვნის მოცილება + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = ფერი +pdfjs-editor-free-text-size-input = ზომა +pdfjs-editor-ink-color-input = ფერი +pdfjs-editor-ink-thickness-input = სისქე +pdfjs-editor-ink-opacity-input = გაუმჭვირვალობა +pdfjs-editor-stamp-add-image-button = + .title = სურათის დამატება +pdfjs-editor-stamp-add-image-button-label = სურათის დამატება +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = სისქე +pdfjs-editor-free-highlight-thickness-title = + .title = სისქის შეცვლა წარწერის გარდა სხვა ნაწილების მონიშვნისას +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = ნაწერის ჩასწორება + .default-content = დაიწყეთ აკრეფა… +pdfjs-free-text = + .aria-label = ნაწერის ჩასწორება +pdfjs-free-text-default-content = აკრიფეთ… +pdfjs-ink = + .aria-label = დახაზულის შესწორება +pdfjs-ink-canvas = + .aria-label = მომხმარებლის შექმნილი სურათი + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = თანდართული წარწერა +pdfjs-editor-alt-text-edit-button = + .aria-label = დართული წარწერის ჩასწორება +pdfjs-editor-alt-text-edit-button-label = თანდართული წარწერის ჩასწორება +pdfjs-editor-alt-text-dialog-label = არჩევა +pdfjs-editor-alt-text-dialog-description = თანდართული (შემნაცვლებელი) წარწერა გამოსადეგია მათთვის, ვინც ვერ ხედავს სურათებს ან გამოისახება მაშინ, როცა სურათი ვერ ჩაიტვირთება. +pdfjs-editor-alt-text-add-description-label = აღწერილობის მითითება +pdfjs-editor-alt-text-add-description-description = განკუთვნილია 1-2 წინადადებით საგნის, მახასიათებლის ან მოქმედების აღსაწერად. +pdfjs-editor-alt-text-mark-decorative-label = მოინიშნოს მორთულობად +pdfjs-editor-alt-text-mark-decorative-description = განკუთვნილია შესამკობი სურათებისთვის, გარსშემოსავლები ჩარჩოებისა და ჭვირნიშნებისთვის. +pdfjs-editor-alt-text-cancel-button = გაუქმება +pdfjs-editor-alt-text-save-button = შენახვა +pdfjs-editor-alt-text-decorative-tooltip = მოინიშნოს მორთულობად +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = მაგალითად, „ახალგაზრდა მამაკაცი მაგიდასთან ზის და სადილობს“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = დართული წარწერა + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = ზევით მარცხნივ — ზომაცვლა +pdfjs-editor-resizer-label-top-middle = ზევით შუაში — ზომაცვლა +pdfjs-editor-resizer-label-top-right = ზევით მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-label-middle-right = შუაში მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-label-bottom-right = ქვევით მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-label-bottom-middle = ქვევით შუაში — ზომაცვლა +pdfjs-editor-resizer-label-bottom-left = ზვევით მარცხნივ — ზომაცვლა +pdfjs-editor-resizer-label-middle-left = შუაში მარცხნივ — ზომაცვლა +pdfjs-editor-resizer-top-left = + .aria-label = ზევით მარცხნივ — ზომაცვლა +pdfjs-editor-resizer-top-middle = + .aria-label = ზევით შუაში — ზომაცვლა +pdfjs-editor-resizer-top-right = + .aria-label = ზევით მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-middle-right = + .aria-label = შუაში მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-bottom-right = + .aria-label = ქვევით მარჯვნივ — ზომაცვლა +pdfjs-editor-resizer-bottom-middle = + .aria-label = ქვევით შუაში — ზომაცვლა +pdfjs-editor-resizer-bottom-left = + .aria-label = ზვევით მარცხნივ — ზომაცვლა +pdfjs-editor-resizer-middle-left = + .aria-label = შუაში მარცხნივ — ზომაცვლა + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = მოსანიშნი ფერი +pdfjs-editor-colorpicker-button = + .title = ფერის შეცვლა +pdfjs-editor-colorpicker-dropdown = + .aria-label = ფერის არჩევა +pdfjs-editor-colorpicker-yellow = + .title = ყვითელი +pdfjs-editor-colorpicker-green = + .title = მწვანე +pdfjs-editor-colorpicker-blue = + .title = ლურჯი +pdfjs-editor-colorpicker-pink = + .title = ვარდისფერი +pdfjs-editor-colorpicker-red = + .title = წითელი + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = ყველას ჩვენება +pdfjs-editor-highlight-show-all-button = + .title = ყველას ჩვენება + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = დართული წარწერის ჩასწორება (სურათის აღწერის) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = დართული წარწერის დამატება (სურათის აღწერის) +pdfjs-editor-new-alt-text-textarea = + .placeholder = დაწერეთ თქვენი აღწერა აქ… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = მოკლე აღწერა მათთვის, ვინც ვერ ხედავს სურათს ან ვისთანაც ვერ ჩაიტვირთება სურათი. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = ეს დართული წარწერა ავტომატურადაა შედგენილი და შესაძლოა, უმართებულო იყოს. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = ვრცლად +pdfjs-editor-new-alt-text-create-automatically-button-label = დართული წარწერის ავტომატური შედგენა +pdfjs-editor-new-alt-text-not-now-button = ახლა არა +pdfjs-editor-new-alt-text-error-title = დართული წარწერის შედგენა ვერ მოხერხდა +pdfjs-editor-new-alt-text-error-description = გთხოვთ დაწეროთ საკუთარი დანართი და კვლავ სცადოთ მოგვიანებით. +pdfjs-editor-new-alt-text-error-close-button = დახურვა +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = ჩამოიტვირთება დართული წარწერის შესადეგი AI-მოდელი ({ $downloadedSize } ზომით { $totalSize } მბაიტი) + .aria-valuetext = ჩამოიტვირთება დართული წარწერის შესადეგი AI-მოდელი ({ $downloadedSize } ზომით { $totalSize } მბაიტი) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = დართული წარწერა დამატებულია +pdfjs-editor-new-alt-text-added-button-label = დართული წარწერა დამატებულია +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = აკლია დართული წარწერა +pdfjs-editor-new-alt-text-missing-button-label = აკლია დართული წარწერა +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = დართული წარწერის გადახედვა +pdfjs-editor-new-alt-text-to-review-button-label = დართული წარწერის გადახედვა +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = შედგენილია ავტომატურად: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = სურათის დართული წარწერის პარამეტრები +pdfjs-image-alt-text-settings-button-label = სურათის დართული წარწერის პარამეტრები +pdfjs-editor-alt-text-settings-dialog-label = სურათის დართული წარწერის პარამეტრები +pdfjs-editor-alt-text-settings-automatic-title = ავტომატურად დართული წარწერა +pdfjs-editor-alt-text-settings-create-model-button-label = დართული წარწერის ავტომატური შედგენა +pdfjs-editor-alt-text-settings-create-model-description = აღწერს სურათს მათთვის, ვინც ვერ ხედავს ან ვისთანაც ვერ ჩაიტვირთება. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = დართული წარწერის შესადგენი AI-მოდელი ({ $totalSize } მბაიტი) +pdfjs-editor-alt-text-settings-ai-model-description = ეშვება ადგილობრივად თქვენს მოწყობილობასა, ასე რომ მონაცემები დარჩება პირადი. საჭიროა წარწერის ავტომატურად დართვისთვის. +pdfjs-editor-alt-text-settings-delete-model-button = წაშლა +pdfjs-editor-alt-text-settings-download-model-button = ჩამოტვირთვა +pdfjs-editor-alt-text-settings-downloading-model-button = ჩამოიტვრითება... +pdfjs-editor-alt-text-settings-editor-title = დართული წარწერის ჩამსწორებელი +pdfjs-editor-alt-text-settings-show-dialog-button-label = გამოჩნდეს დართული წარწერის ჩამსწორებელი სურათის დამატებისთანავე +pdfjs-editor-alt-text-settings-show-dialog-description = უზრუნველყოფს, რომ თქვენს ყველა სურათს ახლდეს დართული წარწერა. +pdfjs-editor-alt-text-settings-close-button = დახურვა + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = მონიშვნა მოცილებულია +pdfjs-editor-undo-bar-message-freetext = წარწერა მოცილებულია +pdfjs-editor-undo-bar-message-ink = ნახატი მოცილებულია +pdfjs-editor-undo-bar-message-stamp = სურათი მოცილებულია +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } შენიშვნა მოცილებულია + *[other] { $count } შენიშვნა მოცილებულია + } +pdfjs-editor-undo-bar-undo-button = + .title = დაბრუნება +pdfjs-editor-undo-bar-undo-button-label = დაბრუნება +pdfjs-editor-undo-bar-close-button = + .title = დახურვა +pdfjs-editor-undo-bar-close-button-label = დახურვა diff --git a/public/assets/pdfjs/locale/kab/viewer.ftl b/public/assets/pdfjs/locale/kab/viewer.ftl new file mode 100644 index 0000000..dda88c1 --- /dev/null +++ b/public/assets/pdfjs/locale/kab/viewer.ftl @@ -0,0 +1,438 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Asebter azewwar +pdfjs-previous-button-label = Azewwar +pdfjs-next-button = + .title = Asebter d-iteddun +pdfjs-next-button-label = Ddu ɣer zdat +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Asebter +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = ɣef { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } n { $pagesCount }) +pdfjs-zoom-out-button = + .title = Semẓi +pdfjs-zoom-out-button-label = Semẓi +pdfjs-zoom-in-button = + .title = Semɣeṛ +pdfjs-zoom-in-button-label = Semɣeṛ +pdfjs-zoom-select = + .title = Semɣeṛ/Semẓi +pdfjs-presentation-mode-button = + .title = Uɣal ɣer Uskar Tihawt +pdfjs-presentation-mode-button-label = Askar Tihawt +pdfjs-open-file-button = + .title = Ldi Afaylu +pdfjs-open-file-button-label = Ldi +pdfjs-print-button = + .title = Siggez +pdfjs-print-button-label = Siggez +pdfjs-save-button = + .title = Sekles +pdfjs-save-button-label = Sekles +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Sader +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Sader +pdfjs-bookmark-button = + .title = Asebter amiran (Sken-d tansa URL seg usebter amiran) +pdfjs-bookmark-button-label = Asebter amiran + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ifecka +pdfjs-tools-button-label = Ifecka +pdfjs-first-page-button = + .title = Ddu ɣer usebter amezwaru +pdfjs-first-page-button-label = Ddu ɣer usebter amezwaru +pdfjs-last-page-button = + .title = Ddu ɣer usebter aneggaru +pdfjs-last-page-button-label = Ddu ɣer usebter aneggaru +pdfjs-page-rotate-cw-button = + .title = Tuzzya tusrigt +pdfjs-page-rotate-cw-button-label = Tuzzya tusrigt +pdfjs-page-rotate-ccw-button = + .title = Tuzzya amgal-usrig +pdfjs-page-rotate-ccw-button-label = Tuzzya amgal-usrig +pdfjs-cursor-text-select-tool-button = + .title = Rmed afecku n tefrant n uḍris +pdfjs-cursor-text-select-tool-button-label = Afecku n tefrant n uḍris +pdfjs-cursor-hand-tool-button = + .title = Rmed afecku afus +pdfjs-cursor-hand-tool-button-label = Afecku afus +pdfjs-scroll-page-button = + .title = Seqdec adrurem n usebter +pdfjs-scroll-page-button-label = Adrurem n usebter +pdfjs-scroll-vertical-button = + .title = Seqdec adrurem ubdid +pdfjs-scroll-vertical-button-label = Adrurem ubdid +pdfjs-scroll-horizontal-button = + .title = Seqdec adrurem aglawan +pdfjs-scroll-horizontal-button-label = Adrurem aglawan +pdfjs-scroll-wrapped-button = + .title = Seqdec adrurem yuẓen +pdfjs-scroll-wrapped-button-label = Adrurem yuẓen +pdfjs-spread-none-button = + .title = Ur sedday ara isiɣzaf n usebter +pdfjs-spread-none-button-label = Ulac isiɣzaf +pdfjs-spread-odd-button = + .title = Seddu isiɣzaf n usebter ibeddun s yisebtar irayuganen +pdfjs-spread-odd-button-label = Isiɣzaf irayuganen +pdfjs-spread-even-button = + .title = Seddu isiɣzaf n usebter ibeddun s yisebtar iyuganen +pdfjs-spread-even-button-label = Isiɣzaf iyuganen + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Taɣaṛa n isemli… +pdfjs-document-properties-button-label = Taɣaṛa n isemli… +pdfjs-document-properties-file-name = Isem n ufaylu: +pdfjs-document-properties-file-size = Teɣzi n ufaylu: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } yibiten) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } yibiten) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KAṬ ({ $size_b } ibiten) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MAṬ ({ $size_b } iṭamḍanen) +pdfjs-document-properties-title = Azwel: +pdfjs-document-properties-author = Ameskar: +pdfjs-document-properties-subject = Amgay: +pdfjs-document-properties-keywords = Awalen n tsaruţ +pdfjs-document-properties-creation-date = Azemz n tmerna: +pdfjs-document-properties-modification-date = Azemz n usnifel: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Yerna-t: +pdfjs-document-properties-producer = Afecku n uselket PDF: +pdfjs-document-properties-version = Lqem PDF: +pdfjs-document-properties-page-count = Amḍan n yisebtar: +pdfjs-document-properties-page-size = Tuγzi n usebter: +pdfjs-document-properties-page-size-unit-inches = deg +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = s teɣzi +pdfjs-document-properties-page-size-orientation-landscape = s tehri +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Asekkil +pdfjs-document-properties-page-size-name-legal = Usḍif + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Taskant Web taruradt: +pdfjs-document-properties-linearized-yes = Ih +pdfjs-document-properties-linearized-no = Ala +pdfjs-document-properties-close-button = Mdel + +## Print + +pdfjs-print-progress-message = Aheggi i usiggez n isemli… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Sefsex +pdfjs-printing-not-supported = Ɣuṛ-k: Asiggez ur ittusefrak ara yakan imaṛṛa deg iminig-a. +pdfjs-printing-not-ready = Ɣuṛ-k: Afaylu PDF ur d-yuli ara imeṛṛa akken ad ittusiggez. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Sken/Fer agalis adisan +pdfjs-toggle-sidebar-notification-button = + .title = Ffer/Sekn agalis adisan (isemli yegber aɣawas/ticeqqufin yeddan/tissiwin) +pdfjs-toggle-sidebar-button-label = Sken/Fer agalis adisan +pdfjs-document-outline-button = + .title = Sken isemli (Senned snat tikal i wesemɣer/Afneẓ n iferdisen meṛṛa) +pdfjs-document-outline-button-label = Isɣalen n isebtar +pdfjs-attachments-button = + .title = Sken ticeqqufin yeddan +pdfjs-attachments-button-label = Ticeqqufin yeddan +pdfjs-layers-button = + .title = Skeen tissiwin (sit sin yiberdan i uwennez n meṛṛa tissiwin ɣer waddad amezwer) +pdfjs-layers-button-label = Tissiwin +pdfjs-thumbs-button = + .title = Sken tanfult. +pdfjs-thumbs-button-label = Tinfulin +pdfjs-current-outline-item-button = + .title = Af-d aferdis n uɣawas amiran +pdfjs-current-outline-item-button-label = Aferdis n uɣawas amiran +pdfjs-findbar-button = + .title = Nadi deg isemli +pdfjs-findbar-button-label = Nadi +pdfjs-additional-layers = Tissiwin-nniḍen + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Asebter { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Tanfult n usebter { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Nadi + .placeholder = Nadi deg isemli… +pdfjs-find-previous-button = + .title = Aff-d tamseḍriwt n twinest n deffir +pdfjs-find-previous-button-label = Azewwar +pdfjs-find-next-button = + .title = Aff-d timseḍriwt n twinest d-iteddun +pdfjs-find-next-button-label = Ddu ɣer zdat +pdfjs-find-highlight-checkbox = Err izirig imaṛṛa +pdfjs-find-match-case-checkbox-label = Qadeṛ amasal n isekkilen +pdfjs-find-match-diacritics-checkbox-label = Qadeṛ ifeskilen +pdfjs-find-entire-word-checkbox-label = Awalen iččuranen +pdfjs-find-reached-top = Yabbeḍ s afella n usebter, tuɣalin s wadda +pdfjs-find-reached-bottom = Tebḍeḍ s adda n usebter, tuɣalin s afella +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Timeḍriwt { $current } ɣef { $total } + *[other] Timeḍriwin { $current } ɣef { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Ugar n { $limit } umṣada + *[other] Ugar n { $limit } yimṣadayen + } +pdfjs-find-not-found = Ulac tawinest + +## Predefined zoom values + +pdfjs-page-scale-width = Tehri n usebter +pdfjs-page-scale-fit = Asebter imaṛṛa +pdfjs-page-scale-auto = Asemɣeṛ/Asemẓi awurman +pdfjs-page-scale-actual = Teɣzi tilawt +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Asebter { $page } + +## Loading indicator messages + +pdfjs-loading-error = Teḍra-d tuccḍa deg alluy n PDF: +pdfjs-invalid-file-error = Afaylu PDF arameɣtu neɣ yexṣeṛ. +pdfjs-missing-file-error = Ulac afaylu PDF. +pdfjs-unexpected-response-error = Aqeddac yerra-d yir tiririt ur nettwaṛǧi ara. +pdfjs-rendering-error = Teḍra-d tuccḍa deg uskan n usebter. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Tabzimt { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Sekcem awal uffir akken ad ldiḍ afaylu-yagi PDF +pdfjs-password-invalid = Awal uffir mačči d ameɣtu, Ɛreḍ tikelt-nniḍen. +pdfjs-password-ok-button = IH +pdfjs-password-cancel-button = Sefsex +pdfjs-web-fonts-disabled = Tisefsiyin web ttwassensent; D awezɣi useqdec n tsefsiyin yettwarnan ɣer PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Aḍris +pdfjs-editor-free-text-button-label = Aḍris +pdfjs-editor-ink-button = + .title = Suneɣ +pdfjs-editor-ink-button-label = Suneɣ +pdfjs-editor-stamp-button = + .title = Rnu neɣ ẓreg tugniwin +pdfjs-editor-stamp-button-label = Rnu neɣ ẓreg tugniwin +pdfjs-editor-highlight-button = + .title = Derrer +pdfjs-editor-highlight-button-label = Derrer +pdfjs-highlight-floating-button1 = + .title = Derrer + .aria-label = Derrer +pdfjs-highlight-floating-button-label = Derrer + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Kkes asuneɣ +pdfjs-editor-remove-freetext-button = + .title = Kkes aḍris +pdfjs-editor-remove-stamp-button = + .title = Kkes tugna +pdfjs-editor-remove-highlight-button = + .title = Kkes aderrer + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Initen +pdfjs-editor-free-text-size-input = Teɣzi +pdfjs-editor-ink-color-input = Ini +pdfjs-editor-ink-thickness-input = Tuzert +pdfjs-editor-ink-opacity-input = Tebrek +pdfjs-editor-stamp-add-image-button = + .title = Rnu tawlaft +pdfjs-editor-stamp-add-image-button-label = Rnu tawlaft +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tuzert +pdfjs-editor-free-highlight-thickness-title = + .title = Beddel tuzert mi ara d-tesbeggneḍ iferdisen niḍen ur nelli d aḍris +pdfjs-free-text = + .aria-label = Amaẓrag n uḍris +pdfjs-free-text-default-content = Bdu tira... +pdfjs-ink = + .aria-label = Amaẓrag n usuneɣ +pdfjs-ink-canvas = + .aria-label = Tugna yettwarnan sɣur useqdac + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = Aḍris amaskal +pdfjs-editor-alt-text-edit-button-label = Ẓreg aḍris amaskal +pdfjs-editor-alt-text-dialog-label = Fren taxtirt +pdfjs-editor-alt-text-add-description-label = Rnu aglam +pdfjs-editor-alt-text-mark-decorative-label = Creḍ d adlag +pdfjs-editor-alt-text-cancel-button = Sefsex +pdfjs-editor-alt-text-save-button = Sekles +pdfjs-editor-alt-text-decorative-tooltip = Yettwacreḍ d adlag + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Tiɣmert n ufella n zelmeḍ — semsawi teɣzi +pdfjs-editor-resizer-label-top-middle = Talemmat n ufella — semsawi teɣzi +pdfjs-editor-resizer-label-top-right = Tiɣmert n ufella n yeffus — semsawi teɣzi +pdfjs-editor-resizer-label-middle-right = Talemmast tayeffust — semsawi teɣzi +pdfjs-editor-resizer-label-bottom-right = Tiɣmert n wadda n yeffus — semsawi teɣzi +pdfjs-editor-resizer-label-bottom-middle = Talemmat n wadda — semsawi teɣzi +pdfjs-editor-resizer-label-bottom-left = Tiɣmert n wadda n zelmeḍ — semsawi teɣzi +pdfjs-editor-resizer-label-middle-left = Talemmast tazelmdaḍt — semsawi teɣzi +pdfjs-editor-resizer-top-left = + .aria-label = Tiɣmert n ufella n zelmeḍ — semsawi teɣzi +pdfjs-editor-resizer-top-middle = + .aria-label = Talemmat n ufella — semsawi teɣzi +pdfjs-editor-resizer-top-right = + .aria-label = Tiɣmert n ufella n yeffus — semsawi teɣzi +pdfjs-editor-resizer-middle-right = + .aria-label = Talemmast tayeffust — semsawi teɣzi +pdfjs-editor-resizer-bottom-right = + .aria-label = Tiɣmert n wadda n yeffus — semsawi teɣzi +pdfjs-editor-resizer-bottom-middle = + .aria-label = Talemmat n wadda — semsawi teɣzi +pdfjs-editor-resizer-bottom-left = + .aria-label = Tiɣmert n wadda n zelmeḍ — semsawi teɣzi +pdfjs-editor-resizer-middle-left = + .aria-label = Talemmast tazelmdaḍt — semsawi teɣzi + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Ini n uderrer +pdfjs-editor-colorpicker-button = + .title = Senfel ini +pdfjs-editor-colorpicker-dropdown = + .aria-label = Afran n yiniten +pdfjs-editor-colorpicker-yellow = + .title = Awraɣ +pdfjs-editor-colorpicker-green = + .title = Azegzaw +pdfjs-editor-colorpicker-blue = + .title = Amidadi +pdfjs-editor-colorpicker-pink = + .title = Axuxi +pdfjs-editor-colorpicker-red = + .title = Azggaɣ + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Sken akk +pdfjs-editor-highlight-show-all-button = + .title = Sken akk + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Rnu aḍris niḍen (aglam n tugna) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Aru aglam-ik dagi… +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Issin ugar +pdfjs-editor-new-alt-text-create-automatically-button-label = Rnu aḍris niḍen s wudem awurman +pdfjs-editor-new-alt-text-not-now-button = Mačči tura +pdfjs-editor-new-alt-text-error-title = D awezɣi timerna n uḍris niḍen s wudem awurman +pdfjs-editor-new-alt-text-error-close-button = Mdel + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-delete-model-button = Kkes +pdfjs-editor-alt-text-settings-download-model-button = Sader +pdfjs-editor-alt-text-settings-downloading-model-button = Asader… +pdfjs-editor-alt-text-settings-close-button = Mdel diff --git a/public/assets/pdfjs/locale/kk/viewer.ftl b/public/assets/pdfjs/locale/kk/viewer.ftl new file mode 100644 index 0000000..fb14226 --- /dev/null +++ b/public/assets/pdfjs/locale/kk/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Алдыңғы парақ +pdfjs-previous-button-label = Алдыңғысы +pdfjs-next-button = + .title = Келесі парақ +pdfjs-next-button-label = Келесі +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Парақ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } ішінен +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = (парақ { $pageNumber }, { $pagesCount } ішінен) +pdfjs-zoom-out-button = + .title = Кішірейту +pdfjs-zoom-out-button-label = Кішірейту +pdfjs-zoom-in-button = + .title = Үлкейту +pdfjs-zoom-in-button-label = Үлкейту +pdfjs-zoom-select = + .title = Масштаб +pdfjs-presentation-mode-button = + .title = Презентация режиміне ауысу +pdfjs-presentation-mode-button-label = Презентация режимі +pdfjs-open-file-button = + .title = Файлды ашу +pdfjs-open-file-button-label = Ашу +pdfjs-print-button = + .title = Баспаға шығару +pdfjs-print-button-label = Баспаға шығару +pdfjs-save-button = + .title = Сақтау +pdfjs-save-button-label = Сақтау +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Жүктеп алу +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Жүктеп алу +pdfjs-bookmark-button = + .title = Ағымдағы бет (Ағымдағы беттен URL адресін көру) +pdfjs-bookmark-button-label = Ағымдағы бет + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Құралдар +pdfjs-tools-button-label = Құралдар +pdfjs-first-page-button = + .title = Алғашқы параққа өту +pdfjs-first-page-button-label = Алғашқы параққа өту +pdfjs-last-page-button = + .title = Соңғы параққа өту +pdfjs-last-page-button-label = Соңғы параққа өту +pdfjs-page-rotate-cw-button = + .title = Сағат тілі бағытымен айналдыру +pdfjs-page-rotate-cw-button-label = Сағат тілі бағытымен бұру +pdfjs-page-rotate-ccw-button = + .title = Сағат тілі бағытына қарсы бұру +pdfjs-page-rotate-ccw-button-label = Сағат тілі бағытына қарсы бұру +pdfjs-cursor-text-select-tool-button = + .title = Мәтінді таңдау құралын іске қосу +pdfjs-cursor-text-select-tool-button-label = Мәтінді таңдау құралы +pdfjs-cursor-hand-tool-button = + .title = Қол құралын іске қосу +pdfjs-cursor-hand-tool-button-label = Қол құралы +pdfjs-scroll-page-button = + .title = Беттерді айналдыруды пайдалану +pdfjs-scroll-page-button-label = Беттерді айналдыру +pdfjs-scroll-vertical-button = + .title = Вертикалды айналдыруды қолдану +pdfjs-scroll-vertical-button-label = Вертикалды айналдыру +pdfjs-scroll-horizontal-button = + .title = Горизонталды айналдыруды қолдану +pdfjs-scroll-horizontal-button-label = Горизонталды айналдыру +pdfjs-scroll-wrapped-button = + .title = Масштабталатын айналдыруды қолдану +pdfjs-scroll-wrapped-button-label = Масштабталатын айналдыру +pdfjs-spread-none-button = + .title = Жазық беттер режимін қолданбау +pdfjs-spread-none-button-label = Жазық беттер режимсіз +pdfjs-spread-odd-button = + .title = Жазық беттер тақ нөмірлі беттерден басталады +pdfjs-spread-odd-button-label = Тақ нөмірлі беттер сол жақтан +pdfjs-spread-even-button = + .title = Жазық беттер жұп нөмірлі беттерден басталады +pdfjs-spread-even-button-label = Жұп нөмірлі беттер сол жақтан + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Құжат қасиеттері… +pdfjs-document-properties-button-label = Құжат қасиеттері… +pdfjs-document-properties-file-name = Файл аты: +pdfjs-document-properties-file-size = Файл өлшемі: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } КБ ({ $b } байт) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байт) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } байт) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байт) +pdfjs-document-properties-title = Тақырыбы: +pdfjs-document-properties-author = Авторы: +pdfjs-document-properties-subject = Тақырыбы: +pdfjs-document-properties-keywords = Кілт сөздер: +pdfjs-document-properties-creation-date = Жасалған күні: +pdfjs-document-properties-modification-date = Түзету күні: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Жасаған: +pdfjs-document-properties-producer = PDF өндірген: +pdfjs-document-properties-version = PDF нұсқасы: +pdfjs-document-properties-page-count = Беттер саны: +pdfjs-document-properties-page-size = Бет өлшемі: +pdfjs-document-properties-page-size-unit-inches = дюйм +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = тік +pdfjs-document-properties-page-size-orientation-landscape = жатық +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Жылдам Web көрінісі: +pdfjs-document-properties-linearized-yes = Иә +pdfjs-document-properties-linearized-no = Жоқ +pdfjs-document-properties-close-button = Жабу + +## Print + +pdfjs-print-progress-message = Құжатты баспаға шығару үшін дайындау… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Бас тарту +pdfjs-printing-not-supported = Ескерту: Баспаға шығаруды бұл браузер толығымен қолдамайды. +pdfjs-printing-not-ready = Ескерту: Баспаға шығару үшін, бұл PDF толығымен жүктеліп алынбады. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Бүйір панелін көрсету/жасыру +pdfjs-toggle-sidebar-notification-button = + .title = Бүйір панелін көрсету/жасыру (құжатта құрылымы/салынымдар/қабаттар бар) +pdfjs-toggle-sidebar-button-label = Бүйір панелін көрсету/жасыру +pdfjs-document-outline-button = + .title = Құжат құрылымын көрсету (барлық нәрселерді жазық қылу/жинау үшін қос шерту керек) +pdfjs-document-outline-button-label = Құжат құрамасы +pdfjs-attachments-button = + .title = Салынымдарды көрсету +pdfjs-attachments-button-label = Салынымдар +pdfjs-layers-button = + .title = Қабаттарды көрсету (барлық қабаттарды бастапқы күйге келтіру үшін екі рет шертіңіз) +pdfjs-layers-button-label = Қабаттар +pdfjs-thumbs-button = + .title = Кіші көріністерді көрсету +pdfjs-thumbs-button-label = Кіші көріністер +pdfjs-current-outline-item-button = + .title = Құрылымның ағымдағы элементін табу +pdfjs-current-outline-item-button-label = Құрылымның ағымдағы элементі +pdfjs-findbar-button = + .title = Құжаттан табу +pdfjs-findbar-button-label = Табу +pdfjs-additional-layers = Қосымша қабаттар + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } парағы +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } парағы үшін кіші көрінісі + +## Find panel button title and messages + +pdfjs-find-input = + .title = Табу + .placeholder = Құжаттан табу… +pdfjs-find-previous-button = + .title = Осы сөздердің мәтіннен алдыңғы кездесуін табу +pdfjs-find-previous-button-label = Алдыңғысы +pdfjs-find-next-button = + .title = Осы сөздердің мәтіннен келесі кездесуін табу +pdfjs-find-next-button-label = Келесі +pdfjs-find-highlight-checkbox = Барлығын түспен ерекшелеу +pdfjs-find-match-case-checkbox-label = Регистрді ескеру +pdfjs-find-match-diacritics-checkbox-label = Диакритиканы ескеру +pdfjs-find-entire-word-checkbox-label = Сөздер толығымен +pdfjs-find-reached-top = Құжаттың басына жеттік, соңынан бастап жалғастырамыз +pdfjs-find-reached-bottom = Құжаттың соңына жеттік, басынан бастап жалғастырамыз +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } сәйкестік, барлығы { $total } + *[other] { $current } сәйкестік, барлығы { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] { $limit } сәйкестіктен көп + *[other] { $limit } сәйкестіктен көп + } +pdfjs-find-not-found = Сөз(дер) табылмады + +## Predefined zoom values + +pdfjs-page-scale-width = Парақ ені +pdfjs-page-scale-fit = Парақты сыйдыру +pdfjs-page-scale-auto = Автомасштабтау +pdfjs-page-scale-actual = Нақты өлшемі +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Бет { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF жүктеу кезінде қате кетті. +pdfjs-invalid-file-error = Зақымдалған немесе қате PDF файл. +pdfjs-missing-file-error = PDF файлы жоқ. +pdfjs-unexpected-response-error = Сервердің күтпеген жауабы. +pdfjs-rendering-error = Парақты өңдеу кезінде қате кетті. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } аңдатпасы] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Бұл PDF файлын ашу үшін парольді енгізіңіз. +pdfjs-password-invalid = Пароль дұрыс емес. Қайталап көріңіз. +pdfjs-password-ok-button = ОК +pdfjs-password-cancel-button = Бас тарту +pdfjs-web-fonts-disabled = Веб қаріптері сөндірілген: құрамына енгізілген PDF қаріптерін қолдану мүмкін емес. + +## Editing + +pdfjs-editor-free-text-button = + .title = Мәтін +pdfjs-editor-free-text-button-label = Мәтін +pdfjs-editor-ink-button = + .title = Сурет салу +pdfjs-editor-ink-button-label = Сурет салу +pdfjs-editor-stamp-button = + .title = Суреттерді қосу немесе түзету +pdfjs-editor-stamp-button-label = Суреттерді қосу немесе түзету +pdfjs-editor-highlight-button = + .title = Ерекшелеу +pdfjs-editor-highlight-button-label = Ерекшелеу +pdfjs-highlight-floating-button1 = + .title = Ерекшелеу + .aria-label = Ерекшелеу +pdfjs-highlight-floating-button-label = Ерекшелеу + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Сызбаны өшіру +pdfjs-editor-remove-freetext-button = + .title = Мәтінді өшіру +pdfjs-editor-remove-stamp-button = + .title = Суретті өшіру +pdfjs-editor-remove-highlight-button = + .title = Түспен ерекшелеуді өшіру + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Түс +pdfjs-editor-free-text-size-input = Өлшемі +pdfjs-editor-ink-color-input = Түс +pdfjs-editor-ink-thickness-input = Қалыңдығы +pdfjs-editor-ink-opacity-input = Мөлдірсіздігі +pdfjs-editor-stamp-add-image-button = + .title = Суретті қосу +pdfjs-editor-stamp-add-image-button-label = Суретті қосу +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Қалыңдығы +pdfjs-editor-free-highlight-thickness-title = + .title = Мәтіннен басқа элементтерді ерекшелеу кезінде қалыңдықты өзгерту +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Мәтін түзеткіші + .default-content = Теріп бастаңыз… +pdfjs-free-text = + .aria-label = Мәтін түзеткіші +pdfjs-free-text-default-content = Теруді бастау… +pdfjs-ink = + .aria-label = Сурет түзеткіші +pdfjs-ink-canvas = + .aria-label = Пайдаланушы жасаған сурет + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Балама мәтін +pdfjs-editor-alt-text-edit-button = + .aria-label = Балама мәтінді өңдеу +pdfjs-editor-alt-text-edit-button-label = Балама мәтінді өңдеу +pdfjs-editor-alt-text-dialog-label = Опцияны таңдау +pdfjs-editor-alt-text-dialog-description = Балама мәтін адамдар суретті көре алмағанда немесе ол жүктелмегенде көмектеседі. +pdfjs-editor-alt-text-add-description-label = Сипаттаманы қосу +pdfjs-editor-alt-text-add-description-description = Тақырыпты, баптауды немесе әрекетті сипаттайтын 1-2 сөйлемді қолдануға тырысыңыз. +pdfjs-editor-alt-text-mark-decorative-label = Декоративті деп белгілеу +pdfjs-editor-alt-text-mark-decorative-description = Бұл жиектер немесе су белгілері сияқты оюлық суреттер үшін пайдаланылады. +pdfjs-editor-alt-text-cancel-button = Бас тарту +pdfjs-editor-alt-text-save-button = Сақтау +pdfjs-editor-alt-text-decorative-tooltip = Декоративті деп белгіленген +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Мысалы, "Жас жігіт тамақ ішу үшін үстел басына отырады" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Балама мәтін + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Жоғарғы сол жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-label-top-middle = Жоғарғы ортасы — өлшемін өзгерту +pdfjs-editor-resizer-label-top-right = Жоғарғы оң жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-label-middle-right = Ортаңғы оң жақ — өлшемін өзгерту +pdfjs-editor-resizer-label-bottom-right = Төменгі оң жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-label-bottom-middle = Төменгі ортасы — өлшемін өзгерту +pdfjs-editor-resizer-label-bottom-left = Төменгі сол жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-label-middle-left = Ортаңғы сол жақ — өлшемін өзгерту +pdfjs-editor-resizer-top-left = + .aria-label = Жоғарғы сол жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-top-middle = + .aria-label = Жоғарғы ортасы — өлшемін өзгерту +pdfjs-editor-resizer-top-right = + .aria-label = Жоғарғы оң жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-middle-right = + .aria-label = Ортаңғы оң жақ — өлшемін өзгерту +pdfjs-editor-resizer-bottom-right = + .aria-label = Төменгі оң жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-bottom-middle = + .aria-label = Төменгі ортасы — өлшемін өзгерту +pdfjs-editor-resizer-bottom-left = + .aria-label = Төменгі сол жақ бұрыш — өлшемін өзгерту +pdfjs-editor-resizer-middle-left = + .aria-label = Ортаңғы сол жақ — өлшемін өзгерту + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Ерекшелеу түсі +pdfjs-editor-colorpicker-button = + .title = Түсті өзгерту +pdfjs-editor-colorpicker-dropdown = + .aria-label = Түс таңдаулары +pdfjs-editor-colorpicker-yellow = + .title = Сары +pdfjs-editor-colorpicker-green = + .title = Жасыл +pdfjs-editor-colorpicker-blue = + .title = Көк +pdfjs-editor-colorpicker-pink = + .title = Қызғылт +pdfjs-editor-colorpicker-red = + .title = Қызыл + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Барлығын көрсету +pdfjs-editor-highlight-show-all-button = + .title = Барлығын көрсету + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Балама мәтінді өңдеу (сурет сипаттамасы) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Балама мәтінді қосу (сурет сипаттамасы) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Сипаттамаңызды осында жазыңыз… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Суретті көре алмайтын адамдар үшін немесе сурет жүктелмеген кезіне арналған қысқаша сипаттама. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Бұл балама мәтін автоматты түрде жасалды және дәлсіз болуы мүмкін. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Көбірек білу +pdfjs-editor-new-alt-text-create-automatically-button-label = Балама мәтінді автоматты түрде жасау +pdfjs-editor-new-alt-text-not-now-button = Қазір емес +pdfjs-editor-new-alt-text-error-title = Балама мәтінді автоматты түрде жасау мүмкін болмады +pdfjs-editor-new-alt-text-error-description = Өзіңіздің балама мәтініңізді жазыңыз немесе кейінірек қайталап көріңіз. +pdfjs-editor-new-alt-text-error-close-button = Жабу +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Балама мәтін үшін ЖИ моделі жүктеп алынуда ({ $downloadedSize }/{ $totalSize } МБ) + .aria-valuetext = Балама мәтін үшін ЖИ моделі жүктеп алынуда ({ $downloadedSize }/{ $totalSize } МБ) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Балама мәтін қосылды +pdfjs-editor-new-alt-text-added-button-label = Балама мәтін қосылды +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Балама мәтін жоқ +pdfjs-editor-new-alt-text-missing-button-label = Балама мәтін жоқ +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Балама мәтінге пікір қалдыру +pdfjs-editor-new-alt-text-to-review-button-label = Балама мәтінге пікір қалдыру +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Автоматты түрде жасалды: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Суреттің балама мәтінінің баптаулары +pdfjs-image-alt-text-settings-button-label = Суреттің балама мәтінінің баптаулары +pdfjs-editor-alt-text-settings-dialog-label = Суреттің балама мәтінінің баптаулары +pdfjs-editor-alt-text-settings-automatic-title = Автоматты балама мәтін +pdfjs-editor-alt-text-settings-create-model-button-label = Балама мәтінді автоматты түрде жасау +pdfjs-editor-alt-text-settings-create-model-description = Суретті көре алмайтын адамдар үшін немесе сурет жүктелмеген кезіне арналған сипаттамаларды ұсынады. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Баламалы мәтіннің ЖИ моделі ({ $totalSize } МБ) +pdfjs-editor-alt-text-settings-ai-model-description = Деректеріңіз жеке болып қалуы үшін құрылғыңызда жергілікті түрде жұмыс істейді. Автоматты балама мәтін үшін қажет. +pdfjs-editor-alt-text-settings-delete-model-button = Өшіру +pdfjs-editor-alt-text-settings-download-model-button = Жүктеп алу +pdfjs-editor-alt-text-settings-downloading-model-button = Жүктеліп алынуда… +pdfjs-editor-alt-text-settings-editor-title = Баламалы мәтін редакторы +pdfjs-editor-alt-text-settings-show-dialog-button-label = Суретті қосқанда балама мәтін редакторын бірден көрсету +pdfjs-editor-alt-text-settings-show-dialog-description = Барлық суреттерде балама мәтін бар екеніне көз жеткізуге көмектеседі. +pdfjs-editor-alt-text-settings-close-button = Жабу + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Ерекшелеу өшірілді +pdfjs-editor-undo-bar-message-freetext = Мәтін өшірілді +pdfjs-editor-undo-bar-message-ink = Сызба өшірілді +pdfjs-editor-undo-bar-message-stamp = Сурет өшірілді +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } анимация өшірілді + *[other] { $count } анимация өшірілді + } +pdfjs-editor-undo-bar-undo-button = + .title = Болдырмау +pdfjs-editor-undo-bar-undo-button-label = Болдырмау +pdfjs-editor-undo-bar-close-button = + .title = Жабу +pdfjs-editor-undo-bar-close-button-label = Жабу diff --git a/public/assets/pdfjs/locale/km/viewer.ftl b/public/assets/pdfjs/locale/km/viewer.ftl new file mode 100644 index 0000000..6efd105 --- /dev/null +++ b/public/assets/pdfjs/locale/km/viewer.ftl @@ -0,0 +1,223 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = ទំព័រ​មុន +pdfjs-previous-button-label = មុន +pdfjs-next-button = + .title = ទំព័រ​បន្ទាប់ +pdfjs-next-button-label = បន្ទាប់ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ទំព័រ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = នៃ { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } នៃ { $pagesCount }) +pdfjs-zoom-out-button = + .title = ​បង្រួម +pdfjs-zoom-out-button-label = ​បង្រួម +pdfjs-zoom-in-button = + .title = ​ពង្រីក +pdfjs-zoom-in-button-label = ​ពង្រីក +pdfjs-zoom-select = + .title = ពង្រីក +pdfjs-presentation-mode-button = + .title = ប្ដូរ​ទៅ​របៀប​បទ​បង្ហាញ +pdfjs-presentation-mode-button-label = របៀប​បទ​បង្ហាញ +pdfjs-open-file-button = + .title = បើក​ឯកសារ +pdfjs-open-file-button-label = បើក +pdfjs-print-button = + .title = បោះពុម្ព +pdfjs-print-button-label = បោះពុម្ព + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ឧបករណ៍ +pdfjs-tools-button-label = ឧបករណ៍ +pdfjs-first-page-button = + .title = ទៅកាន់​ទំព័រ​ដំបូង​ +pdfjs-first-page-button-label = ទៅកាន់​ទំព័រ​ដំបូង​ +pdfjs-last-page-button = + .title = ទៅកាន់​ទំព័រ​ចុងក្រោយ​ +pdfjs-last-page-button-label = ទៅកាន់​ទំព័រ​ចុងក្រោយ +pdfjs-page-rotate-cw-button = + .title = បង្វិល​ស្រប​ទ្រនិច​នាឡិកា +pdfjs-page-rotate-cw-button-label = បង្វិល​ស្រប​ទ្រនិច​នាឡិកា +pdfjs-page-rotate-ccw-button = + .title = បង្វិល​ច្រាស​ទ្រនិច​នាឡិកា​​ +pdfjs-page-rotate-ccw-button-label = បង្វិល​ច្រាស​ទ្រនិច​នាឡិកា​​ +pdfjs-cursor-text-select-tool-button = + .title = បើក​ឧបករណ៍​ជ្រើស​អត្ថបទ +pdfjs-cursor-text-select-tool-button-label = ឧបករណ៍​ជ្រើស​អត្ថបទ +pdfjs-cursor-hand-tool-button = + .title = បើក​ឧបករណ៍​ដៃ +pdfjs-cursor-hand-tool-button-label = ឧបករណ៍​ដៃ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = លក្ខណ​សម្បត្តិ​ឯកសារ… +pdfjs-document-properties-button-label = លក្ខណ​សម្បត្តិ​ឯកសារ… +pdfjs-document-properties-file-name = ឈ្មោះ​ឯកសារ៖ +pdfjs-document-properties-file-size = ទំហំ​ឯកសារ៖ +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } បៃ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } បៃ) +pdfjs-document-properties-title = ចំណងជើង៖ +pdfjs-document-properties-author = អ្នក​និពន្ធ៖ +pdfjs-document-properties-subject = ប្រធានបទ៖ +pdfjs-document-properties-keywords = ពាក្យ​គន្លឹះ៖ +pdfjs-document-properties-creation-date = កាលបរិច្ឆេទ​បង្កើត៖ +pdfjs-document-properties-modification-date = កាលបរិច្ឆេទ​កែប្រែ៖ +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = អ្នក​បង្កើត៖ +pdfjs-document-properties-producer = កម្មវិធី​បង្កើត PDF ៖ +pdfjs-document-properties-version = កំណែ PDF ៖ +pdfjs-document-properties-page-count = ចំនួន​ទំព័រ៖ +pdfjs-document-properties-page-size-unit-inches = អ៊ីញ +pdfjs-document-properties-page-size-unit-millimeters = មម +pdfjs-document-properties-page-size-orientation-portrait = បញ្ឈរ +pdfjs-document-properties-page-size-orientation-landscape = ផ្តេក +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = សំបុត្រ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = បាទ/ចាស +pdfjs-document-properties-linearized-no = ទេ +pdfjs-document-properties-close-button = បិទ + +## Print + +pdfjs-print-progress-message = កំពុង​រៀបចំ​ឯកសារ​សម្រាប់​បោះពុម្ព… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = បោះបង់ +pdfjs-printing-not-supported = ការ​ព្រមាន ៖ កា​រ​បោះពុម្ព​មិន​ត្រូវ​បាន​គាំទ្រ​ពេញលេញ​ដោយ​កម្មវិធី​រុករក​នេះ​ទេ ។ +pdfjs-printing-not-ready = ព្រមាន៖ PDF មិន​ត្រូវ​បាន​ផ្ទុក​ទាំងស្រុង​ដើម្បី​បោះពុម្ព​ទេ។ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = បិទ/បើក​គ្រាប់​រំកិល +pdfjs-toggle-sidebar-button-label = បិទ/បើក​គ្រាប់​រំកិល +pdfjs-document-outline-button = + .title = បង្ហាញ​គ្រោង​ឯកសារ (ចុច​ទ្វេ​ដង​ដើម្បី​ពង្រីក/បង្រួម​ធាតុ​ទាំងអស់) +pdfjs-document-outline-button-label = គ្រោង​ឯកសារ +pdfjs-attachments-button = + .title = បង្ហាញ​ឯកសារ​ភ្ជាប់ +pdfjs-attachments-button-label = ឯកសារ​ភ្ជាប់ +pdfjs-thumbs-button = + .title = បង្ហាញ​រូបភាព​តូចៗ +pdfjs-thumbs-button-label = រួបភាព​តូចៗ +pdfjs-findbar-button = + .title = រក​នៅ​ក្នុង​ឯកសារ +pdfjs-findbar-button-label = រក + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = ទំព័រ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = រូបភាព​តូច​របស់​ទំព័រ { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = រក + .placeholder = រក​នៅ​ក្នុង​ឯកសារ... +pdfjs-find-previous-button = + .title = រក​ពាក្យ ឬ​ឃ្លា​ដែល​បាន​ជួប​មុន +pdfjs-find-previous-button-label = មុន +pdfjs-find-next-button = + .title = រក​ពាក្យ ឬ​ឃ្លា​ដែល​បាន​ជួប​បន្ទាប់ +pdfjs-find-next-button-label = បន្ទាប់ +pdfjs-find-highlight-checkbox = បន្លិច​ទាំងអស់ +pdfjs-find-match-case-checkbox-label = ករណី​ដំណូច +pdfjs-find-reached-top = បាន​បន្ត​ពី​ខាង​ក្រោម ទៅ​ដល់​ខាង​​លើ​នៃ​ឯកសារ +pdfjs-find-reached-bottom = បាន​បន្ត​ពី​ខាងលើ ទៅដល់​ចុង​​នៃ​ឯកសារ +pdfjs-find-not-found = រក​មិន​ឃើញ​ពាក្យ ឬ​ឃ្លា + +## Predefined zoom values + +pdfjs-page-scale-width = ទទឹង​ទំព័រ +pdfjs-page-scale-fit = សម​ទំព័រ +pdfjs-page-scale-auto = ពង្រីក​ស្វ័យប្រវត្តិ +pdfjs-page-scale-actual = ទំហំ​ជាក់ស្ដែង +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = មាន​កំហុស​បាន​កើតឡើង​ពេល​កំពុង​ផ្ទុក PDF ។ +pdfjs-invalid-file-error = ឯកសារ PDF ខូច ឬ​មិន​ត្រឹមត្រូវ ។ +pdfjs-missing-file-error = បាត់​ឯកសារ PDF +pdfjs-unexpected-response-error = ការ​ឆ្លើយ​តម​ម៉ាស៊ីន​មេ​ដែល​មិន​បាន​រំពឹង។ +pdfjs-rendering-error = មាន​កំហុស​បាន​កើតឡើង​ពេល​បង្ហាញ​ទំព័រ ។ + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ចំណារ​ពន្យល់] + +## Password + +pdfjs-password-label = បញ្ចូល​ពាក្យសម្ងាត់​ដើម្បី​បើក​ឯកសារ PDF នេះ។ +pdfjs-password-invalid = ពាក្យសម្ងាត់​មិន​ត្រឹមត្រូវ។ សូម​ព្យាយាម​ម្ដងទៀត។ +pdfjs-password-ok-button = យល់​ព្រម +pdfjs-password-cancel-button = បោះបង់ +pdfjs-web-fonts-disabled = បាន​បិទ​ពុម្ពអក្សរ​បណ្ដាញ ៖ មិន​អាច​ប្រើ​ពុម្ពអក្សរ PDF ដែល​បាន​បង្កប់​បាន​ទេ ។ + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/kn/viewer.ftl b/public/assets/pdfjs/locale/kn/viewer.ftl new file mode 100644 index 0000000..0332255 --- /dev/null +++ b/public/assets/pdfjs/locale/kn/viewer.ftl @@ -0,0 +1,213 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = ಹಿಂದಿನ ಪುಟ +pdfjs-previous-button-label = ಹಿಂದಿನ +pdfjs-next-button = + .title = ಮುಂದಿನ ಪುಟ +pdfjs-next-button-label = ಮುಂದಿನ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ಪುಟ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } ರಲ್ಲಿ +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } ರಲ್ಲಿ { $pageNumber }) +pdfjs-zoom-out-button = + .title = ಕಿರಿದಾಗಿಸು +pdfjs-zoom-out-button-label = ಕಿರಿದಾಗಿಸಿ +pdfjs-zoom-in-button = + .title = ಹಿರಿದಾಗಿಸು +pdfjs-zoom-in-button-label = ಹಿರಿದಾಗಿಸಿ +pdfjs-zoom-select = + .title = ಗಾತ್ರಬದಲಿಸು +pdfjs-presentation-mode-button = + .title = ಪ್ರಸ್ತುತಿ (ಪ್ರಸೆಂಟೇಶನ್) ಕ್ರಮಕ್ಕೆ ಬದಲಾಯಿಸು +pdfjs-presentation-mode-button-label = ಪ್ರಸ್ತುತಿ (ಪ್ರಸೆಂಟೇಶನ್) ಕ್ರಮ +pdfjs-open-file-button = + .title = ಕಡತವನ್ನು ತೆರೆ +pdfjs-open-file-button-label = ತೆರೆಯಿರಿ +pdfjs-print-button = + .title = ಮುದ್ರಿಸು +pdfjs-print-button-label = ಮುದ್ರಿಸಿ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ಉಪಕರಣಗಳು +pdfjs-tools-button-label = ಉಪಕರಣಗಳು +pdfjs-first-page-button = + .title = ಮೊದಲ ಪುಟಕ್ಕೆ ತೆರಳು +pdfjs-first-page-button-label = ಮೊದಲ ಪುಟಕ್ಕೆ ತೆರಳು +pdfjs-last-page-button = + .title = ಕೊನೆಯ ಪುಟಕ್ಕೆ ತೆರಳು +pdfjs-last-page-button-label = ಕೊನೆಯ ಪುಟಕ್ಕೆ ತೆರಳು +pdfjs-page-rotate-cw-button = + .title = ಪ್ರದಕ್ಷಿಣೆಯಲ್ಲಿ ತಿರುಗಿಸು +pdfjs-page-rotate-cw-button-label = ಪ್ರದಕ್ಷಿಣೆಯಲ್ಲಿ ತಿರುಗಿಸು +pdfjs-page-rotate-ccw-button = + .title = ಅಪ್ರದಕ್ಷಿಣೆಯಲ್ಲಿ ತಿರುಗಿಸು +pdfjs-page-rotate-ccw-button-label = ಅಪ್ರದಕ್ಷಿಣೆಯಲ್ಲಿ ತಿರುಗಿಸು +pdfjs-cursor-text-select-tool-button = + .title = ಪಠ್ಯ ಆಯ್ಕೆ ಉಪಕರಣವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ +pdfjs-cursor-text-select-tool-button-label = ಪಠ್ಯ ಆಯ್ಕೆಯ ಉಪಕರಣ +pdfjs-cursor-hand-tool-button = + .title = ಕೈ ಉಪಕರಣವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ +pdfjs-cursor-hand-tool-button-label = ಕೈ ಉಪಕರಣ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = ಡಾಕ್ಯುಮೆಂಟ್‌ ಗುಣಗಳು... +pdfjs-document-properties-button-label = ಡಾಕ್ಯುಮೆಂಟ್‌ ಗುಣಗಳು... +pdfjs-document-properties-file-name = ಕಡತದ ಹೆಸರು: +pdfjs-document-properties-file-size = ಕಡತದ ಗಾತ್ರ: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } ಬೈಟ್‍ಗಳು) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } ಬೈಟ್‍ಗಳು) +pdfjs-document-properties-title = ಶೀರ್ಷಿಕೆ: +pdfjs-document-properties-author = ಕರ್ತೃ: +pdfjs-document-properties-subject = ವಿಷಯ: +pdfjs-document-properties-keywords = ಮುಖ್ಯಪದಗಳು: +pdfjs-document-properties-creation-date = ರಚಿಸಿದ ದಿನಾಂಕ: +pdfjs-document-properties-modification-date = ಮಾರ್ಪಡಿಸಲಾದ ದಿನಾಂಕ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ರಚಿಸಿದವರು: +pdfjs-document-properties-producer = PDF ಉತ್ಪಾದಕ: +pdfjs-document-properties-version = PDF ಆವೃತ್ತಿ: +pdfjs-document-properties-page-count = ಪುಟದ ಎಣಿಕೆ: +pdfjs-document-properties-page-size-unit-inches = ಇದರಲ್ಲಿ +pdfjs-document-properties-page-size-orientation-portrait = ಭಾವಚಿತ್ರ +pdfjs-document-properties-page-size-orientation-landscape = ಪ್ರಕೃತಿ ಚಿತ್ರ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = ಮುಚ್ಚು + +## Print + +pdfjs-print-progress-message = ಮುದ್ರಿಸುವುದಕ್ಕಾಗಿ ದಸ್ತಾವೇಜನ್ನು ಸಿದ್ಧಗೊಳಿಸಲಾಗುತ್ತಿದೆ… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ರದ್ದು ಮಾಡು +pdfjs-printing-not-supported = ಎಚ್ಚರಿಕೆ: ಈ ಜಾಲವೀಕ್ಷಕದಲ್ಲಿ ಮುದ್ರಣಕ್ಕೆ ಸಂಪೂರ್ಣ ಬೆಂಬಲವಿಲ್ಲ. +pdfjs-printing-not-ready = ಎಚ್ಚರಿಕೆ: PDF ಕಡತವು ಮುದ್ರಿಸಲು ಸಂಪೂರ್ಣವಾಗಿ ಲೋಡ್ ಆಗಿಲ್ಲ. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ಬದಿಪಟ್ಟಿಯನ್ನು ಹೊರಳಿಸು +pdfjs-toggle-sidebar-button-label = ಬದಿಪಟ್ಟಿಯನ್ನು ಹೊರಳಿಸು +pdfjs-document-outline-button-label = ದಸ್ತಾವೇಜಿನ ಹೊರರೇಖೆ +pdfjs-attachments-button = + .title = ಲಗತ್ತುಗಳನ್ನು ತೋರಿಸು +pdfjs-attachments-button-label = ಲಗತ್ತುಗಳು +pdfjs-thumbs-button = + .title = ಚಿಕ್ಕಚಿತ್ರದಂತೆ ತೋರಿಸು +pdfjs-thumbs-button-label = ಚಿಕ್ಕಚಿತ್ರಗಳು +pdfjs-findbar-button = + .title = ದಸ್ತಾವೇಜಿನಲ್ಲಿ ಹುಡುಕು +pdfjs-findbar-button-label = ಹುಡುಕು + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = ಪುಟ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = ಪುಟವನ್ನು ಚಿಕ್ಕಚಿತ್ರದಂತೆ ತೋರಿಸು { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ಹುಡುಕು + .placeholder = ದಸ್ತಾವೇಜಿನಲ್ಲಿ ಹುಡುಕು… +pdfjs-find-previous-button = + .title = ವಾಕ್ಯದ ಹಿಂದಿನ ಇರುವಿಕೆಯನ್ನು ಹುಡುಕು +pdfjs-find-previous-button-label = ಹಿಂದಿನ +pdfjs-find-next-button = + .title = ವಾಕ್ಯದ ಮುಂದಿನ ಇರುವಿಕೆಯನ್ನು ಹುಡುಕು +pdfjs-find-next-button-label = ಮುಂದಿನ +pdfjs-find-highlight-checkbox = ಎಲ್ಲವನ್ನು ಹೈಲೈಟ್ ಮಾಡು +pdfjs-find-match-case-checkbox-label = ಕೇಸನ್ನು ಹೊಂದಿಸು +pdfjs-find-reached-top = ದಸ್ತಾವೇಜಿನ ಮೇಲ್ಭಾಗವನ್ನು ತಲುಪಿದೆ, ಕೆಳಗಿನಿಂದ ಆರಂಭಿಸು +pdfjs-find-reached-bottom = ದಸ್ತಾವೇಜಿನ ಕೊನೆಯನ್ನು ತಲುಪಿದೆ, ಮೇಲಿನಿಂದ ಆರಂಭಿಸು +pdfjs-find-not-found = ವಾಕ್ಯವು ಕಂಡು ಬಂದಿಲ್ಲ + +## Predefined zoom values + +pdfjs-page-scale-width = ಪುಟದ ಅಗಲ +pdfjs-page-scale-fit = ಪುಟದ ಸರಿಹೊಂದಿಕೆ +pdfjs-page-scale-auto = ಸ್ವಯಂಚಾಲಿತ ಗಾತ್ರಬದಲಾವಣೆ +pdfjs-page-scale-actual = ನಿಜವಾದ ಗಾತ್ರ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF ಅನ್ನು ಲೋಡ್ ಮಾಡುವಾಗ ಒಂದು ದೋಷ ಎದುರಾಗಿದೆ. +pdfjs-invalid-file-error = ಅಮಾನ್ಯವಾದ ಅಥವ ಹಾಳಾದ PDF ಕಡತ. +pdfjs-missing-file-error = PDF ಕಡತ ಇಲ್ಲ. +pdfjs-unexpected-response-error = ಅನಿರೀಕ್ಷಿತವಾದ ಪೂರೈಕೆಗಣಕದ ಪ್ರತಿಕ್ರಿಯೆ. +pdfjs-rendering-error = ಪುಟವನ್ನು ನಿರೂಪಿಸುವಾಗ ಒಂದು ದೋಷ ಎದುರಾಗಿದೆ. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ಟಿಪ್ಪಣಿ] + +## Password + +pdfjs-password-label = PDF ಅನ್ನು ತೆರೆಯಲು ಗುಪ್ತಪದವನ್ನು ನಮೂದಿಸಿ. +pdfjs-password-invalid = ಅಮಾನ್ಯವಾದ ಗುಪ್ತಪದ, ದಯವಿಟ್ಟು ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = ರದ್ದು ಮಾಡು +pdfjs-web-fonts-disabled = ಜಾಲ ಅಕ್ಷರಶೈಲಿಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ: ಅಡಕಗೊಳಿಸಿದ PDF ಅಕ್ಷರಶೈಲಿಗಳನ್ನು ಬಳಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ko/viewer.ftl b/public/assets/pdfjs/locale/ko/viewer.ftl new file mode 100644 index 0000000..a321a11 --- /dev/null +++ b/public/assets/pdfjs/locale/ko/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = 이전 페이지 +pdfjs-previous-button-label = 이전 +pdfjs-next-button = + .title = 다음 페이지 +pdfjs-next-button-label = 다음 +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = 페이지 +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = 축소 +pdfjs-zoom-out-button-label = 축소 +pdfjs-zoom-in-button = + .title = 확대 +pdfjs-zoom-in-button-label = 확대 +pdfjs-zoom-select = + .title = 확대/축소 +pdfjs-presentation-mode-button = + .title = 프레젠테이션 모드로 전환 +pdfjs-presentation-mode-button-label = 프레젠테이션 모드 +pdfjs-open-file-button = + .title = 파일 열기 +pdfjs-open-file-button-label = 열기 +pdfjs-print-button = + .title = 인쇄 +pdfjs-print-button-label = 인쇄 +pdfjs-save-button = + .title = 저장 +pdfjs-save-button-label = 저장 +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = 다운로드 +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = 다운로드 +pdfjs-bookmark-button = + .title = 현재 페이지 (현재 페이지에서 URL 보기) +pdfjs-bookmark-button-label = 현재 페이지 + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = 도구 +pdfjs-tools-button-label = 도구 +pdfjs-first-page-button = + .title = 첫 페이지로 이동 +pdfjs-first-page-button-label = 첫 페이지로 이동 +pdfjs-last-page-button = + .title = 마지막 페이지로 이동 +pdfjs-last-page-button-label = 마지막 페이지로 이동 +pdfjs-page-rotate-cw-button = + .title = 시계방향으로 회전 +pdfjs-page-rotate-cw-button-label = 시계방향으로 회전 +pdfjs-page-rotate-ccw-button = + .title = 시계 반대방향으로 회전 +pdfjs-page-rotate-ccw-button-label = 시계 반대방향으로 회전 +pdfjs-cursor-text-select-tool-button = + .title = 텍스트 선택 도구 활성화 +pdfjs-cursor-text-select-tool-button-label = 텍스트 선택 도구 +pdfjs-cursor-hand-tool-button = + .title = 손 도구 활성화 +pdfjs-cursor-hand-tool-button-label = 손 도구 +pdfjs-scroll-page-button = + .title = 페이지 스크롤 사용 +pdfjs-scroll-page-button-label = 페이지 스크롤 +pdfjs-scroll-vertical-button = + .title = 세로 스크롤 사용 +pdfjs-scroll-vertical-button-label = 세로 스크롤 +pdfjs-scroll-horizontal-button = + .title = 가로 스크롤 사용 +pdfjs-scroll-horizontal-button-label = 가로 스크롤 +pdfjs-scroll-wrapped-button = + .title = 래핑(자동 줄 바꿈) 스크롤 사용 +pdfjs-scroll-wrapped-button-label = 래핑 스크롤 +pdfjs-spread-none-button = + .title = 한 페이지 보기 +pdfjs-spread-none-button-label = 펼침 없음 +pdfjs-spread-odd-button = + .title = 홀수 페이지로 시작하는 두 페이지 보기 +pdfjs-spread-odd-button-label = 홀수 펼침 +pdfjs-spread-even-button = + .title = 짝수 페이지로 시작하는 두 페이지 보기 +pdfjs-spread-even-button-label = 짝수 펼침 + +## Document properties dialog + +pdfjs-document-properties-button = + .title = 문서 속성… +pdfjs-document-properties-button-label = 문서 속성… +pdfjs-document-properties-file-name = 파일 이름: +pdfjs-document-properties-file-size = 파일 크기: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } 바이트) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } 바이트) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b }바이트) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b }바이트) +pdfjs-document-properties-title = 제목: +pdfjs-document-properties-author = 작성자: +pdfjs-document-properties-subject = 주제: +pdfjs-document-properties-keywords = 키워드: +pdfjs-document-properties-creation-date = 작성 날짜: +pdfjs-document-properties-modification-date = 수정 날짜: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = 작성 프로그램: +pdfjs-document-properties-producer = PDF 변환 소프트웨어: +pdfjs-document-properties-version = PDF 버전: +pdfjs-document-properties-page-count = 페이지 수: +pdfjs-document-properties-page-size = 페이지 크기: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = 세로 방향 +pdfjs-document-properties-page-size-orientation-landscape = 가로 방향 +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = 레터 +pdfjs-document-properties-page-size-name-legal = 리걸 + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = 빠른 웹 보기: +pdfjs-document-properties-linearized-yes = 예 +pdfjs-document-properties-linearized-no = 아니요 +pdfjs-document-properties-close-button = 닫기 + +## Print + +pdfjs-print-progress-message = 인쇄 문서 준비 중… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = 취소 +pdfjs-printing-not-supported = 경고: 이 브라우저는 인쇄를 완전히 지원하지 않습니다. +pdfjs-printing-not-ready = 경고: 이 PDF를 인쇄를 할 수 있을 정도로 읽어들이지 못했습니다. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = 사이드바 표시/숨기기 +pdfjs-toggle-sidebar-notification-button = + .title = 사이드바 표시/숨기기 (문서에 아웃라인/첨부파일/레이어 포함됨) +pdfjs-toggle-sidebar-button-label = 사이드바 표시/숨기기 +pdfjs-document-outline-button = + .title = 문서 아웃라인 보기 (더블 클릭해서 모든 항목 펼치기/접기) +pdfjs-document-outline-button-label = 문서 아웃라인 +pdfjs-attachments-button = + .title = 첨부파일 보기 +pdfjs-attachments-button-label = 첨부파일 +pdfjs-layers-button = + .title = 레이어 보기 (더블 클릭해서 모든 레이어를 기본 상태로 재설정) +pdfjs-layers-button-label = 레이어 +pdfjs-thumbs-button = + .title = 미리보기 +pdfjs-thumbs-button-label = 미리보기 +pdfjs-current-outline-item-button = + .title = 현재 아웃라인 항목 찾기 +pdfjs-current-outline-item-button-label = 현재 아웃라인 항목 +pdfjs-findbar-button = + .title = 검색 +pdfjs-findbar-button-label = 검색 +pdfjs-additional-layers = 추가 레이어 + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } 페이지 +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } 페이지 미리보기 + +## Find panel button title and messages + +pdfjs-find-input = + .title = 찾기 + .placeholder = 문서에서 찾기… +pdfjs-find-previous-button = + .title = 지정 문자열에 일치하는 1개 부분을 검색 +pdfjs-find-previous-button-label = 이전 +pdfjs-find-next-button = + .title = 지정 문자열에 일치하는 다음 부분을 검색 +pdfjs-find-next-button-label = 다음 +pdfjs-find-highlight-checkbox = 모두 강조 표시 +pdfjs-find-match-case-checkbox-label = 대/소문자 구분 +pdfjs-find-match-diacritics-checkbox-label = 분음 부호 일치 +pdfjs-find-entire-word-checkbox-label = 단어 단위로 +pdfjs-find-reached-top = 문서 처음까지 검색하고 끝으로 돌아와 검색했습니다. +pdfjs-find-reached-bottom = 문서 끝까지 검색하고 앞으로 돌아와 검색했습니다. +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = { $current } / { $total } 일치 +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = { $limit }개 이상 일치 +pdfjs-find-not-found = 검색 결과 없음 + +## Predefined zoom values + +pdfjs-page-scale-width = 페이지 너비에 맞추기 +pdfjs-page-scale-fit = 페이지에 맞추기 +pdfjs-page-scale-auto = 자동 +pdfjs-page-scale-actual = 실제 크기 +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page } 페이지 + +## Loading indicator messages + +pdfjs-loading-error = PDF를 로드하는 동안 오류가 발생했습니다. +pdfjs-invalid-file-error = 잘못되었거나 손상된 PDF 파일. +pdfjs-missing-file-error = PDF 파일 없음. +pdfjs-unexpected-response-error = 예기치 않은 서버 응답입니다. +pdfjs-rendering-error = 페이지를 렌더링하는 동안 오류가 발생했습니다. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } 주석] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = 이 PDF 파일을 열 수 있는 비밀번호를 입력하세요. +pdfjs-password-invalid = 잘못된 비밀번호입니다. 다시 시도하세요. +pdfjs-password-ok-button = 확인 +pdfjs-password-cancel-button = 취소 +pdfjs-web-fonts-disabled = 웹 폰트가 비활성화됨: 내장된 PDF 글꼴을 사용할 수 없습니다. + +## Editing + +pdfjs-editor-free-text-button = + .title = 텍스트 +pdfjs-editor-free-text-button-label = 텍스트 +pdfjs-editor-ink-button = + .title = 그리기 +pdfjs-editor-ink-button-label = 그리기 +pdfjs-editor-stamp-button = + .title = 이미지 추가 또는 편집 +pdfjs-editor-stamp-button-label = 이미지 추가 또는 편집 +pdfjs-editor-highlight-button = + .title = 강조 표시 +pdfjs-editor-highlight-button-label = 강조 표시 +pdfjs-highlight-floating-button1 = + .title = 강조 표시 + .aria-label = 강조 표시 +pdfjs-highlight-floating-button-label = 강조 표시 + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = 그리기 제거 +pdfjs-editor-remove-freetext-button = + .title = 텍스트 제거 +pdfjs-editor-remove-stamp-button = + .title = 이미지 제거 +pdfjs-editor-remove-highlight-button = + .title = 강조 표시 제거 + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = 색상 +pdfjs-editor-free-text-size-input = 크기 +pdfjs-editor-ink-color-input = 색상 +pdfjs-editor-ink-thickness-input = 두께 +pdfjs-editor-ink-opacity-input = 불투명도 +pdfjs-editor-stamp-add-image-button = + .title = 이미지 추가 +pdfjs-editor-stamp-add-image-button-label = 이미지 추가 +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = 두께 +pdfjs-editor-free-highlight-thickness-title = + .title = 텍스트 이외의 항목을 강조 표시할 때 두께 변경 +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = 텍스트 편집기 + .default-content = 입력을 시작하세요… +pdfjs-free-text = + .aria-label = 텍스트 편집기 +pdfjs-free-text-default-content = 입력하세요… +pdfjs-ink = + .aria-label = 그리기 편집기 +pdfjs-ink-canvas = + .aria-label = 사용자 생성 이미지 + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = 대체 텍스트 +pdfjs-editor-alt-text-edit-button = + .aria-label = 대체 텍스트 편집 +pdfjs-editor-alt-text-edit-button-label = 대체 텍스트 편집 +pdfjs-editor-alt-text-dialog-label = 옵션을 선택하세요 +pdfjs-editor-alt-text-dialog-description = 대체 텍스트는 사람들이 이미지를 볼 수 없거나 이미지가 로드되지 않을 때 도움이 됩니다. +pdfjs-editor-alt-text-add-description-label = 설명 추가 +pdfjs-editor-alt-text-add-description-description = 주제, 설정, 동작을 설명하는 1~2개의 문장을 목표로 하세요. +pdfjs-editor-alt-text-mark-decorative-label = 장식용으로 표시 +pdfjs-editor-alt-text-mark-decorative-description = 테두리나 워터마크와 같은 장식적인 이미지에 사용됩니다. +pdfjs-editor-alt-text-cancel-button = 취소 +pdfjs-editor-alt-text-save-button = 저장 +pdfjs-editor-alt-text-decorative-tooltip = 장식용으로 표시됨 +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = 예를 들어, “한 청년이 식탁에 앉아 식사를 하고 있습니다.” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = 대체 텍스트 + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = 왼쪽 위 — 크기 조정 +pdfjs-editor-resizer-label-top-middle = 가운데 위 - 크기 조정 +pdfjs-editor-resizer-label-top-right = 오른쪽 위 — 크기 조정 +pdfjs-editor-resizer-label-middle-right = 오른쪽 가운데 — 크기 조정 +pdfjs-editor-resizer-label-bottom-right = 오른쪽 아래 - 크기 조정 +pdfjs-editor-resizer-label-bottom-middle = 가운데 아래 — 크기 조정 +pdfjs-editor-resizer-label-bottom-left = 왼쪽 아래 - 크기 조정 +pdfjs-editor-resizer-label-middle-left = 왼쪽 가운데 — 크기 조정 +pdfjs-editor-resizer-top-left = + .aria-label = 왼쪽 위 — 크기 조정 +pdfjs-editor-resizer-top-middle = + .aria-label = 가운데 위 - 크기 조정 +pdfjs-editor-resizer-top-right = + .aria-label = 오른쪽 위 — 크기 조정 +pdfjs-editor-resizer-middle-right = + .aria-label = 오른쪽 가운데 — 크기 조정 +pdfjs-editor-resizer-bottom-right = + .aria-label = 오른쪽 아래 - 크기 조정 +pdfjs-editor-resizer-bottom-middle = + .aria-label = 가운데 아래 — 크기 조정 +pdfjs-editor-resizer-bottom-left = + .aria-label = 왼쪽 아래 - 크기 조정 +pdfjs-editor-resizer-middle-left = + .aria-label = 왼쪽 가운데 — 크기 조정 + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = 색상 +pdfjs-editor-colorpicker-button = + .title = 색상 변경 +pdfjs-editor-colorpicker-dropdown = + .aria-label = 색상 선택 +pdfjs-editor-colorpicker-yellow = + .title = 노란색 +pdfjs-editor-colorpicker-green = + .title = 녹색 +pdfjs-editor-colorpicker-blue = + .title = 파란색 +pdfjs-editor-colorpicker-pink = + .title = 분홍색 +pdfjs-editor-colorpicker-red = + .title = 빨간색 + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = 모두 보기 +pdfjs-editor-highlight-show-all-button = + .title = 모두 보기 + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = 대체 텍스트 (이미지 설명) 편집 +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = 대체 텍스트 (이미지 설명) 추가 +pdfjs-editor-new-alt-text-textarea = + .placeholder = 여기에 설명을 작성하세요… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = 이미지가 보이지 않거나 이미지가 로딩되지 않는 경우를 위한 간단한 설명입니다. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = 이 대체 텍스트는 자동으로 생성되었으므로 정확하지 않을 수 있습니다. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = 더 알아보기 +pdfjs-editor-new-alt-text-create-automatically-button-label = 자동으로 대체 텍스트 생성 +pdfjs-editor-new-alt-text-not-now-button = 나중에 +pdfjs-editor-new-alt-text-error-title = 대체 텍스트를 자동으로 생성할 수 없습니다. +pdfjs-editor-new-alt-text-error-description = 대체 텍스트를 직접 작성하거나 나중에 다시 시도하세요. +pdfjs-editor-new-alt-text-error-close-button = 닫기 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = 대체 텍스트 AI 모델 다운로드 중 ({ $downloadedSize } / { $totalSize } MB) + .aria-valuetext = 대체 텍스트 AI 모델 다운로드 중 ({ $downloadedSize } / { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = 대체 텍스트 추가됨 +pdfjs-editor-new-alt-text-added-button-label = 대체 텍스트 추가됨 +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = 대체 텍스트 누락 +pdfjs-editor-new-alt-text-missing-button-label = 대체 텍스트 누락 +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = 대체 텍스트 검토 +pdfjs-editor-new-alt-text-to-review-button-label = 대체 텍스트 검토 +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = 자동으로 생성됨: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = 이미지 대체 텍스트 설정 +pdfjs-image-alt-text-settings-button-label = 이미지 대체 텍스트 설정 +pdfjs-editor-alt-text-settings-dialog-label = 이미지 대체 텍스트 설정 +pdfjs-editor-alt-text-settings-automatic-title = 자동 대체 텍스트 +pdfjs-editor-alt-text-settings-create-model-button-label = 자동으로 대체 텍스트 생성 +pdfjs-editor-alt-text-settings-create-model-description = 이미지가 보이지 않거나 이미지가 로딩되지 않을 때 도움이 되는 설명을 제안합니다. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = 대체 텍스트 AI 모델 ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = 사용자의 장치에서 로컬로 실행되므로 데이터가 비공개로 유지됩니다. 자동 대체 텍스트에 필요합니다. +pdfjs-editor-alt-text-settings-delete-model-button = 삭제 +pdfjs-editor-alt-text-settings-download-model-button = 다운로드 +pdfjs-editor-alt-text-settings-downloading-model-button = 다운로드 중… +pdfjs-editor-alt-text-settings-editor-title = 대체 텍스트 편집기 +pdfjs-editor-alt-text-settings-show-dialog-button-label = 이미지 추가 시 바로 대체 텍스트 편집기 표시 +pdfjs-editor-alt-text-settings-show-dialog-description = 모든 이미지에 대체 텍스트가 있는지 확인하는 데 도움이 됩니다. +pdfjs-editor-alt-text-settings-close-button = 닫기 + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = 강조 표시 제거됨 +pdfjs-editor-undo-bar-message-freetext = 텍스트 제거됨 +pdfjs-editor-undo-bar-message-ink = 그리기 제거됨 +pdfjs-editor-undo-bar-message-stamp = 이미지 제거됨 +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = 주석 { $count }개 제거됨 +pdfjs-editor-undo-bar-undo-button = + .title = 실행 취소 +pdfjs-editor-undo-bar-undo-button-label = 실행 취소 +pdfjs-editor-undo-bar-close-button = + .title = 닫기 +pdfjs-editor-undo-bar-close-button-label = 닫기 diff --git a/public/assets/pdfjs/locale/lij/viewer.ftl b/public/assets/pdfjs/locale/lij/viewer.ftl new file mode 100644 index 0000000..b2941f9 --- /dev/null +++ b/public/assets/pdfjs/locale/lij/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina primma +pdfjs-previous-button-label = Precedente +pdfjs-next-button = + .title = Pagina dòppo +pdfjs-next-button-label = Pròscima +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Diminoisci zoom +pdfjs-zoom-out-button-label = Diminoisci zoom +pdfjs-zoom-in-button = + .title = Aomenta zoom +pdfjs-zoom-in-button-label = Aomenta zoom +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Vanni into mòddo de prezentaçion +pdfjs-presentation-mode-button-label = Mòddo de prezentaçion +pdfjs-open-file-button = + .title = Arvi file +pdfjs-open-file-button-label = Arvi +pdfjs-print-button = + .title = Stanpa +pdfjs-print-button-label = Stanpa + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Atressi +pdfjs-tools-button-label = Atressi +pdfjs-first-page-button = + .title = Vanni a-a primma pagina +pdfjs-first-page-button-label = Vanni a-a primma pagina +pdfjs-last-page-button = + .title = Vanni a l'urtima pagina +pdfjs-last-page-button-label = Vanni a l'urtima pagina +pdfjs-page-rotate-cw-button = + .title = Gia into verso oraio +pdfjs-page-rotate-cw-button-label = Gia into verso oraio +pdfjs-page-rotate-ccw-button = + .title = Gia into verso antioraio +pdfjs-page-rotate-ccw-button-label = Gia into verso antioraio +pdfjs-cursor-text-select-tool-button = + .title = Abilita strumento de seleçion do testo +pdfjs-cursor-text-select-tool-button-label = Strumento de seleçion do testo +pdfjs-cursor-hand-tool-button = + .title = Abilita strumento man +pdfjs-cursor-hand-tool-button-label = Strumento man +pdfjs-scroll-vertical-button = + .title = Deuvia rebelamento verticale +pdfjs-scroll-vertical-button-label = Rebelamento verticale +pdfjs-scroll-horizontal-button = + .title = Deuvia rebelamento orizontâ +pdfjs-scroll-horizontal-button-label = Rebelamento orizontâ +pdfjs-scroll-wrapped-button = + .title = Deuvia rebelamento incapsolou +pdfjs-scroll-wrapped-button-label = Rebelamento incapsolou +pdfjs-spread-none-button = + .title = No unite a-a difuxon de pagina +pdfjs-spread-none-button-label = No difuxon +pdfjs-spread-odd-button = + .title = Uniscite a-a difuxon de pagina co-o numero dèspa +pdfjs-spread-odd-button-label = Difuxon dèspa +pdfjs-spread-even-button = + .title = Uniscite a-a difuxon de pagina co-o numero pari +pdfjs-spread-even-button-label = Difuxon pari + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propietæ do documento… +pdfjs-document-properties-button-label = Propietæ do documento… +pdfjs-document-properties-file-name = Nomme schedaio: +pdfjs-document-properties-file-size = Dimenscion schedaio: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Titolo: +pdfjs-document-properties-author = Aoto: +pdfjs-document-properties-subject = Ogetto: +pdfjs-document-properties-keywords = Paròlle ciave: +pdfjs-document-properties-creation-date = Dæta creaçion: +pdfjs-document-properties-modification-date = Dæta cangiamento: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Aotô originale: +pdfjs-document-properties-producer = Produtô PDF: +pdfjs-document-properties-version = Verscion PDF: +pdfjs-document-properties-page-count = Contezzo pagine: +pdfjs-document-properties-page-size = Dimenscion da pagina: +pdfjs-document-properties-page-size-unit-inches = dii gròsci +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = drito +pdfjs-document-properties-page-size-orientation-landscape = desteizo +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letia +pdfjs-document-properties-page-size-name-legal = Lezze + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista veloce do Web: +pdfjs-document-properties-linearized-yes = Sci +pdfjs-document-properties-linearized-no = No +pdfjs-document-properties-close-button = Særa + +## Print + +pdfjs-print-progress-message = Praparo o documento pe-a stanpa… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Anulla +pdfjs-printing-not-supported = Atençion: a stanpa a no l'é conpletamente soportâ da sto navegatô. +pdfjs-printing-not-ready = Atençion: o PDF o no l'é ancon caregou conpletamente pe-a stanpa. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Ativa/dizativa bara de scianco +pdfjs-toggle-sidebar-button-label = Ativa/dizativa bara de scianco +pdfjs-document-outline-button = + .title = Fanni vedde o contorno do documento (scicca doggio pe espande/ridue tutti i elementi) +pdfjs-document-outline-button-label = Contorno do documento +pdfjs-attachments-button = + .title = Fanni vedde alegæ +pdfjs-attachments-button-label = Alegæ +pdfjs-thumbs-button = + .title = Mostra miniatue +pdfjs-thumbs-button-label = Miniatue +pdfjs-findbar-button = + .title = Treuva into documento +pdfjs-findbar-button-label = Treuva + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatua da pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Treuva + .placeholder = Treuva into documento… +pdfjs-find-previous-button = + .title = Treuva a ripetiçion precedente do testo da çercâ +pdfjs-find-previous-button-label = Precedente +pdfjs-find-next-button = + .title = Treuva a ripetiçion dòppo do testo da çercâ +pdfjs-find-next-button-label = Segoente +pdfjs-find-highlight-checkbox = Evidençia +pdfjs-find-match-case-checkbox-label = Maioscole/minoscole +pdfjs-find-entire-word-checkbox-label = Poula intrega +pdfjs-find-reached-top = Razonto a fin da pagina, continoa da l'iniçio +pdfjs-find-reached-bottom = Razonto l'iniçio da pagina, continoa da-a fin +pdfjs-find-not-found = Testo no trovou + +## Predefined zoom values + +pdfjs-page-scale-width = Larghessa pagina +pdfjs-page-scale-fit = Adatta a una pagina +pdfjs-page-scale-auto = Zoom aotomatico +pdfjs-page-scale-actual = Dimenscioin efetive +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = S'é verificou 'n'erô itno caregamento do PDF. +pdfjs-invalid-file-error = O schedaio PDF o l'é no valido ò aroinou. +pdfjs-missing-file-error = O schedaio PDF o no gh'é. +pdfjs-unexpected-response-error = Risposta inprevista do-u server +pdfjs-rendering-error = Gh'é stæto 'n'erô itno rendering da pagina. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotaçion: { $type }] + +## Password + +pdfjs-password-label = Dimme a paròlla segreta pe arvî sto schedaio PDF. +pdfjs-password-invalid = Paròlla segreta sbalia. Preuva torna. +pdfjs-password-ok-button = Va ben +pdfjs-password-cancel-button = Anulla +pdfjs-web-fonts-disabled = I font do web en dizativæ: inposcibile adeuviâ i carateri do PDF. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/lo/viewer.ftl b/public/assets/pdfjs/locale/lo/viewer.ftl new file mode 100644 index 0000000..557e201 --- /dev/null +++ b/public/assets/pdfjs/locale/lo/viewer.ftl @@ -0,0 +1,313 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = ຫນ້າກ່ອນຫນ້າ +pdfjs-previous-button-label = ກ່ອນຫນ້າ +pdfjs-next-button = + .title = ຫນ້າຖັດໄປ +pdfjs-next-button-label = ຖັດໄປ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ຫນ້າ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = ຈາກ { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } ຈາກ { $pagesCount }) +pdfjs-zoom-out-button = + .title = ຂະຫຍາຍອອກ +pdfjs-zoom-out-button-label = ຂະຫຍາຍອອກ +pdfjs-zoom-in-button = + .title = ຂະຫຍາຍເຂົ້າ +pdfjs-zoom-in-button-label = ຂະຫຍາຍເຂົ້າ +pdfjs-zoom-select = + .title = ຂະຫຍາຍ +pdfjs-presentation-mode-button = + .title = ສັບປ່ຽນເປັນໂຫມດການນຳສະເຫນີ +pdfjs-presentation-mode-button-label = ໂຫມດການນຳສະເຫນີ +pdfjs-open-file-button = + .title = ເປີດໄຟລ໌ +pdfjs-open-file-button-label = ເປີດ +pdfjs-print-button = + .title = ພິມ +pdfjs-print-button-label = ພິມ +pdfjs-save-button = + .title = ບັນທຶກ +pdfjs-save-button-label = ບັນທຶກ +pdfjs-bookmark-button = + .title = ໜ້າປັດຈຸບັນ (ເບິ່ງ URL ຈາກໜ້າປັດຈຸບັນ) +pdfjs-bookmark-button-label = ຫນ້າ​ປັດ​ຈຸ​ບັນ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ເຄື່ອງມື +pdfjs-tools-button-label = ເຄື່ອງມື +pdfjs-first-page-button = + .title = ໄປທີ່ຫນ້າທຳອິດ +pdfjs-first-page-button-label = ໄປທີ່ຫນ້າທຳອິດ +pdfjs-last-page-button = + .title = ໄປທີ່ຫນ້າສຸດທ້າຍ +pdfjs-last-page-button-label = ໄປທີ່ຫນ້າສຸດທ້າຍ +pdfjs-page-rotate-cw-button = + .title = ຫມູນຕາມເຂັມໂມງ +pdfjs-page-rotate-cw-button-label = ຫມູນຕາມເຂັມໂມງ +pdfjs-page-rotate-ccw-button = + .title = ຫມູນທວນເຂັມໂມງ +pdfjs-page-rotate-ccw-button-label = ຫມູນທວນເຂັມໂມງ +pdfjs-cursor-text-select-tool-button = + .title = ເປີດໃຊ້ເຄື່ອງມືການເລືອກຂໍ້ຄວາມ +pdfjs-cursor-text-select-tool-button-label = ເຄື່ອງມືເລືອກຂໍ້ຄວາມ +pdfjs-cursor-hand-tool-button = + .title = ເປີດໃຊ້ເຄື່ອງມືມື +pdfjs-cursor-hand-tool-button-label = ເຄື່ອງມືມື +pdfjs-scroll-page-button = + .title = ໃຊ້ການເລື່ອນໜ້າ +pdfjs-scroll-page-button-label = ເລື່ອນໜ້າ +pdfjs-scroll-vertical-button = + .title = ໃຊ້ການເລື່ອນແນວຕັ້ງ +pdfjs-scroll-vertical-button-label = ເລື່ອນແນວຕັ້ງ +pdfjs-scroll-horizontal-button = + .title = ໃຊ້ການເລື່ອນແນວນອນ +pdfjs-scroll-horizontal-button-label = ເລື່ອນແນວນອນ +pdfjs-scroll-wrapped-button = + .title = ໃຊ້ Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = ບໍ່ຕ້ອງຮ່ວມການແຜ່ກະຈາຍຫນ້າ +pdfjs-spread-none-button-label = ບໍ່ມີການແຜ່ກະຈາຍ +pdfjs-spread-odd-button = + .title = ເຂົ້າຮ່ວມການແຜ່ກະຈາຍຫນ້າເລີ່ມຕົ້ນດ້ວຍຫນ້າເລກຄີກ +pdfjs-spread-odd-button-label = ການແຜ່ກະຈາຍຄີກ +pdfjs-spread-even-button = + .title = ເຂົ້າຮ່ວມການແຜ່ກະຈາຍຂອງຫນ້າເລີ່ມຕົ້ນດ້ວຍຫນ້າເລກຄູ່ +pdfjs-spread-even-button-label = ການແຜ່ກະຈາຍຄູ່ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = ຄຸນສົມບັດເອກະສານ... +pdfjs-document-properties-button-label = ຄຸນສົມບັດເອກະສານ... +pdfjs-document-properties-file-name = ຊື່ໄຟລ໌: +pdfjs-document-properties-file-size = ຂະຫນາດໄຟລ໌: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } ໄບຕ໌) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } ໄບຕ໌) +pdfjs-document-properties-title = ຫົວຂໍ້: +pdfjs-document-properties-author = ຜູ້ຂຽນ: +pdfjs-document-properties-subject = ຫົວຂໍ້: +pdfjs-document-properties-keywords = ຄໍາທີ່ຕ້ອງການຄົ້ນຫາ: +pdfjs-document-properties-creation-date = ວັນທີສ້າງ: +pdfjs-document-properties-modification-date = ວັນທີແກ້ໄຂ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ຜູ້ສ້າງ: +pdfjs-document-properties-producer = ຜູ້ຜະລິດ PDF: +pdfjs-document-properties-version = ເວີຊັ່ນ PDF: +pdfjs-document-properties-page-count = ຈຳນວນໜ້າ: +pdfjs-document-properties-page-size = ຂະໜາດໜ້າ: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = ລວງຕັ້ງ +pdfjs-document-properties-page-size-orientation-landscape = ລວງນອນ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = ຈົດໝາຍ +pdfjs-document-properties-page-size-name-legal = ຂໍ້ກົດຫມາຍ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = ມຸມມອງເວັບທີ່ໄວ: +pdfjs-document-properties-linearized-yes = ແມ່ນ +pdfjs-document-properties-linearized-no = ບໍ່ +pdfjs-document-properties-close-button = ປິດ + +## Print + +pdfjs-print-progress-message = ກຳລັງກະກຽມເອກະສານສຳລັບການພິມ... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ຍົກເລີກ +pdfjs-printing-not-supported = ຄຳເຕືອນ: ບຼາວເຊີນີ້ບໍ່ຮອງຮັບການພິມຢ່າງເຕັມທີ່. +pdfjs-printing-not-ready = ຄໍາ​ເຕືອນ​: PDF ບໍ່​ໄດ້​ຖືກ​ໂຫຼດ​ຢ່າງ​ເຕັມ​ທີ່​ສໍາ​ລັບ​ການ​ພິມ​. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ເປີດ/ປິດແຖບຂ້າງ +pdfjs-toggle-sidebar-notification-button = + .title = ສະຫຼັບແຖບດ້ານຂ້າງ (ເອກະສານປະກອບມີໂຄງຮ່າງ/ໄຟລ໌ແນບ/ຊັ້ນຂໍ້ມູນ) +pdfjs-toggle-sidebar-button-label = ເປີດ/ປິດແຖບຂ້າງ +pdfjs-document-outline-button = + .title = ສະ​ແດງ​ໂຄງ​ຮ່າງ​ເອ​ກະ​ສານ (ກົດ​ສອງ​ຄັ້ງ​ເພື່ອ​ຂະ​ຫຍາຍ / ຫຍໍ້​ລາຍ​ການ​ທັງ​ຫມົດ​) +pdfjs-document-outline-button-label = ເຄົ້າຮ່າງເອກະສານ +pdfjs-attachments-button = + .title = ສະແດງໄຟລ໌ແນບ +pdfjs-attachments-button-label = ໄຟລ໌ແນບ +pdfjs-layers-button = + .title = ສະແດງຊັ້ນຂໍ້ມູນ (ຄລິກສອງເທື່ອເພື່ອຣີເຊັດຊັ້ນຂໍ້ມູນທັງໝົດໃຫ້ເປັນສະຖານະເລີ່ມຕົ້ນ) +pdfjs-layers-button-label = ຊັ້ນ +pdfjs-thumbs-button = + .title = ສະແດງຮູບຫຍໍ້ +pdfjs-thumbs-button-label = ຮູບຕົວຢ່າງ +pdfjs-current-outline-item-button = + .title = ຊອກຫາລາຍການໂຄງຮ່າງປະຈຸບັນ +pdfjs-current-outline-item-button-label = ລາຍການໂຄງຮ່າງປະຈຸບັນ +pdfjs-findbar-button = + .title = ຊອກຫາໃນເອກະສານ +pdfjs-findbar-button-label = ຄົ້ນຫາ +pdfjs-additional-layers = ຊັ້ນຂໍ້ມູນເພີ່ມເຕີມ + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = ໜ້າ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = ຮູບຕົວຢ່າງຂອງໜ້າ { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ຄົ້ນຫາ + .placeholder = ຊອກຫາໃນເອກະສານ... +pdfjs-find-previous-button = + .title = ຊອກຫາການປະກົດຕົວທີ່ຜ່ານມາຂອງປະໂຫຍກ +pdfjs-find-previous-button-label = ກ່ອນຫນ້ານີ້ +pdfjs-find-next-button = + .title = ຊອກຫາຕຳແຫນ່ງຖັດໄປຂອງວະລີ +pdfjs-find-next-button-label = ຕໍ່ໄປ +pdfjs-find-highlight-checkbox = ໄຮໄລທ໌ທັງຫມົດ +pdfjs-find-match-case-checkbox-label = ກໍລະນີທີ່ກົງກັນ +pdfjs-find-match-diacritics-checkbox-label = ເຄື່ອງໝາຍກຳກັບການອອກສຽງກົງກັນ +pdfjs-find-entire-word-checkbox-label = ກົງກັນທຸກຄຳ +pdfjs-find-reached-top = ມາຮອດເທິງຂອງເອກະສານ, ສືບຕໍ່ຈາກລຸ່ມ +pdfjs-find-reached-bottom = ຮອດຕອນທ້າຍຂອງເອກະສານ, ສືບຕໍ່ຈາກເທິງ +pdfjs-find-not-found = ບໍ່ພົບວະລີທີ່ຕ້ອງການ + +## Predefined zoom values + +pdfjs-page-scale-width = ຄວາມກວ້າງໜ້າ +pdfjs-page-scale-fit = ໜ້າພໍດີ +pdfjs-page-scale-auto = ຊູມອັດຕະໂນມັດ +pdfjs-page-scale-actual = ຂະໜາດຕົວຈິງ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = ໜ້າ { $page } + +## Loading indicator messages + +pdfjs-loading-error = ມີຂໍ້ຜິດພາດເກີດຂື້ນຂະນະທີ່ກຳລັງໂຫລດ PDF. +pdfjs-invalid-file-error = ໄຟລ໌ PDF ບໍ່ຖືກຕ້ອງຫລືເສຍຫາຍ. +pdfjs-missing-file-error = ບໍ່ມີໄຟລ໌ PDF. +pdfjs-unexpected-response-error = ການຕອບສະໜອງຂອງເຊີບເວີທີ່ບໍ່ຄາດຄິດ. +pdfjs-rendering-error = ມີຂໍ້ຜິດພາດເກີດຂື້ນຂະນະທີ່ກຳລັງເຣັນເດີຫນ້າ. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ຄຳບັນຍາຍ] + +## Password + +pdfjs-password-label = ໃສ່ລະຫັດຜ່ານເພື່ອເປີດໄຟລ໌ PDF ນີ້. +pdfjs-password-invalid = ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ. ກະລຸນາລອງອີກຄັ້ງ. +pdfjs-password-ok-button = ຕົກລົງ +pdfjs-password-cancel-button = ຍົກເລີກ +pdfjs-web-fonts-disabled = ຟອນເວັບຖືກປິດໃຊ້ງານ: ບໍ່ສາມາດໃຊ້ຟອນ PDF ທີ່ຝັງໄວ້ໄດ້. + +## Editing + +pdfjs-editor-free-text-button = + .title = ຂໍ້ຄວາມ +pdfjs-editor-free-text-button-label = ຂໍ້ຄວາມ +pdfjs-editor-ink-button = + .title = ແຕ້ມ +pdfjs-editor-ink-button-label = ແຕ້ມ + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = ສີ +pdfjs-editor-free-text-size-input = ຂະຫນາດ +pdfjs-editor-ink-color-input = ສີ +pdfjs-editor-ink-thickness-input = ຄວາມຫນາ +pdfjs-editor-ink-opacity-input = ຄວາມໂປ່ງໃສ +pdfjs-free-text = + .aria-label = ຕົວແກ້ໄຂຂໍ້ຄວາມ +pdfjs-free-text-default-content = ເລີ່ມພິມ... +pdfjs-ink = + .aria-label = ຕົວແກ້ໄຂຮູບແຕ້ມ +pdfjs-ink-canvas = + .aria-label = ຮູບພາບທີ່ຜູ້ໃຊ້ສ້າງ + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/locale.json b/public/assets/pdfjs/locale/locale.json new file mode 100644 index 0000000..2012211 --- /dev/null +++ b/public/assets/pdfjs/locale/locale.json @@ -0,0 +1 @@ +{"ach":"ach/viewer.ftl","af":"af/viewer.ftl","an":"an/viewer.ftl","ar":"ar/viewer.ftl","ast":"ast/viewer.ftl","az":"az/viewer.ftl","be":"be/viewer.ftl","bg":"bg/viewer.ftl","bn":"bn/viewer.ftl","bo":"bo/viewer.ftl","br":"br/viewer.ftl","brx":"brx/viewer.ftl","bs":"bs/viewer.ftl","ca":"ca/viewer.ftl","cak":"cak/viewer.ftl","ckb":"ckb/viewer.ftl","cs":"cs/viewer.ftl","cy":"cy/viewer.ftl","da":"da/viewer.ftl","de":"de/viewer.ftl","dsb":"dsb/viewer.ftl","el":"el/viewer.ftl","en-ca":"en-CA/viewer.ftl","en-gb":"en-GB/viewer.ftl","en-us":"en-US/viewer.ftl","eo":"eo/viewer.ftl","es-ar":"es-AR/viewer.ftl","es-cl":"es-CL/viewer.ftl","es-es":"es-ES/viewer.ftl","es-mx":"es-MX/viewer.ftl","et":"et/viewer.ftl","eu":"eu/viewer.ftl","fa":"fa/viewer.ftl","ff":"ff/viewer.ftl","fi":"fi/viewer.ftl","fr":"fr/viewer.ftl","fur":"fur/viewer.ftl","fy-nl":"fy-NL/viewer.ftl","ga-ie":"ga-IE/viewer.ftl","gd":"gd/viewer.ftl","gl":"gl/viewer.ftl","gn":"gn/viewer.ftl","gu-in":"gu-IN/viewer.ftl","he":"he/viewer.ftl","hi-in":"hi-IN/viewer.ftl","hr":"hr/viewer.ftl","hsb":"hsb/viewer.ftl","hu":"hu/viewer.ftl","hy-am":"hy-AM/viewer.ftl","hye":"hye/viewer.ftl","ia":"ia/viewer.ftl","id":"id/viewer.ftl","is":"is/viewer.ftl","it":"it/viewer.ftl","ja":"ja/viewer.ftl","ka":"ka/viewer.ftl","kab":"kab/viewer.ftl","kk":"kk/viewer.ftl","km":"km/viewer.ftl","kn":"kn/viewer.ftl","ko":"ko/viewer.ftl","lij":"lij/viewer.ftl","lo":"lo/viewer.ftl","lt":"lt/viewer.ftl","ltg":"ltg/viewer.ftl","lv":"lv/viewer.ftl","meh":"meh/viewer.ftl","mk":"mk/viewer.ftl","mr":"mr/viewer.ftl","ms":"ms/viewer.ftl","my":"my/viewer.ftl","nb-no":"nb-NO/viewer.ftl","ne-np":"ne-NP/viewer.ftl","nl":"nl/viewer.ftl","nn-no":"nn-NO/viewer.ftl","oc":"oc/viewer.ftl","pa-in":"pa-IN/viewer.ftl","pl":"pl/viewer.ftl","pt-br":"pt-BR/viewer.ftl","pt-pt":"pt-PT/viewer.ftl","rm":"rm/viewer.ftl","ro":"ro/viewer.ftl","ru":"ru/viewer.ftl","sat":"sat/viewer.ftl","sc":"sc/viewer.ftl","scn":"scn/viewer.ftl","sco":"sco/viewer.ftl","si":"si/viewer.ftl","sk":"sk/viewer.ftl","skr":"skr/viewer.ftl","sl":"sl/viewer.ftl","son":"son/viewer.ftl","sq":"sq/viewer.ftl","sr":"sr/viewer.ftl","sv-se":"sv-SE/viewer.ftl","szl":"szl/viewer.ftl","ta":"ta/viewer.ftl","te":"te/viewer.ftl","tg":"tg/viewer.ftl","th":"th/viewer.ftl","tl":"tl/viewer.ftl","tr":"tr/viewer.ftl","trs":"trs/viewer.ftl","uk":"uk/viewer.ftl","ur":"ur/viewer.ftl","uz":"uz/viewer.ftl","vi":"vi/viewer.ftl","wo":"wo/viewer.ftl","xh":"xh/viewer.ftl","zh-cn":"zh-CN/viewer.ftl","zh-tw":"zh-TW/viewer.ftl"} \ No newline at end of file diff --git a/public/assets/pdfjs/locale/lt/viewer.ftl b/public/assets/pdfjs/locale/lt/viewer.ftl new file mode 100644 index 0000000..a8ee7a0 --- /dev/null +++ b/public/assets/pdfjs/locale/lt/viewer.ftl @@ -0,0 +1,268 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Ankstesnis puslapis +pdfjs-previous-button-label = Ankstesnis +pdfjs-next-button = + .title = Kitas puslapis +pdfjs-next-button-label = Kitas +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Puslapis +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = iš { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } iš { $pagesCount }) +pdfjs-zoom-out-button = + .title = Sumažinti +pdfjs-zoom-out-button-label = Sumažinti +pdfjs-zoom-in-button = + .title = Padidinti +pdfjs-zoom-in-button-label = Padidinti +pdfjs-zoom-select = + .title = Mastelis +pdfjs-presentation-mode-button = + .title = Pereiti į pateikties veikseną +pdfjs-presentation-mode-button-label = Pateikties veiksena +pdfjs-open-file-button = + .title = Atverti failą +pdfjs-open-file-button-label = Atverti +pdfjs-print-button = + .title = Spausdinti +pdfjs-print-button-label = Spausdinti + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Priemonės +pdfjs-tools-button-label = Priemonės +pdfjs-first-page-button = + .title = Eiti į pirmą puslapį +pdfjs-first-page-button-label = Eiti į pirmą puslapį +pdfjs-last-page-button = + .title = Eiti į paskutinį puslapį +pdfjs-last-page-button-label = Eiti į paskutinį puslapį +pdfjs-page-rotate-cw-button = + .title = Pasukti pagal laikrodžio rodyklę +pdfjs-page-rotate-cw-button-label = Pasukti pagal laikrodžio rodyklę +pdfjs-page-rotate-ccw-button = + .title = Pasukti prieš laikrodžio rodyklę +pdfjs-page-rotate-ccw-button-label = Pasukti prieš laikrodžio rodyklę +pdfjs-cursor-text-select-tool-button = + .title = Įjungti teksto žymėjimo įrankį +pdfjs-cursor-text-select-tool-button-label = Teksto žymėjimo įrankis +pdfjs-cursor-hand-tool-button = + .title = Įjungti vilkimo įrankį +pdfjs-cursor-hand-tool-button-label = Vilkimo įrankis +pdfjs-scroll-page-button = + .title = Naudoti puslapio slinkimą +pdfjs-scroll-page-button-label = Puslapio slinkimas +pdfjs-scroll-vertical-button = + .title = Naudoti vertikalų slinkimą +pdfjs-scroll-vertical-button-label = Vertikalus slinkimas +pdfjs-scroll-horizontal-button = + .title = Naudoti horizontalų slinkimą +pdfjs-scroll-horizontal-button-label = Horizontalus slinkimas +pdfjs-scroll-wrapped-button = + .title = Naudoti išklotą slinkimą +pdfjs-scroll-wrapped-button-label = Išklotas slinkimas +pdfjs-spread-none-button = + .title = Nejungti puslapių į dvilapius +pdfjs-spread-none-button-label = Be dvilapių +pdfjs-spread-odd-button = + .title = Sujungti į dvilapius pradedant nelyginiais puslapiais +pdfjs-spread-odd-button-label = Nelyginiai dvilapiai +pdfjs-spread-even-button = + .title = Sujungti į dvilapius pradedant lyginiais puslapiais +pdfjs-spread-even-button-label = Lyginiai dvilapiai + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumento savybės… +pdfjs-document-properties-button-label = Dokumento savybės… +pdfjs-document-properties-file-name = Failo vardas: +pdfjs-document-properties-file-size = Failo dydis: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } B) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } B) +pdfjs-document-properties-title = Antraštė: +pdfjs-document-properties-author = Autorius: +pdfjs-document-properties-subject = Tema: +pdfjs-document-properties-keywords = Reikšminiai žodžiai: +pdfjs-document-properties-creation-date = Sukūrimo data: +pdfjs-document-properties-modification-date = Modifikavimo data: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Kūrėjas: +pdfjs-document-properties-producer = PDF generatorius: +pdfjs-document-properties-version = PDF versija: +pdfjs-document-properties-page-count = Puslapių skaičius: +pdfjs-document-properties-page-size = Puslapio dydis: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = stačias +pdfjs-document-properties-page-size-orientation-landscape = gulsčias +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Laiškas +pdfjs-document-properties-page-size-name-legal = Dokumentas + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Spartus žiniatinklio rodinys: +pdfjs-document-properties-linearized-yes = Taip +pdfjs-document-properties-linearized-no = Ne +pdfjs-document-properties-close-button = Užverti + +## Print + +pdfjs-print-progress-message = Dokumentas ruošiamas spausdinimui… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Atsisakyti +pdfjs-printing-not-supported = Dėmesio! Spausdinimas šioje naršyklėje nėra pilnai realizuotas. +pdfjs-printing-not-ready = Dėmesio! PDF failas dar nėra pilnai įkeltas spausdinimui. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Rodyti / slėpti šoninį polangį +pdfjs-toggle-sidebar-notification-button = + .title = Parankinė (dokumentas turi struktūrą / priedų / sluoksnių) +pdfjs-toggle-sidebar-button-label = Šoninis polangis +pdfjs-document-outline-button = + .title = Rodyti dokumento struktūrą (spustelėkite dukart norėdami išplėsti/suskleisti visus elementus) +pdfjs-document-outline-button-label = Dokumento struktūra +pdfjs-attachments-button = + .title = Rodyti priedus +pdfjs-attachments-button-label = Priedai +pdfjs-layers-button = + .title = Rodyti sluoksnius (spustelėkite dukart, norėdami atstatyti visus sluoksnius į numatytąją būseną) +pdfjs-layers-button-label = Sluoksniai +pdfjs-thumbs-button = + .title = Rodyti puslapių miniatiūras +pdfjs-thumbs-button-label = Miniatiūros +pdfjs-current-outline-item-button = + .title = Rasti dabartinį struktūros elementą +pdfjs-current-outline-item-button-label = Dabartinis struktūros elementas +pdfjs-findbar-button = + .title = Ieškoti dokumente +pdfjs-findbar-button-label = Rasti +pdfjs-additional-layers = Papildomi sluoksniai + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } puslapis +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } puslapio miniatiūra + +## Find panel button title and messages + +pdfjs-find-input = + .title = Rasti + .placeholder = Rasti dokumente… +pdfjs-find-previous-button = + .title = Ieškoti ankstesnio frazės egzemplioriaus +pdfjs-find-previous-button-label = Ankstesnis +pdfjs-find-next-button = + .title = Ieškoti tolesnio frazės egzemplioriaus +pdfjs-find-next-button-label = Tolesnis +pdfjs-find-highlight-checkbox = Viską paryškinti +pdfjs-find-match-case-checkbox-label = Skirti didžiąsias ir mažąsias raides +pdfjs-find-match-diacritics-checkbox-label = Skirti diakritinius ženklus +pdfjs-find-entire-word-checkbox-label = Ištisi žodžiai +pdfjs-find-reached-top = Pasiekus dokumento pradžią, paieška pratęsta nuo pabaigos +pdfjs-find-reached-bottom = Pasiekus dokumento pabaigą, paieška pratęsta nuo pradžios +pdfjs-find-not-found = Ieškoma frazė nerasta + +## Predefined zoom values + +pdfjs-page-scale-width = Priderinti prie lapo pločio +pdfjs-page-scale-fit = Pritaikyti prie lapo dydžio +pdfjs-page-scale-auto = Automatinis mastelis +pdfjs-page-scale-actual = Tikras dydis +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page } puslapis + +## Loading indicator messages + +pdfjs-loading-error = Įkeliant PDF failą įvyko klaida. +pdfjs-invalid-file-error = Tai nėra PDF failas arba jis yra sugadintas. +pdfjs-missing-file-error = PDF failas nerastas. +pdfjs-unexpected-response-error = Netikėtas serverio atsakas. +pdfjs-rendering-error = Atvaizduojant puslapį įvyko klaida. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [„{ $type }“ tipo anotacija] + +## Password + +pdfjs-password-label = Įveskite slaptažodį šiam PDF failui atverti. +pdfjs-password-invalid = Slaptažodis neteisingas. Bandykite dar kartą. +pdfjs-password-ok-button = Gerai +pdfjs-password-cancel-button = Atsisakyti +pdfjs-web-fonts-disabled = Saityno šriftai išjungti – PDF faile esančių šriftų naudoti negalima. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ltg/viewer.ftl b/public/assets/pdfjs/locale/ltg/viewer.ftl new file mode 100644 index 0000000..d262165 --- /dev/null +++ b/public/assets/pdfjs/locale/ltg/viewer.ftl @@ -0,0 +1,246 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Īprīkšejā lopa +pdfjs-previous-button-label = Īprīkšejā +pdfjs-next-button = + .title = Nuokomuo lopa +pdfjs-next-button-label = Nuokomuo +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Lopa +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = nu { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } nu { $pagesCount }) +pdfjs-zoom-out-button = + .title = Attuolynuot +pdfjs-zoom-out-button-label = Attuolynuot +pdfjs-zoom-in-button = + .title = Pītuvynuot +pdfjs-zoom-in-button-label = Pītuvynuot +pdfjs-zoom-select = + .title = Palelynuojums +pdfjs-presentation-mode-button = + .title = Puorslēgtīs iz Prezentacejis režymu +pdfjs-presentation-mode-button-label = Prezentacejis režyms +pdfjs-open-file-button = + .title = Attaiseit failu +pdfjs-open-file-button-label = Attaiseit +pdfjs-print-button = + .title = Drukuošona +pdfjs-print-button-label = Drukōt + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Reiki +pdfjs-tools-button-label = Reiki +pdfjs-first-page-button = + .title = Īt iz pyrmū lopu +pdfjs-first-page-button-label = Īt iz pyrmū lopu +pdfjs-last-page-button = + .title = Īt iz piedejū lopu +pdfjs-last-page-button-label = Īt iz piedejū lopu +pdfjs-page-rotate-cw-button = + .title = Pagrīzt pa pulksteni +pdfjs-page-rotate-cw-button-label = Pagrīzt pa pulksteni +pdfjs-page-rotate-ccw-button = + .title = Pagrīzt pret pulksteni +pdfjs-page-rotate-ccw-button-label = Pagrīzt pret pulksteni +pdfjs-cursor-text-select-tool-button = + .title = Aktivizēt teksta izvieles reiku +pdfjs-cursor-text-select-tool-button-label = Teksta izvieles reiks +pdfjs-cursor-hand-tool-button = + .title = Aktivēt rūkys reiku +pdfjs-cursor-hand-tool-button-label = Rūkys reiks +pdfjs-scroll-vertical-button = + .title = Izmontōt vertikalū ritinōšonu +pdfjs-scroll-vertical-button-label = Vertikalō ritinōšona +pdfjs-scroll-horizontal-button = + .title = Izmontōt horizontalū ritinōšonu +pdfjs-scroll-horizontal-button-label = Horizontalō ritinōšona +pdfjs-scroll-wrapped-button = + .title = Izmontōt mārūgojamū ritinōšonu +pdfjs-scroll-wrapped-button-label = Mārūgojamō ritinōšona +pdfjs-spread-none-button = + .title = Naizmontōt lopu atvāruma režimu +pdfjs-spread-none-button-label = Bez atvārumim +pdfjs-spread-odd-button = + .title = Izmontōt lopu atvārumus sōkut nu napōra numeru lopom +pdfjs-spread-odd-button-label = Napōra lopys pa kreisi +pdfjs-spread-even-button = + .title = Izmontōt lopu atvārumus sōkut nu pōra numeru lopom +pdfjs-spread-even-button-label = Pōra lopys pa kreisi + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumenta īstatiejumi… +pdfjs-document-properties-button-label = Dokumenta īstatiejumi… +pdfjs-document-properties-file-name = Faila nūsaukums: +pdfjs-document-properties-file-size = Faila izmārs: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } biti) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } biti) +pdfjs-document-properties-title = Nūsaukums: +pdfjs-document-properties-author = Autors: +pdfjs-document-properties-subject = Tema: +pdfjs-document-properties-keywords = Atslāgi vuordi: +pdfjs-document-properties-creation-date = Izveides datums: +pdfjs-document-properties-modification-date = lobuošonys datums: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Radeituojs: +pdfjs-document-properties-producer = PDF producents: +pdfjs-document-properties-version = PDF verseja: +pdfjs-document-properties-page-count = Lopu skaits: +pdfjs-document-properties-page-size = Lopas izmārs: +pdfjs-document-properties-page-size-unit-inches = collas +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portreta orientaceja +pdfjs-document-properties-page-size-orientation-landscape = ainovys orientaceja +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Jā +pdfjs-document-properties-linearized-no = Nā +pdfjs-document-properties-close-button = Aiztaiseit + +## Print + +pdfjs-print-progress-message = Preparing document for printing… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Atceļt +pdfjs-printing-not-supported = Uzmaneibu: Drukuošona nu itei puorlūka dorbojās tikai daleji. +pdfjs-printing-not-ready = Uzmaneibu: PDF nav pilneibā īluodeits drukuošonai. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Puorslēgt suonu jūslu +pdfjs-toggle-sidebar-button-label = Puorslēgt suonu jūslu +pdfjs-document-outline-button = + .title = Show Document Outline (double-click to expand/collapse all items) +pdfjs-document-outline-button-label = Dokumenta saturs +pdfjs-attachments-button = + .title = Show Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-thumbs-button = + .title = Paruodeit seiktālus +pdfjs-thumbs-button-label = Seiktāli +pdfjs-findbar-button = + .title = Mekleit dokumentā +pdfjs-findbar-button-label = Mekleit + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Lopa { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Lopys { $page } seiktāls + +## Find panel button title and messages + +pdfjs-find-input = + .title = Mekleit + .placeholder = Mekleit dokumentā… +pdfjs-find-previous-button = + .title = Atrast īprīkšejū +pdfjs-find-previous-button-label = Īprīkšejā +pdfjs-find-next-button = + .title = Atrast nuokamū +pdfjs-find-next-button-label = Nuokomuo +pdfjs-find-highlight-checkbox = Īkruosuot vysys +pdfjs-find-match-case-checkbox-label = Lelū, mozū burtu jiuteigs +pdfjs-find-reached-top = Sasnīgts dokumenta suokums, turpynojom nu beigom +pdfjs-find-reached-bottom = Sasnīgtys dokumenta beigys, turpynojom nu suokuma +pdfjs-find-not-found = Frāze nav atrosta + +## Predefined zoom values + +pdfjs-page-scale-width = Lopys plotumā +pdfjs-page-scale-fit = Ītylpynūt lopu +pdfjs-page-scale-auto = Automatiskais izmārs +pdfjs-page-scale-actual = Patīsais izmārs +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Īluodejūt PDF nūtyka klaida. +pdfjs-invalid-file-error = Nadereigs voi būjuots PDF fails. +pdfjs-missing-file-error = PDF fails nav atrosts. +pdfjs-unexpected-response-error = Unexpected server response. +pdfjs-rendering-error = Attālojūt lopu rodās klaida + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = Īvodit paroli, kab attaiseitu PDF failu. +pdfjs-password-invalid = Napareiza parole, raugit vēļreiz. +pdfjs-password-ok-button = Labi +pdfjs-password-cancel-button = Atceļt +pdfjs-web-fonts-disabled = Šķārsteikla fonti nav aktivizāti: Navar īgult PDF fontus. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/lv/viewer.ftl b/public/assets/pdfjs/locale/lv/viewer.ftl new file mode 100644 index 0000000..067dc10 --- /dev/null +++ b/public/assets/pdfjs/locale/lv/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Iepriekšējā lapa +pdfjs-previous-button-label = Iepriekšējā +pdfjs-next-button = + .title = Nākamā lapa +pdfjs-next-button-label = Nākamā +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Lapa +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = no { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } no { $pagesCount }) +pdfjs-zoom-out-button = + .title = Attālināt +pdfjs-zoom-out-button-label = Attālināt +pdfjs-zoom-in-button = + .title = Pietuvināt +pdfjs-zoom-in-button-label = Pietuvināt +pdfjs-zoom-select = + .title = Palielinājums +pdfjs-presentation-mode-button = + .title = Pārslēgties uz Prezentācijas režīmu +pdfjs-presentation-mode-button-label = Prezentācijas režīms +pdfjs-open-file-button = + .title = Atvērt failu +pdfjs-open-file-button-label = Atvērt +pdfjs-print-button = + .title = Drukāšana +pdfjs-print-button-label = Drukāt + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Rīki +pdfjs-tools-button-label = Rīki +pdfjs-first-page-button = + .title = Iet uz pirmo lapu +pdfjs-first-page-button-label = Iet uz pirmo lapu +pdfjs-last-page-button = + .title = Iet uz pēdējo lapu +pdfjs-last-page-button-label = Iet uz pēdējo lapu +pdfjs-page-rotate-cw-button = + .title = Pagriezt pa pulksteni +pdfjs-page-rotate-cw-button-label = Pagriezt pa pulksteni +pdfjs-page-rotate-ccw-button = + .title = Pagriezt pret pulksteni +pdfjs-page-rotate-ccw-button-label = Pagriezt pret pulksteni +pdfjs-cursor-text-select-tool-button = + .title = Aktivizēt teksta izvēles rīku +pdfjs-cursor-text-select-tool-button-label = Teksta izvēles rīks +pdfjs-cursor-hand-tool-button = + .title = Aktivēt rokas rīku +pdfjs-cursor-hand-tool-button-label = Rokas rīks +pdfjs-scroll-vertical-button = + .title = Izmantot vertikālo ritināšanu +pdfjs-scroll-vertical-button-label = Vertikālā ritināšana +pdfjs-scroll-horizontal-button = + .title = Izmantot horizontālo ritināšanu +pdfjs-scroll-horizontal-button-label = Horizontālā ritināšana +pdfjs-scroll-wrapped-button = + .title = Izmantot apkļauto ritināšanu +pdfjs-scroll-wrapped-button-label = Apkļautā ritināšana +pdfjs-spread-none-button = + .title = Nepievienoties lapu izpletumiem +pdfjs-spread-none-button-label = Neizmantot izpletumus +pdfjs-spread-odd-button = + .title = Izmantot lapu izpletumus sākot ar nepāra numuru lapām +pdfjs-spread-odd-button-label = Nepāra izpletumi +pdfjs-spread-even-button = + .title = Izmantot lapu izpletumus sākot ar pāra numuru lapām +pdfjs-spread-even-button-label = Pāra izpletumi + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumenta iestatījumi… +pdfjs-document-properties-button-label = Dokumenta iestatījumi… +pdfjs-document-properties-file-name = Faila nosaukums: +pdfjs-document-properties-file-size = Faila izmērs: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } biti) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } biti) +pdfjs-document-properties-title = Nosaukums: +pdfjs-document-properties-author = Autors: +pdfjs-document-properties-subject = Tēma: +pdfjs-document-properties-keywords = Atslēgas vārdi: +pdfjs-document-properties-creation-date = Izveides datums: +pdfjs-document-properties-modification-date = LAbošanas datums: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Radītājs: +pdfjs-document-properties-producer = PDF producents: +pdfjs-document-properties-version = PDF versija: +pdfjs-document-properties-page-count = Lapu skaits: +pdfjs-document-properties-page-size = Papīra izmērs: +pdfjs-document-properties-page-size-unit-inches = collas +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portretorientācija +pdfjs-document-properties-page-size-orientation-landscape = ainavorientācija +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Vēstule +pdfjs-document-properties-page-size-name-legal = Juridiskie teksti + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Ātrā tīmekļa skats: +pdfjs-document-properties-linearized-yes = Jā +pdfjs-document-properties-linearized-no = Nē +pdfjs-document-properties-close-button = Aizvērt + +## Print + +pdfjs-print-progress-message = Gatavo dokumentu drukāšanai... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Atcelt +pdfjs-printing-not-supported = Uzmanību: Drukāšana no šī pārlūka darbojas tikai daļēji. +pdfjs-printing-not-ready = Uzmanību: PDF nav pilnībā ielādēts drukāšanai. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Pārslēgt sānu joslu +pdfjs-toggle-sidebar-button-label = Pārslēgt sānu joslu +pdfjs-document-outline-button = + .title = Rādīt dokumenta struktūru (veiciet dubultklikšķi lai izvērstu/sakļautu visus vienumus) +pdfjs-document-outline-button-label = Dokumenta saturs +pdfjs-attachments-button = + .title = Rādīt pielikumus +pdfjs-attachments-button-label = Pielikumi +pdfjs-thumbs-button = + .title = Parādīt sīktēlus +pdfjs-thumbs-button-label = Sīktēli +pdfjs-findbar-button = + .title = Meklēt dokumentā +pdfjs-findbar-button-label = Meklēt + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Lapa { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Lapas { $page } sīktēls + +## Find panel button title and messages + +pdfjs-find-input = + .title = Meklēt + .placeholder = Meklēt dokumentā… +pdfjs-find-previous-button = + .title = Atrast iepriekšējo +pdfjs-find-previous-button-label = Iepriekšējā +pdfjs-find-next-button = + .title = Atrast nākamo +pdfjs-find-next-button-label = Nākamā +pdfjs-find-highlight-checkbox = Iekrāsot visas +pdfjs-find-match-case-checkbox-label = Lielo, mazo burtu jutīgs +pdfjs-find-entire-word-checkbox-label = Veselus vārdus +pdfjs-find-reached-top = Sasniegts dokumenta sākums, turpinām no beigām +pdfjs-find-reached-bottom = Sasniegtas dokumenta beigas, turpinām no sākuma +pdfjs-find-not-found = Frāze nav atrasta + +## Predefined zoom values + +pdfjs-page-scale-width = Lapas platumā +pdfjs-page-scale-fit = Ietilpinot lapu +pdfjs-page-scale-auto = Automātiskais izmērs +pdfjs-page-scale-actual = Patiesais izmērs +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Ielādējot PDF notika kļūda. +pdfjs-invalid-file-error = Nederīgs vai bojāts PDF fails. +pdfjs-missing-file-error = PDF fails nav atrasts. +pdfjs-unexpected-response-error = Negaidīa servera atbilde. +pdfjs-rendering-error = Attēlojot lapu radās kļūda + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } anotācija] + +## Password + +pdfjs-password-label = Ievadiet paroli, lai atvērtu PDF failu. +pdfjs-password-invalid = Nepareiza parole, mēģiniet vēlreiz. +pdfjs-password-ok-button = Labi +pdfjs-password-cancel-button = Atcelt +pdfjs-web-fonts-disabled = Tīmekļa fonti nav aktivizēti: Nevar iegult PDF fontus. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/meh/viewer.ftl b/public/assets/pdfjs/locale/meh/viewer.ftl new file mode 100644 index 0000000..d8bddc9 --- /dev/null +++ b/public/assets/pdfjs/locale/meh/viewer.ftl @@ -0,0 +1,87 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página yata +pdfjs-zoom-select = + .title = Nasa´a ka´nu/Nasa´a luli +pdfjs-open-file-button-label = Síne + +## Secondary toolbar and context menu + + +## Document properties dialog + +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = Kuvi +pdfjs-document-properties-close-button = Nakasɨ + +## Print + +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Nkuvi-ka + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-findbar-button-label = Nánuku + +## Thumbnails panel item (tooltip and alt text for images) + + +## Find panel button title and messages + + +## Predefined zoom values + +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } + +## Password + +pdfjs-password-cancel-button = Nkuvi-ka + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/mk/viewer.ftl b/public/assets/pdfjs/locale/mk/viewer.ftl new file mode 100644 index 0000000..47d24b2 --- /dev/null +++ b/public/assets/pdfjs/locale/mk/viewer.ftl @@ -0,0 +1,215 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Претходна страница +pdfjs-previous-button-label = Претходна +pdfjs-next-button = + .title = Следна страница +pdfjs-next-button-label = Следна +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Страница +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = од { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } од { $pagesCount }) +pdfjs-zoom-out-button = + .title = Намалување +pdfjs-zoom-out-button-label = Намали +pdfjs-zoom-in-button = + .title = Зголемување +pdfjs-zoom-in-button-label = Зголеми +pdfjs-zoom-select = + .title = Променување на големина +pdfjs-presentation-mode-button = + .title = Премини во презентациски режим +pdfjs-presentation-mode-button-label = Презентациски режим +pdfjs-open-file-button = + .title = Отворање датотека +pdfjs-open-file-button-label = Отвори +pdfjs-print-button = + .title = Печатење +pdfjs-print-button-label = Печати + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Алатки +pdfjs-tools-button-label = Алатки +pdfjs-first-page-button = + .title = Оди до првата страница +pdfjs-first-page-button-label = Оди до првата страница +pdfjs-last-page-button = + .title = Оди до последната страница +pdfjs-last-page-button-label = Оди до последната страница +pdfjs-page-rotate-cw-button = + .title = Ротирај по стрелките на часовникот +pdfjs-page-rotate-cw-button-label = Ротирај по стрелките на часовникот +pdfjs-page-rotate-ccw-button = + .title = Ротирај спротивно од стрелките на часовникот +pdfjs-page-rotate-ccw-button-label = Ротирај спротивно од стрелките на часовникот +pdfjs-cursor-text-select-tool-button = + .title = Овозможи алатка за избор на текст +pdfjs-cursor-text-select-tool-button-label = Алатка за избор на текст + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Својства на документот… +pdfjs-document-properties-button-label = Својства на документот… +pdfjs-document-properties-file-name = Име на датотека: +pdfjs-document-properties-file-size = Големина на датотеката: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } бајти) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } бајти) +pdfjs-document-properties-title = Наслов: +pdfjs-document-properties-author = Автор: +pdfjs-document-properties-subject = Тема: +pdfjs-document-properties-keywords = Клучни зборови: +pdfjs-document-properties-creation-date = Датум на создавање: +pdfjs-document-properties-modification-date = Датум на промена: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Креатор: +pdfjs-document-properties-version = Верзија на PDF: +pdfjs-document-properties-page-count = Број на страници: +pdfjs-document-properties-page-size = Големина на страница: +pdfjs-document-properties-page-size-unit-inches = инч +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = портрет +pdfjs-document-properties-page-size-orientation-landscape = пејзаж +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Писмо + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = Да +pdfjs-document-properties-linearized-no = Не +pdfjs-document-properties-close-button = Затвори + +## Print + +pdfjs-print-progress-message = Документ се подготвува за печатење… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Откажи +pdfjs-printing-not-supported = Предупредување: Печатењето не е целосно поддржано во овој прелистувач. +pdfjs-printing-not-ready = Предупредување: PDF документот не е целосно вчитан за печатење. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Вклучи странична лента +pdfjs-toggle-sidebar-button-label = Вклучи странична лента +pdfjs-document-outline-button-label = Содржина на документот +pdfjs-attachments-button = + .title = Прикажи додатоци +pdfjs-thumbs-button = + .title = Прикажување на икони +pdfjs-thumbs-button-label = Икони +pdfjs-findbar-button = + .title = Најди во документот +pdfjs-findbar-button-label = Најди + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Страница { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Икона од страница { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Пронајди + .placeholder = Пронајди во документот… +pdfjs-find-previous-button = + .title = Најди ја предходната појава на фразата +pdfjs-find-previous-button-label = Претходно +pdfjs-find-next-button = + .title = Најди ја следната појава на фразата +pdfjs-find-next-button-label = Следно +pdfjs-find-highlight-checkbox = Означи сѐ +pdfjs-find-match-case-checkbox-label = Токму така +pdfjs-find-entire-word-checkbox-label = Цели зборови +pdfjs-find-reached-top = Барањето стигна до почетокот на документот и почнува од крајот +pdfjs-find-reached-bottom = Барањето стигна до крајот на документот и почнува од почеток +pdfjs-find-not-found = Фразата не е пронајдена + +## Predefined zoom values + +pdfjs-page-scale-width = Ширина на страница +pdfjs-page-scale-fit = Цела страница +pdfjs-page-scale-auto = Автоматска големина +pdfjs-page-scale-actual = Вистинска големина +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Настана грешка при вчитувањето на PDF-от. +pdfjs-invalid-file-error = Невалидна или корумпирана PDF датотека. +pdfjs-missing-file-error = Недостасува PDF документ. +pdfjs-unexpected-response-error = Неочекуван одговор од серверот. +pdfjs-rendering-error = Настана грешка при прикажувањето на страницата. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } + +## Password + +pdfjs-password-label = Внесете ја лозинката за да ја отворите оваа датотека. +pdfjs-password-invalid = Невалидна лозинка. Обидете се повторно. +pdfjs-password-ok-button = Во ред +pdfjs-password-cancel-button = Откажи +pdfjs-web-fonts-disabled = Интернет фонтовите се оневозможени: не може да се користат вградените PDF фонтови. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/mr/viewer.ftl b/public/assets/pdfjs/locale/mr/viewer.ftl new file mode 100644 index 0000000..49948b1 --- /dev/null +++ b/public/assets/pdfjs/locale/mr/viewer.ftl @@ -0,0 +1,239 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = मागील पृष्ठ +pdfjs-previous-button-label = मागील +pdfjs-next-button = + .title = पुढील पृष्ठ +pdfjs-next-button-label = पुढील +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = पृष्ठ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount }पैकी +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } पैकी { $pageNumber }) +pdfjs-zoom-out-button = + .title = छोटे करा +pdfjs-zoom-out-button-label = छोटे करा +pdfjs-zoom-in-button = + .title = मोठे करा +pdfjs-zoom-in-button-label = मोठे करा +pdfjs-zoom-select = + .title = लहान किंवा मोठे करा +pdfjs-presentation-mode-button = + .title = प्रस्तुतिकरण मोडचा वापर करा +pdfjs-presentation-mode-button-label = प्रस्तुतिकरण मोड +pdfjs-open-file-button = + .title = फाइल उघडा +pdfjs-open-file-button-label = उघडा +pdfjs-print-button = + .title = छपाई करा +pdfjs-print-button-label = छपाई करा + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = साधने +pdfjs-tools-button-label = साधने +pdfjs-first-page-button = + .title = पहिल्या पृष्ठावर जा +pdfjs-first-page-button-label = पहिल्या पृष्ठावर जा +pdfjs-last-page-button = + .title = शेवटच्या पृष्ठावर जा +pdfjs-last-page-button-label = शेवटच्या पृष्ठावर जा +pdfjs-page-rotate-cw-button = + .title = घड्याळाच्या काट्याच्या दिशेने फिरवा +pdfjs-page-rotate-cw-button-label = घड्याळाच्या काट्याच्या दिशेने फिरवा +pdfjs-page-rotate-ccw-button = + .title = घड्याळाच्या काट्याच्या उलट दिशेने फिरवा +pdfjs-page-rotate-ccw-button-label = घड्याळाच्या काट्याच्या उलट दिशेने फिरवा +pdfjs-cursor-text-select-tool-button = + .title = मजकूर निवड साधन कार्यान्वयीत करा +pdfjs-cursor-text-select-tool-button-label = मजकूर निवड साधन +pdfjs-cursor-hand-tool-button = + .title = हात साधन कार्यान्वित करा +pdfjs-cursor-hand-tool-button-label = हस्त साधन +pdfjs-scroll-vertical-button = + .title = अनुलंब स्क्रोलिंग वापरा +pdfjs-scroll-vertical-button-label = अनुलंब स्क्रोलिंग +pdfjs-scroll-horizontal-button = + .title = क्षैतिज स्क्रोलिंग वापरा +pdfjs-scroll-horizontal-button-label = क्षैतिज स्क्रोलिंग + +## Document properties dialog + +pdfjs-document-properties-button = + .title = दस्तऐवज गुणधर्म… +pdfjs-document-properties-button-label = दस्तऐवज गुणधर्म… +pdfjs-document-properties-file-name = फाइलचे नाव: +pdfjs-document-properties-file-size = फाइल आकार: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } बाइट्स) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } बाइट्स) +pdfjs-document-properties-title = शिर्षक: +pdfjs-document-properties-author = लेखक: +pdfjs-document-properties-subject = विषय: +pdfjs-document-properties-keywords = मुख्यशब्द: +pdfjs-document-properties-creation-date = निर्माण दिनांक: +pdfjs-document-properties-modification-date = दुरूस्ती दिनांक: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = निर्माता: +pdfjs-document-properties-producer = PDF निर्माता: +pdfjs-document-properties-version = PDF आवृत्ती: +pdfjs-document-properties-page-count = पृष्ठ संख्या: +pdfjs-document-properties-page-size = पृष्ठ आकार: +pdfjs-document-properties-page-size-unit-inches = इंच +pdfjs-document-properties-page-size-unit-millimeters = मीमी +pdfjs-document-properties-page-size-orientation-portrait = उभी मांडणी +pdfjs-document-properties-page-size-orientation-landscape = आडवे +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = जलद वेब दृष्य: +pdfjs-document-properties-linearized-yes = हो +pdfjs-document-properties-linearized-no = नाही +pdfjs-document-properties-close-button = बंद करा + +## Print + +pdfjs-print-progress-message = छपाई करीता पृष्ठ तयार करीत आहे… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = रद्द करा +pdfjs-printing-not-supported = सावधानता: या ब्राउझरतर्फे छपाइ पूर्णपणे समर्थीत नाही. +pdfjs-printing-not-ready = सावधानता: छपाईकरिता PDF पूर्णतया लोड झाले नाही. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = बाजूचीपट्टी टॉगल करा +pdfjs-toggle-sidebar-button-label = बाजूचीपट्टी टॉगल करा +pdfjs-document-outline-button = + .title = दस्तऐवज बाह्यरेखा दर्शवा (विस्तृत करण्यासाठी दोनवेळा क्लिक करा /सर्व घटक दाखवा) +pdfjs-document-outline-button-label = दस्तऐवज रूपरेषा +pdfjs-attachments-button = + .title = जोडपत्र दाखवा +pdfjs-attachments-button-label = जोडपत्र +pdfjs-thumbs-button = + .title = थंबनेल्स् दाखवा +pdfjs-thumbs-button-label = थंबनेल्स् +pdfjs-findbar-button = + .title = दस्तऐवजात शोधा +pdfjs-findbar-button-label = शोधा + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = पृष्ठ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = पृष्ठाचे थंबनेल { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = शोधा + .placeholder = दस्तऐवजात शोधा… +pdfjs-find-previous-button = + .title = वाकप्रयोगची मागील घटना शोधा +pdfjs-find-previous-button-label = मागील +pdfjs-find-next-button = + .title = वाकप्रयोगची पुढील घटना शोधा +pdfjs-find-next-button-label = पुढील +pdfjs-find-highlight-checkbox = सर्व ठळक करा +pdfjs-find-match-case-checkbox-label = आकार जुळवा +pdfjs-find-entire-word-checkbox-label = संपूर्ण शब्द +pdfjs-find-reached-top = दस्तऐवजाच्या शीर्षकास पोहचले, तळपासून पुढे +pdfjs-find-reached-bottom = दस्तऐवजाच्या तळाला पोहचले, शीर्षकापासून पुढे +pdfjs-find-not-found = वाकप्रयोग आढळले नाही + +## Predefined zoom values + +pdfjs-page-scale-width = पृष्ठाची रूंदी +pdfjs-page-scale-fit = पृष्ठ बसवा +pdfjs-page-scale-auto = स्वयं लाहन किंवा मोठे करणे +pdfjs-page-scale-actual = प्रत्यक्ष आकार +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF लोड करतेवेळी त्रुटी आढळली. +pdfjs-invalid-file-error = अवैध किंवा दोषीत PDF फाइल. +pdfjs-missing-file-error = न आढळणारी PDF फाइल. +pdfjs-unexpected-response-error = अनपेक्षित सर्व्हर प्रतिसाद. +pdfjs-rendering-error = पृष्ठ दाखवतेवेळी त्रुटी आढळली. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } टिपण्णी] + +## Password + +pdfjs-password-label = ही PDF फाइल उघडण्याकरिता पासवर्ड द्या. +pdfjs-password-invalid = अवैध पासवर्ड. कृपया पुन्हा प्रयत्न करा. +pdfjs-password-ok-button = ठीक आहे +pdfjs-password-cancel-button = रद्द करा +pdfjs-web-fonts-disabled = वेब टंक असमर्थीत आहेत: एम्बेडेड PDF टंक वापर अशक्य. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ms/viewer.ftl b/public/assets/pdfjs/locale/ms/viewer.ftl new file mode 100644 index 0000000..11b8665 --- /dev/null +++ b/public/assets/pdfjs/locale/ms/viewer.ftl @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Halaman Dahulu +pdfjs-previous-button-label = Dahulu +pdfjs-next-button = + .title = Halaman Berikut +pdfjs-next-button-label = Berikut +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Halaman +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = daripada { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } daripada { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zum Keluar +pdfjs-zoom-out-button-label = Zum Keluar +pdfjs-zoom-in-button = + .title = Zum Masuk +pdfjs-zoom-in-button-label = Zum Masuk +pdfjs-zoom-select = + .title = Zum +pdfjs-presentation-mode-button = + .title = Tukar ke Mod Persembahan +pdfjs-presentation-mode-button-label = Mod Persembahan +pdfjs-open-file-button = + .title = Buka Fail +pdfjs-open-file-button-label = Buka +pdfjs-print-button = + .title = Cetak +pdfjs-print-button-label = Cetak + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Alatan +pdfjs-tools-button-label = Alatan +pdfjs-first-page-button = + .title = Pergi ke Halaman Pertama +pdfjs-first-page-button-label = Pergi ke Halaman Pertama +pdfjs-last-page-button = + .title = Pergi ke Halaman Terakhir +pdfjs-last-page-button-label = Pergi ke Halaman Terakhir +pdfjs-page-rotate-cw-button = + .title = Berputar ikut arah Jam +pdfjs-page-rotate-cw-button-label = Berputar ikut arah Jam +pdfjs-page-rotate-ccw-button = + .title = Pusing berlawan arah jam +pdfjs-page-rotate-ccw-button-label = Pusing berlawan arah jam +pdfjs-cursor-text-select-tool-button = + .title = Dayakan Alatan Pilihan Teks +pdfjs-cursor-text-select-tool-button-label = Alatan Pilihan Teks +pdfjs-cursor-hand-tool-button = + .title = Dayakan Alatan Tangan +pdfjs-cursor-hand-tool-button-label = Alatan Tangan +pdfjs-scroll-vertical-button = + .title = Guna Skrol Menegak +pdfjs-scroll-vertical-button-label = Skrol Menegak +pdfjs-scroll-horizontal-button = + .title = Guna Skrol Mengufuk +pdfjs-scroll-horizontal-button-label = Skrol Mengufuk +pdfjs-scroll-wrapped-button = + .title = Guna Skrol Berbalut +pdfjs-scroll-wrapped-button-label = Skrol Berbalut +pdfjs-spread-none-button = + .title = Jangan hubungkan hamparan halaman +pdfjs-spread-none-button-label = Tanpa Hamparan +pdfjs-spread-odd-button = + .title = Hubungkan hamparan halaman dengan halaman nombor ganjil +pdfjs-spread-odd-button-label = Hamparan Ganjil +pdfjs-spread-even-button = + .title = Hubungkan hamparan halaman dengan halaman nombor genap +pdfjs-spread-even-button-label = Hamparan Seimbang + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Sifat Dokumen… +pdfjs-document-properties-button-label = Sifat Dokumen… +pdfjs-document-properties-file-name = Nama fail: +pdfjs-document-properties-file-size = Saiz fail: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bait) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bait) +pdfjs-document-properties-title = Tajuk: +pdfjs-document-properties-author = Pengarang: +pdfjs-document-properties-subject = Subjek: +pdfjs-document-properties-keywords = Kata kunci: +pdfjs-document-properties-creation-date = Masa Dicipta: +pdfjs-document-properties-modification-date = Tarikh Ubahsuai: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Pencipta: +pdfjs-document-properties-producer = Pengeluar PDF: +pdfjs-document-properties-version = Versi PDF: +pdfjs-document-properties-page-count = Kiraan Laman: +pdfjs-document-properties-page-size = Saiz Halaman: +pdfjs-document-properties-page-size-unit-inches = dalam +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = potret +pdfjs-document-properties-page-size-orientation-landscape = landskap +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Paparan Web Pantas: +pdfjs-document-properties-linearized-yes = Ya +pdfjs-document-properties-linearized-no = Tidak +pdfjs-document-properties-close-button = Tutup + +## Print + +pdfjs-print-progress-message = Menyediakan dokumen untuk dicetak… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Batal +pdfjs-printing-not-supported = Amaran: Cetakan ini tidak sepenuhnya disokong oleh pelayar ini. +pdfjs-printing-not-ready = Amaran: PDF tidak sepenuhnya dimuatkan untuk dicetak. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Togol Bar Sisi +pdfjs-toggle-sidebar-button-label = Togol Bar Sisi +pdfjs-document-outline-button = + .title = Papar Rangka Dokumen (klik-dua-kali untuk kembangkan/kolaps semua item) +pdfjs-document-outline-button-label = Rangka Dokumen +pdfjs-attachments-button = + .title = Papar Lampiran +pdfjs-attachments-button-label = Lampiran +pdfjs-thumbs-button = + .title = Papar Thumbnails +pdfjs-thumbs-button-label = Imej kecil +pdfjs-findbar-button = + .title = Cari didalam Dokumen +pdfjs-findbar-button-label = Cari + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Halaman { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Halaman Imej kecil { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Cari + .placeholder = Cari dalam dokumen… +pdfjs-find-previous-button = + .title = Cari teks frasa berkenaan yang terdahulu +pdfjs-find-previous-button-label = Dahulu +pdfjs-find-next-button = + .title = Cari teks frasa berkenaan yang berikut +pdfjs-find-next-button-label = Berikut +pdfjs-find-highlight-checkbox = Serlahkan semua +pdfjs-find-match-case-checkbox-label = Huruf sepadan +pdfjs-find-entire-word-checkbox-label = Seluruh perkataan +pdfjs-find-reached-top = Mencapai teratas daripada dokumen, sambungan daripada bawah +pdfjs-find-reached-bottom = Mencapai terakhir daripada dokumen, sambungan daripada atas +pdfjs-find-not-found = Frasa tidak ditemui + +## Predefined zoom values + +pdfjs-page-scale-width = Lebar Halaman +pdfjs-page-scale-fit = Muat Halaman +pdfjs-page-scale-auto = Zoom Automatik +pdfjs-page-scale-actual = Saiz Sebenar +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Masalah berlaku semasa menuatkan sebuah PDF. +pdfjs-invalid-file-error = Tidak sah atau fail PDF rosak. +pdfjs-missing-file-error = Fail PDF Hilang. +pdfjs-unexpected-response-error = Respon pelayan yang tidak dijangka. +pdfjs-rendering-error = Ralat berlaku ketika memberikan halaman. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Anotasi] + +## Password + +pdfjs-password-label = Masukan kata kunci untuk membuka fail PDF ini. +pdfjs-password-invalid = Kata laluan salah. Cuba lagi. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Batal +pdfjs-web-fonts-disabled = Fon web dinyahdayakan: tidak dapat menggunakan fon terbenam PDF. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/my/viewer.ftl b/public/assets/pdfjs/locale/my/viewer.ftl new file mode 100644 index 0000000..d3b973d --- /dev/null +++ b/public/assets/pdfjs/locale/my/viewer.ftl @@ -0,0 +1,206 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = အရင် စာမျက်နှာ +pdfjs-previous-button-label = အရင်နေရာ +pdfjs-next-button = + .title = ရှေ့ စာမျက်နှာ +pdfjs-next-button-label = နောက်တခု +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = စာမျက်နှာ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } ၏ +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } ၏ { $pageNumber }) +pdfjs-zoom-out-button = + .title = ချုံ့ပါ +pdfjs-zoom-out-button-label = ချုံ့ပါ +pdfjs-zoom-in-button = + .title = ချဲ့ပါ +pdfjs-zoom-in-button-label = ချဲ့ပါ +pdfjs-zoom-select = + .title = ချုံ့/ချဲ့ပါ +pdfjs-presentation-mode-button = + .title = ဆွေးနွေးတင်ပြစနစ်သို့ ကူးပြောင်းပါ +pdfjs-presentation-mode-button-label = ဆွေးနွေးတင်ပြစနစ် +pdfjs-open-file-button = + .title = ဖိုင်အားဖွင့်ပါ။ +pdfjs-open-file-button-label = ဖွင့်ပါ +pdfjs-print-button = + .title = ပုံနှိုပ်ပါ +pdfjs-print-button-label = ပုံနှိုပ်ပါ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ကိရိယာများ +pdfjs-tools-button-label = ကိရိယာများ +pdfjs-first-page-button = + .title = ပထမ စာမျက်နှာသို့ +pdfjs-first-page-button-label = ပထမ စာမျက်နှာသို့ +pdfjs-last-page-button = + .title = နောက်ဆုံး စာမျက်နှာသို့ +pdfjs-last-page-button-label = နောက်ဆုံး စာမျက်နှာသို့ +pdfjs-page-rotate-cw-button = + .title = နာရီလက်တံ အတိုင်း +pdfjs-page-rotate-cw-button-label = နာရီလက်တံ အတိုင်း +pdfjs-page-rotate-ccw-button = + .title = နာရီလက်တံ ပြောင်းပြန် +pdfjs-page-rotate-ccw-button-label = နာရီလက်တံ ပြောင်းပြန် + +## Document properties dialog + +pdfjs-document-properties-button = + .title = မှတ်တမ်းမှတ်ရာ ဂုဏ်သတ္တိများ +pdfjs-document-properties-button-label = မှတ်တမ်းမှတ်ရာ ဂုဏ်သတ္တိများ +pdfjs-document-properties-file-name = ဖိုင် : +pdfjs-document-properties-file-size = ဖိုင်ဆိုဒ် : +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } ကီလိုဘိုတ် ({ $size_b }ဘိုတ်) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = ခေါင်းစဉ်‌ - +pdfjs-document-properties-author = ရေးသားသူ: +pdfjs-document-properties-subject = အကြောင်းအရာ: +pdfjs-document-properties-keywords = သော့ချက် စာလုံး: +pdfjs-document-properties-creation-date = ထုတ်လုပ်ရက်စွဲ: +pdfjs-document-properties-modification-date = ပြင်ဆင်ရက်စွဲ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ဖန်တီးသူ: +pdfjs-document-properties-producer = PDF ထုတ်လုပ်သူ: +pdfjs-document-properties-version = PDF ဗားရှင်း: +pdfjs-document-properties-page-count = စာမျက်နှာအရေအတွက်: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = ပိတ် + +## Print + +pdfjs-print-progress-message = Preparing document for printing… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ပယ်​ဖျက်ပါ +pdfjs-printing-not-supported = သတိပေးချက်၊ပရင့်ထုတ်ခြင်းကိုဤဘယောက်ဆာသည် ပြည့်ဝစွာထောက်ပံ့မထားပါ ။ +pdfjs-printing-not-ready = သတိပေးချက်: ယခု PDF ဖိုင်သည် ပုံနှိပ်ရန် မပြည့်စုံပါ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ဘေးတန်းဖွင့်ပိတ် +pdfjs-toggle-sidebar-button-label = ဖွင့်ပိတ် ဆလိုက်ဒါ +pdfjs-document-outline-button = + .title = စာတမ်းအကျဉ်းချုပ်ကို ပြပါ (စာရင်းအားလုံးကို ချုံ့/ချဲ့ရန် ကလစ်နှစ်ချက်နှိပ်ပါ) +pdfjs-document-outline-button-label = စာတမ်းအကျဉ်းချုပ် +pdfjs-attachments-button = + .title = တွဲချက်များ ပြပါ +pdfjs-attachments-button-label = တွဲထားချက်များ +pdfjs-thumbs-button = + .title = ပုံရိပ်ငယ်များကို ပြပါ +pdfjs-thumbs-button-label = ပုံရိပ်ငယ်များ +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = ရှာဖွေပါ + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = စာမျက်နှာ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = စာမျက်နှာရဲ့ ပုံရိပ်ငယ် { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ရှာဖွေပါ + .placeholder = စာတမ်းထဲတွင် ရှာဖွေရန်… +pdfjs-find-previous-button = + .title = စကားစုရဲ့ အရင် ​ဖြစ်ပွားမှုကို ရှာဖွေပါ +pdfjs-find-previous-button-label = နောက်သို့ +pdfjs-find-next-button = + .title = စကားစုရဲ့ နောက်ထပ် ​ဖြစ်ပွားမှုကို ရှာဖွေပါ +pdfjs-find-next-button-label = ရှေ့သို့ +pdfjs-find-highlight-checkbox = အားလုံးကို မျဉ်းသားပါ +pdfjs-find-match-case-checkbox-label = စာလုံး တိုက်ဆိုင်ပါ +pdfjs-find-reached-top = စာမျက်နှာထိပ် ရောက်နေပြီ၊ အဆုံးကနေ ပြန်စပါ +pdfjs-find-reached-bottom = စာမျက်နှာအဆုံး ရောက်နေပြီ၊ ထိပ်ကနေ ပြန်စပါ +pdfjs-find-not-found = စကားစု မတွေ့ရဘူး + +## Predefined zoom values + +pdfjs-page-scale-width = စာမျက်နှာ အကျယ် +pdfjs-page-scale-fit = စာမျက်နှာ ကွက်တိ +pdfjs-page-scale-auto = အလိုအလျောက် ချုံ့ချဲ့ +pdfjs-page-scale-actual = အမှန်တကယ်ရှိတဲ့ အရွယ် +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF ဖိုင် ကိုဆွဲတင်နေချိန်မှာ အမှားတစ်ခုတွေ့ရပါတယ်။ +pdfjs-invalid-file-error = မရသော သို့ ပျက်နေသော PDF ဖိုင် +pdfjs-missing-file-error = PDF ပျောက်ဆုံး +pdfjs-unexpected-response-error = မမျှော်လင့်ထားသော ဆာဗာမှ ပြန်ကြားချက် +pdfjs-rendering-error = စာမျက်နှာကို ပုံဖော်နေချိန်မှာ အမှားတစ်ခုတွေ့ရပါတယ်။ + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } အဓိပ္ပာယ်ဖွင့်ဆိုချက်] + +## Password + +pdfjs-password-label = ယခု PDF ကို ဖွင့်ရန် စကားဝှက်ကို ရိုက်ပါ။ +pdfjs-password-invalid = စာဝှက် မှားသည်။ ထပ်ကြိုးစားကြည့်ပါ။ +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = ပယ်​ဖျက်ပါ +pdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/nb-NO/viewer.ftl b/public/assets/pdfjs/locale/nb-NO/viewer.ftl new file mode 100644 index 0000000..a644157 --- /dev/null +++ b/public/assets/pdfjs/locale/nb-NO/viewer.ftl @@ -0,0 +1,495 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Forrige side +pdfjs-previous-button-label = Forrige +pdfjs-next-button = + .title = Neste side +pdfjs-next-button-label = Neste +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Side +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = av { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } av { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom ut +pdfjs-zoom-out-button-label = Zoom ut +pdfjs-zoom-in-button = + .title = Zoom inn +pdfjs-zoom-in-button-label = Zoom inn +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Bytt til presentasjonsmodus +pdfjs-presentation-mode-button-label = Presentasjonsmodus +pdfjs-open-file-button = + .title = Åpne fil +pdfjs-open-file-button-label = Åpne +pdfjs-print-button = + .title = Skriv ut +pdfjs-print-button-label = Skriv ut +pdfjs-save-button = + .title = Lagre +pdfjs-save-button-label = Lagre +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Last ned +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Last ned +pdfjs-bookmark-button = + .title = Gjeldende side (se URL fra gjeldende side) +pdfjs-bookmark-button-label = Gjeldende side + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Verktøy +pdfjs-tools-button-label = Verktøy +pdfjs-first-page-button = + .title = Gå til første side +pdfjs-first-page-button-label = Gå til første side +pdfjs-last-page-button = + .title = Gå til siste side +pdfjs-last-page-button-label = Gå til siste side +pdfjs-page-rotate-cw-button = + .title = Roter med klokken +pdfjs-page-rotate-cw-button-label = Roter med klokken +pdfjs-page-rotate-ccw-button = + .title = Roter mot klokken +pdfjs-page-rotate-ccw-button-label = Roter mot klokken +pdfjs-cursor-text-select-tool-button = + .title = Aktiver tekstmarkeringsverktøy +pdfjs-cursor-text-select-tool-button-label = Tekstmarkeringsverktøy +pdfjs-cursor-hand-tool-button = + .title = Aktiver handverktøy +pdfjs-cursor-hand-tool-button-label = Handverktøy +pdfjs-scroll-page-button = + .title = Bruk siderulling +pdfjs-scroll-page-button-label = Siderulling +pdfjs-scroll-vertical-button = + .title = Bruk vertikal rulling +pdfjs-scroll-vertical-button-label = Vertikal rulling +pdfjs-scroll-horizontal-button = + .title = Bruk horisontal rulling +pdfjs-scroll-horizontal-button-label = Horisontal rulling +pdfjs-scroll-wrapped-button = + .title = Bruk flersiderulling +pdfjs-scroll-wrapped-button-label = Flersiderulling +pdfjs-spread-none-button = + .title = Vis enkeltsider +pdfjs-spread-none-button-label = Enkeltsider +pdfjs-spread-odd-button = + .title = Vis oppslag med ulike sidenumre til venstre +pdfjs-spread-odd-button-label = Oppslag med forside +pdfjs-spread-even-button = + .title = Vis oppslag med like sidenumre til venstre +pdfjs-spread-even-button-label = Oppslag uten forside + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentegenskaper … +pdfjs-document-properties-button-label = Dokumentegenskaper … +pdfjs-document-properties-file-name = Filnavn: +pdfjs-document-properties-file-size = Filstørrelse: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Dokumentegenskaper … +pdfjs-document-properties-author = Forfatter: +pdfjs-document-properties-subject = Emne: +pdfjs-document-properties-keywords = Nøkkelord: +pdfjs-document-properties-creation-date = Opprettet dato: +pdfjs-document-properties-modification-date = Endret dato: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Opprettet av: +pdfjs-document-properties-producer = PDF-verktøy: +pdfjs-document-properties-version = PDF-versjon: +pdfjs-document-properties-page-count = Sideantall: +pdfjs-document-properties-page-size = Sidestørrelse: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = stående +pdfjs-document-properties-page-size-orientation-landscape = liggende +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Hurtig nettvisning: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nei +pdfjs-document-properties-close-button = Lukk + +## Print + +pdfjs-print-progress-message = Forbereder dokument for utskrift … +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Avbryt +pdfjs-printing-not-supported = Advarsel: Utskrift er ikke fullstendig støttet av denne nettleseren. +pdfjs-printing-not-ready = Advarsel: PDF er ikke fullstendig innlastet for utskrift. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Slå av/på sidestolpe +pdfjs-toggle-sidebar-notification-button = + .title = Vis/gjem sidestolpe (dokumentet inneholder oversikt/vedlegg/lag) +pdfjs-toggle-sidebar-button-label = Slå av/på sidestolpe +pdfjs-document-outline-button = + .title = Vis dokumentdisposisjonen (dobbeltklikk for å utvide/skjule alle elementer) +pdfjs-document-outline-button-label = Dokumentdisposisjon +pdfjs-attachments-button = + .title = Vis vedlegg +pdfjs-attachments-button-label = Vedlegg +pdfjs-layers-button = + .title = Vis lag (dobbeltklikk for å tilbakestille alle lag til standardtilstand) +pdfjs-layers-button-label = Lag +pdfjs-thumbs-button = + .title = Vis miniatyrbilde +pdfjs-thumbs-button-label = Miniatyrbilde +pdfjs-current-outline-item-button = + .title = Finn gjeldende disposisjonselement +pdfjs-current-outline-item-button-label = Gjeldende disposisjonselement +pdfjs-findbar-button = + .title = Finn i dokumentet +pdfjs-findbar-button-label = Finn +pdfjs-additional-layers = Ytterligere lag + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Side { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatyrbilde av side { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Søk + .placeholder = Søk i dokument… +pdfjs-find-previous-button = + .title = Finn forrige forekomst av frasen +pdfjs-find-previous-button-label = Forrige +pdfjs-find-next-button = + .title = Finn neste forekomst av frasen +pdfjs-find-next-button-label = Neste +pdfjs-find-highlight-checkbox = Uthev alle +pdfjs-find-match-case-checkbox-label = Skill store/små bokstaver +pdfjs-find-match-diacritics-checkbox-label = Samsvar diakritiske tegn +pdfjs-find-entire-word-checkbox-label = Hele ord +pdfjs-find-reached-top = Nådde toppen av dokumentet, fortsetter fra bunnen +pdfjs-find-reached-bottom = Nådde bunnen av dokumentet, fortsetter fra toppen +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } av { $total } treff + *[other] { $current } av { $total } treff + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mer enn { $limit } treff + *[other] Mer enn { $limit } treff + } +pdfjs-find-not-found = Fant ikke teksten + +## Predefined zoom values + +pdfjs-page-scale-width = Sidebredde +pdfjs-page-scale-fit = Tilpass til siden +pdfjs-page-scale-auto = Automatisk zoom +pdfjs-page-scale-actual = Virkelig størrelse +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Side { $page } + +## Loading indicator messages + +pdfjs-loading-error = En feil oppstod ved lasting av PDF. +pdfjs-invalid-file-error = Ugyldig eller skadet PDF-fil. +pdfjs-missing-file-error = Manglende PDF-fil. +pdfjs-unexpected-response-error = Uventet serverrespons. +pdfjs-rendering-error = En feil oppstod ved opptegning av siden. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } annotasjon] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Skriv inn passordet for å åpne denne PDF-filen. +pdfjs-password-invalid = Ugyldig passord. Prøv igjen. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Avbryt +pdfjs-web-fonts-disabled = Web-fonter er avslått: Kan ikke bruke innbundne PDF-fonter. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Tegn +pdfjs-editor-ink-button-label = Tegn +pdfjs-editor-stamp-button = + .title = Legg til eller rediger bilder +pdfjs-editor-stamp-button-label = Legg til eller rediger bilder +pdfjs-editor-highlight-button = + .title = Markere +pdfjs-editor-highlight-button-label = Markere +pdfjs-highlight-floating-button1 = + .title = Markere + .aria-label = Markere +pdfjs-highlight-floating-button-label = Markere + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Fjern tegningen +pdfjs-editor-remove-freetext-button = + .title = Fjern tekst +pdfjs-editor-remove-stamp-button = + .title = Fjern bildet +pdfjs-editor-remove-highlight-button = + .title = Fjern utheving + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Farge +pdfjs-editor-free-text-size-input = Størrelse +pdfjs-editor-ink-color-input = Farge +pdfjs-editor-ink-thickness-input = Tykkelse +pdfjs-editor-ink-opacity-input = Ugjennomsiktighet +pdfjs-editor-stamp-add-image-button = + .title = Legg til bilde +pdfjs-editor-stamp-add-image-button-label = Legg til bilde +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tykkelse +pdfjs-editor-free-highlight-thickness-title = + .title = Endre tykkelse når du markerer andre elementer enn tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstredigering + .default-content = Begynn å skrive… +pdfjs-free-text = + .aria-label = Tekstredigering +pdfjs-free-text-default-content = Begynn å skrive… +pdfjs-ink = + .aria-label = Tegneredigering +pdfjs-ink-canvas = + .aria-label = Brukerskapt bilde + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt-tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Rediger alt-tekst +pdfjs-editor-alt-text-edit-button-label = Rediger alt-tekst tekst +pdfjs-editor-alt-text-dialog-label = Velg et alternativ +pdfjs-editor-alt-text-dialog-description = Alt-tekst (alternativ tekst) hjelper når folk ikke kan se bildet eller når det ikke lastes inn. +pdfjs-editor-alt-text-add-description-label = Legg til en beskrivelse +pdfjs-editor-alt-text-add-description-description = Gå etter 1-2 setninger som beskriver emnet, settingen eller handlingene. +pdfjs-editor-alt-text-mark-decorative-label = Merk som dekorativt +pdfjs-editor-alt-text-mark-decorative-description = Dette brukes til dekorative bilder, som kantlinjer eller vannmerker. +pdfjs-editor-alt-text-cancel-button = Avbryt +pdfjs-editor-alt-text-save-button = Lagre +pdfjs-editor-alt-text-decorative-tooltip = Merket som dekorativ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = For eksempel, «En ung mann setter seg ved et bord for å spise et måltid» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt-tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Øverste venstre hjørne – endre størrelse +pdfjs-editor-resizer-label-top-middle = Øverst i midten — endre størrelse +pdfjs-editor-resizer-label-top-right = Øverste høyre hjørne – endre størrelse +pdfjs-editor-resizer-label-middle-right = Midt til høyre – endre størrelse +pdfjs-editor-resizer-label-bottom-right = Nederste høyre hjørne – endre størrelse +pdfjs-editor-resizer-label-bottom-middle = Nederst i midten — endre størrelse +pdfjs-editor-resizer-label-bottom-left = Nederste venstre hjørne – endre størrelse +pdfjs-editor-resizer-label-middle-left = Midt til venstre — endre størrelse +pdfjs-editor-resizer-top-left = + .aria-label = Øverste venstre hjørne – endre størrelse +pdfjs-editor-resizer-top-middle = + .aria-label = Øverst i midten — endre størrelse +pdfjs-editor-resizer-top-right = + .aria-label = Øverste høyre hjørne – endre størrelse +pdfjs-editor-resizer-middle-right = + .aria-label = Midt til høyre – endre størrelse +pdfjs-editor-resizer-bottom-right = + .aria-label = Nederste høyre hjørne – endre størrelse +pdfjs-editor-resizer-bottom-middle = + .aria-label = Nederst i midten — endre størrelse +pdfjs-editor-resizer-bottom-left = + .aria-label = Nederste venstre hjørne – endre størrelse +pdfjs-editor-resizer-middle-left = + .aria-label = Midt til venstre — endre størrelse + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Uthevingsfarge +pdfjs-editor-colorpicker-button = + .title = Endre farge +pdfjs-editor-colorpicker-dropdown = + .aria-label = Fargevalg +pdfjs-editor-colorpicker-yellow = + .title = Gul +pdfjs-editor-colorpicker-green = + .title = Grønn +pdfjs-editor-colorpicker-blue = + .title = Blå +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Rød + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Vis alle +pdfjs-editor-highlight-show-all-button = + .title = Vis alle + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Rediger alternativ tekst (bildebeskrivelse) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Legg til alternativ tekst (bildebeskrivelse) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skriv din beskrivelse her… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kort beskrivelse for folk som ikke kan se bildet eller når bildet ikke lastes inn. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Denne alternative teksten ble opprettet automatisk og kan være unøyaktig. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Les mer +pdfjs-editor-new-alt-text-create-automatically-button-label = Lag alternativ tekst automatisk +pdfjs-editor-new-alt-text-not-now-button = Ikke nå +pdfjs-editor-new-alt-text-error-title = Kunne ikke opprette alternativ tekst automatisk +pdfjs-editor-new-alt-text-error-description = Skriv din egen alternativ-tekst eller prøv igjen senere. +pdfjs-editor-new-alt-text-error-close-button = Lukk +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Laster ned alternativ tekst AI-modell ({ $downloadedSize } av { $totalSize } MB) + .aria-valuetext = Laster ned alternativ tekst AI-modell ({ $downloadedSize } av { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alt-tekst lagt til +pdfjs-editor-new-alt-text-added-button-label = Alternativ tekst lagt til +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Mangler alt-tekst +pdfjs-editor-new-alt-text-missing-button-label = Mangler alternativ tekst +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Gjennomgå alt-tekst +pdfjs-editor-new-alt-text-to-review-button-label = Gjennomgå alternativ tekst +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Opprettet automatisk: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Innstillinger for alternativ tekst for bilde +pdfjs-image-alt-text-settings-button-label = Innstillinger for alternativ tekst for bilde +pdfjs-editor-alt-text-settings-dialog-label = Innstillinger for alternativ tekst for bilde +pdfjs-editor-alt-text-settings-automatic-title = Automatisk alternativ tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Opprett alternativ tekst automatisk +pdfjs-editor-alt-text-settings-create-model-description = Foreslår beskrivelser for å hjelpe folk som ikke kan se bildet eller når bildet ikke lastes inn. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alternativ tekst AI-modell ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Kjører lokalt på enheten din slik at dataene dine forblir private. Nødvendig for automatisk alternativ tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Slett +pdfjs-editor-alt-text-settings-download-model-button = Last ned +pdfjs-editor-alt-text-settings-downloading-model-button = Laster ned… +pdfjs-editor-alt-text-settings-editor-title = Alternativ tekst-redigerer +pdfjs-editor-alt-text-settings-show-dialog-button-label = Vis alternativ tekst-redigerer direkte når du legger til et bilde +pdfjs-editor-alt-text-settings-show-dialog-description = Hjelper deg å sørge for at alle bildene dine har alternativ tekst. +pdfjs-editor-alt-text-settings-close-button = Lukk diff --git a/public/assets/pdfjs/locale/ne-NP/viewer.ftl b/public/assets/pdfjs/locale/ne-NP/viewer.ftl new file mode 100644 index 0000000..65193b6 --- /dev/null +++ b/public/assets/pdfjs/locale/ne-NP/viewer.ftl @@ -0,0 +1,234 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = अघिल्लो पृष्ठ +pdfjs-previous-button-label = अघिल्लो +pdfjs-next-button = + .title = पछिल्लो पृष्ठ +pdfjs-next-button-label = पछिल्लो +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = पृष्ठ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } मध्ये +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pagesCount } को { $pageNumber }) +pdfjs-zoom-out-button = + .title = जुम घटाउनुहोस् +pdfjs-zoom-out-button-label = जुम घटाउनुहोस् +pdfjs-zoom-in-button = + .title = जुम बढाउनुहोस् +pdfjs-zoom-in-button-label = जुम बढाउनुहोस् +pdfjs-zoom-select = + .title = जुम गर्नुहोस् +pdfjs-presentation-mode-button = + .title = प्रस्तुति मोडमा जानुहोस् +pdfjs-presentation-mode-button-label = प्रस्तुति मोड +pdfjs-open-file-button = + .title = फाइल खोल्नुहोस् +pdfjs-open-file-button-label = खोल्नुहोस् +pdfjs-print-button = + .title = मुद्रण गर्नुहोस् +pdfjs-print-button-label = मुद्रण गर्नुहोस् + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = औजारहरू +pdfjs-tools-button-label = औजारहरू +pdfjs-first-page-button = + .title = पहिलो पृष्ठमा जानुहोस् +pdfjs-first-page-button-label = पहिलो पृष्ठमा जानुहोस् +pdfjs-last-page-button = + .title = पछिल्लो पृष्ठमा जानुहोस् +pdfjs-last-page-button-label = पछिल्लो पृष्ठमा जानुहोस् +pdfjs-page-rotate-cw-button = + .title = घडीको दिशामा घुमाउनुहोस् +pdfjs-page-rotate-cw-button-label = घडीको दिशामा घुमाउनुहोस् +pdfjs-page-rotate-ccw-button = + .title = घडीको विपरित दिशामा घुमाउनुहोस् +pdfjs-page-rotate-ccw-button-label = घडीको विपरित दिशामा घुमाउनुहोस् +pdfjs-cursor-text-select-tool-button = + .title = पाठ चयन उपकरण सक्षम गर्नुहोस् +pdfjs-cursor-text-select-tool-button-label = पाठ चयन उपकरण +pdfjs-cursor-hand-tool-button = + .title = हाते उपकरण सक्षम गर्नुहोस् +pdfjs-cursor-hand-tool-button-label = हाते उपकरण +pdfjs-scroll-vertical-button = + .title = ठाडो स्क्रोलिङ्ग प्रयोग गर्नुहोस् +pdfjs-scroll-vertical-button-label = ठाडो स्क्र्रोलिङ्ग +pdfjs-scroll-horizontal-button = + .title = तेर्सो स्क्रोलिङ्ग प्रयोग गर्नुहोस् +pdfjs-scroll-horizontal-button-label = तेर्सो स्क्रोलिङ्ग +pdfjs-scroll-wrapped-button = + .title = लिपि स्क्रोलिङ्ग प्रयोग गर्नुहोस् +pdfjs-scroll-wrapped-button-label = लिपि स्क्रोलिङ्ग +pdfjs-spread-none-button = + .title = पृष्ठ स्प्रेडमा सामेल हुनुहुन्न +pdfjs-spread-none-button-label = स्प्रेड छैन + +## Document properties dialog + +pdfjs-document-properties-button = + .title = कागजात विशेषताहरू... +pdfjs-document-properties-button-label = कागजात विशेषताहरू... +pdfjs-document-properties-file-name = फाइल नाम: +pdfjs-document-properties-file-size = फाइल आकार: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = शीर्षक: +pdfjs-document-properties-author = लेखक: +pdfjs-document-properties-subject = विषयः +pdfjs-document-properties-keywords = शब्दकुञ्जीः +pdfjs-document-properties-creation-date = सिर्जना गरिएको मिति: +pdfjs-document-properties-modification-date = परिमार्जित मिति: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = सर्जक: +pdfjs-document-properties-producer = PDF निर्माता: +pdfjs-document-properties-version = PDF संस्करण +pdfjs-document-properties-page-count = पृष्ठ गणना: +pdfjs-document-properties-page-size = पृष्ठ आकार: +pdfjs-document-properties-page-size-unit-inches = इन्च +pdfjs-document-properties-page-size-unit-millimeters = मि.मि. +pdfjs-document-properties-page-size-orientation-portrait = पोट्रेट +pdfjs-document-properties-page-size-orientation-landscape = परिदृश्य +pdfjs-document-properties-page-size-name-letter = अक्षर +pdfjs-document-properties-page-size-name-legal = कानूनी + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-linearized-yes = हो +pdfjs-document-properties-linearized-no = होइन +pdfjs-document-properties-close-button = बन्द गर्नुहोस् + +## Print + +pdfjs-print-progress-message = मुद्रणका लागि कागजात तयारी गरिदै… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = रद्द गर्नुहोस् +pdfjs-printing-not-supported = चेतावनी: यो ब्राउजरमा मुद्रण पूर्णतया समर्थित छैन। +pdfjs-printing-not-ready = चेतावनी: PDF मुद्रणका लागि पूर्णतया लोड भएको छैन। + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = टगल साइडबार +pdfjs-toggle-sidebar-button-label = टगल साइडबार +pdfjs-document-outline-button = + .title = कागजातको रूपरेखा देखाउनुहोस् (सबै वस्तुहरू विस्तार/पतन गर्न डबल-क्लिक गर्नुहोस्) +pdfjs-document-outline-button-label = दस्तावेजको रूपरेखा +pdfjs-attachments-button = + .title = संलग्नहरू देखाउनुहोस् +pdfjs-attachments-button-label = संलग्नकहरू +pdfjs-thumbs-button = + .title = थम्बनेलहरू देखाउनुहोस् +pdfjs-thumbs-button-label = थम्बनेलहरू +pdfjs-findbar-button = + .title = कागजातमा फेला पार्नुहोस् +pdfjs-findbar-button-label = फेला पार्नुहोस् + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = पृष्ठ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } पृष्ठको थम्बनेल + +## Find panel button title and messages + +pdfjs-find-input = + .title = फेला पार्नुहोस् + .placeholder = कागजातमा फेला पार्नुहोस्… +pdfjs-find-previous-button = + .title = यस वाक्यांशको अघिल्लो घटना फेला पार्नुहोस् +pdfjs-find-previous-button-label = अघिल्लो +pdfjs-find-next-button = + .title = यस वाक्यांशको पछिल्लो घटना फेला पार्नुहोस् +pdfjs-find-next-button-label = अर्को +pdfjs-find-highlight-checkbox = सबै हाइलाइट गर्ने +pdfjs-find-match-case-checkbox-label = केस जोडा मिलाउनुहोस् +pdfjs-find-entire-word-checkbox-label = पुरा शब्दहरु +pdfjs-find-reached-top = पृष्ठको शिर्षमा पुगीयो, तलबाट जारी गरिएको थियो +pdfjs-find-reached-bottom = पृष्ठको अन्त्यमा पुगीयो, शिर्षबाट जारी गरिएको थियो +pdfjs-find-not-found = वाक्यांश फेला परेन + +## Predefined zoom values + +pdfjs-page-scale-width = पृष्ठ चौडाइ +pdfjs-page-scale-fit = पृष्ठ ठिक्क मिल्ने +pdfjs-page-scale-auto = स्वचालित जुम +pdfjs-page-scale-actual = वास्तविक आकार +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = यो PDF लोड गर्दा एउटा त्रुटि देखापर्‍यो। +pdfjs-invalid-file-error = अवैध वा दुषित PDF फाइल। +pdfjs-missing-file-error = हराईरहेको PDF फाइल। +pdfjs-unexpected-response-error = अप्रत्याशित सर्भर प्रतिक्रिया। +pdfjs-rendering-error = पृष्ठ प्रतिपादन गर्दा एउटा त्रुटि देखापर्‍यो। + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = यस PDF फाइललाई खोल्न गोप्यशब्द प्रविष्ट गर्नुहोस्। +pdfjs-password-invalid = अवैध गोप्यशब्द। पुनः प्रयास गर्नुहोस्। +pdfjs-password-ok-button = ठिक छ +pdfjs-password-cancel-button = रद्द गर्नुहोस् +pdfjs-web-fonts-disabled = वेब फन्ट असक्षम छन्: एम्बेडेड PDF फन्ट प्रयोग गर्न असमर्थ। + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/nl/viewer.ftl b/public/assets/pdfjs/locale/nl/viewer.ftl new file mode 100644 index 0000000..fe24ce7 --- /dev/null +++ b/public/assets/pdfjs/locale/nl/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Vorige pagina +pdfjs-previous-button-label = Vorige +pdfjs-next-button = + .title = Volgende pagina +pdfjs-next-button-label = Volgende +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = van { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } van { $pagesCount }) +pdfjs-zoom-out-button = + .title = Uitzoomen +pdfjs-zoom-out-button-label = Uitzoomen +pdfjs-zoom-in-button = + .title = Inzoomen +pdfjs-zoom-in-button-label = Inzoomen +pdfjs-zoom-select = + .title = Zoomen +pdfjs-presentation-mode-button = + .title = Wisselen naar presentatiemodus +pdfjs-presentation-mode-button-label = Presentatiemodus +pdfjs-open-file-button = + .title = Bestand openen +pdfjs-open-file-button-label = Openen +pdfjs-print-button = + .title = Afdrukken +pdfjs-print-button-label = Afdrukken +pdfjs-save-button = + .title = Opslaan +pdfjs-save-button-label = Opslaan +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Downloaden +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Downloaden +pdfjs-bookmark-button = + .title = Huidige pagina (URL van huidige pagina bekijken) +pdfjs-bookmark-button-label = Huidige pagina + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Hulpmiddelen +pdfjs-tools-button-label = Hulpmiddelen +pdfjs-first-page-button = + .title = Naar eerste pagina gaan +pdfjs-first-page-button-label = Naar eerste pagina gaan +pdfjs-last-page-button = + .title = Naar laatste pagina gaan +pdfjs-last-page-button-label = Naar laatste pagina gaan +pdfjs-page-rotate-cw-button = + .title = Rechtsom draaien +pdfjs-page-rotate-cw-button-label = Rechtsom draaien +pdfjs-page-rotate-ccw-button = + .title = Linksom draaien +pdfjs-page-rotate-ccw-button-label = Linksom draaien +pdfjs-cursor-text-select-tool-button = + .title = Tekstselectiehulpmiddel inschakelen +pdfjs-cursor-text-select-tool-button-label = Tekstselectiehulpmiddel +pdfjs-cursor-hand-tool-button = + .title = Handhulpmiddel inschakelen +pdfjs-cursor-hand-tool-button-label = Handhulpmiddel +pdfjs-scroll-page-button = + .title = Paginascrollen gebruiken +pdfjs-scroll-page-button-label = Paginascrollen +pdfjs-scroll-vertical-button = + .title = Verticaal scrollen gebruiken +pdfjs-scroll-vertical-button-label = Verticaal scrollen +pdfjs-scroll-horizontal-button = + .title = Horizontaal scrollen gebruiken +pdfjs-scroll-horizontal-button-label = Horizontaal scrollen +pdfjs-scroll-wrapped-button = + .title = Scrollen met terugloop gebruiken +pdfjs-scroll-wrapped-button-label = Scrollen met terugloop +pdfjs-spread-none-button = + .title = Dubbele pagina’s niet samenvoegen +pdfjs-spread-none-button-label = Geen dubbele pagina’s +pdfjs-spread-odd-button = + .title = Dubbele pagina’s samenvoegen vanaf oneven pagina’s +pdfjs-spread-odd-button-label = Oneven dubbele pagina’s +pdfjs-spread-even-button = + .title = Dubbele pagina’s samenvoegen vanaf even pagina’s +pdfjs-spread-even-button-label = Even dubbele pagina’s + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Documenteigenschappen… +pdfjs-document-properties-button-label = Documenteigenschappen… +pdfjs-document-properties-file-name = Bestandsnaam: +pdfjs-document-properties-file-size = Bestandsgrootte: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Auteur: +pdfjs-document-properties-subject = Onderwerp: +pdfjs-document-properties-keywords = Sleutelwoorden: +pdfjs-document-properties-creation-date = Aanmaakdatum: +pdfjs-document-properties-modification-date = Wijzigingsdatum: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Maker: +pdfjs-document-properties-producer = PDF-producent: +pdfjs-document-properties-version = PDF-versie: +pdfjs-document-properties-page-count = Aantal pagina’s: +pdfjs-document-properties-page-size = Paginagrootte: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = staand +pdfjs-document-properties-page-size-orientation-landscape = liggend +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Snelle webweergave: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nee +pdfjs-document-properties-close-button = Sluiten + +## Print + +pdfjs-print-progress-message = Document voorbereiden voor afdrukken… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Annuleren +pdfjs-printing-not-supported = Waarschuwing: afdrukken wordt niet volledig ondersteund door deze browser. +pdfjs-printing-not-ready = Waarschuwing: de PDF is niet volledig geladen voor afdrukken. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Zijbalk in-/uitschakelen +pdfjs-toggle-sidebar-notification-button = + .title = Zijbalk in-/uitschakelen (document bevat overzicht/bijlagen/lagen) +pdfjs-toggle-sidebar-button-label = Zijbalk in-/uitschakelen +pdfjs-document-outline-button = + .title = Documentoverzicht tonen (dubbelklik om alle items uit/samen te vouwen) +pdfjs-document-outline-button-label = Documentoverzicht +pdfjs-attachments-button = + .title = Bijlagen tonen +pdfjs-attachments-button-label = Bijlagen +pdfjs-layers-button = + .title = Lagen tonen (dubbelklik om alle lagen naar de standaardstatus terug te zetten) +pdfjs-layers-button-label = Lagen +pdfjs-thumbs-button = + .title = Miniaturen tonen +pdfjs-thumbs-button-label = Miniaturen +pdfjs-current-outline-item-button = + .title = Huidig item in inhoudsopgave zoeken +pdfjs-current-outline-item-button-label = Huidig item in inhoudsopgave +pdfjs-findbar-button = + .title = Zoeken in document +pdfjs-findbar-button-label = Zoeken +pdfjs-additional-layers = Aanvullende lagen + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatuur van pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Zoeken + .placeholder = Zoeken in document… +pdfjs-find-previous-button = + .title = De vorige overeenkomst van de tekst zoeken +pdfjs-find-previous-button-label = Vorige +pdfjs-find-next-button = + .title = De volgende overeenkomst van de tekst zoeken +pdfjs-find-next-button-label = Volgende +pdfjs-find-highlight-checkbox = Alles markeren +pdfjs-find-match-case-checkbox-label = Hoofdlettergevoelig +pdfjs-find-match-diacritics-checkbox-label = Diakritische tekens gebruiken +pdfjs-find-entire-word-checkbox-label = Hele woorden +pdfjs-find-reached-top = Bovenkant van document bereikt, doorgegaan vanaf onderkant +pdfjs-find-reached-bottom = Onderkant van document bereikt, doorgegaan vanaf bovenkant +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } van { $total } overeenkomst + *[other] { $current } van { $total } overeenkomsten + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Meer dan { $limit } overeenkomst + *[other] Meer dan { $limit } overeenkomsten + } +pdfjs-find-not-found = Tekst niet gevonden + +## Predefined zoom values + +pdfjs-page-scale-width = Paginabreedte +pdfjs-page-scale-fit = Hele pagina +pdfjs-page-scale-auto = Automatisch zoomen +pdfjs-page-scale-actual = Werkelijke grootte +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Er is een fout opgetreden bij het laden van de PDF. +pdfjs-invalid-file-error = Ongeldig of beschadigd PDF-bestand. +pdfjs-missing-file-error = PDF-bestand ontbreekt. +pdfjs-unexpected-response-error = Onverwacht serverantwoord. +pdfjs-rendering-error = Er is een fout opgetreden bij het weergeven van de pagina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-aantekening] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Voer het wachtwoord in om dit PDF-bestand te openen. +pdfjs-password-invalid = Ongeldig wachtwoord. Probeer het opnieuw. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Annuleren +pdfjs-web-fonts-disabled = Weblettertypen zijn uitgeschakeld: gebruik van ingebedde PDF-lettertypen is niet mogelijk. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Tekenen +pdfjs-editor-ink-button-label = Tekenen +pdfjs-editor-stamp-button = + .title = Afbeeldingen toevoegen of bewerken +pdfjs-editor-stamp-button-label = Afbeeldingen toevoegen of bewerken +pdfjs-editor-highlight-button = + .title = Markeren +pdfjs-editor-highlight-button-label = Markeren +pdfjs-highlight-floating-button1 = + .title = Markeren + .aria-label = Markeren +pdfjs-highlight-floating-button-label = Markeren + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Tekening verwijderen +pdfjs-editor-remove-freetext-button = + .title = Tekst verwijderen +pdfjs-editor-remove-stamp-button = + .title = Afbeelding verwijderen +pdfjs-editor-remove-highlight-button = + .title = Markering verwijderen + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Kleur +pdfjs-editor-free-text-size-input = Grootte +pdfjs-editor-ink-color-input = Kleur +pdfjs-editor-ink-thickness-input = Dikte +pdfjs-editor-ink-opacity-input = Opaciteit +pdfjs-editor-stamp-add-image-button = + .title = Afbeelding toevoegen +pdfjs-editor-stamp-add-image-button-label = Afbeelding toevoegen +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Dikte +pdfjs-editor-free-highlight-thickness-title = + .title = Dikte wijzigen bij accentuering van andere items dan tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstbewerker + .default-content = Start met typen… +pdfjs-free-text = + .aria-label = Tekstbewerker +pdfjs-free-text-default-content = Begin met typen… +pdfjs-ink = + .aria-label = Tekeningbewerker +pdfjs-ink-canvas = + .aria-label = Door gebruiker gemaakte afbeelding + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatieve tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternatieve tekst bewerken +pdfjs-editor-alt-text-edit-button-label = Alternatieve tekst bewerken +pdfjs-editor-alt-text-dialog-label = Kies een optie +pdfjs-editor-alt-text-dialog-description = Alternatieve tekst helpt wanneer mensen de afbeelding niet kunnen zien of wanneer deze niet wordt geladen. +pdfjs-editor-alt-text-add-description-label = Voeg een beschrijving toe +pdfjs-editor-alt-text-add-description-description = Streef naar 1-2 zinnen die het onderwerp, de omgeving of de acties beschrijven. +pdfjs-editor-alt-text-mark-decorative-label = Als decoratief markeren +pdfjs-editor-alt-text-mark-decorative-description = Dit wordt gebruikt voor sierafbeeldingen, zoals randen of watermerken. +pdfjs-editor-alt-text-cancel-button = Annuleren +pdfjs-editor-alt-text-save-button = Opslaan +pdfjs-editor-alt-text-decorative-tooltip = Als decoratief gemarkeerd +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Bijvoorbeeld: ‘Een jonge man gaat aan een tafel zitten om te eten’ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatieve tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Linkerbovenhoek – formaat wijzigen +pdfjs-editor-resizer-label-top-middle = Midden boven – formaat wijzigen +pdfjs-editor-resizer-label-top-right = Rechterbovenhoek – formaat wijzigen +pdfjs-editor-resizer-label-middle-right = Midden rechts – formaat wijzigen +pdfjs-editor-resizer-label-bottom-right = Rechterbenedenhoek – formaat wijzigen +pdfjs-editor-resizer-label-bottom-middle = Midden onder – formaat wijzigen +pdfjs-editor-resizer-label-bottom-left = Linkerbenedenhoek – formaat wijzigen +pdfjs-editor-resizer-label-middle-left = Links midden – formaat wijzigen +pdfjs-editor-resizer-top-left = + .aria-label = Linkerbovenhoek – formaat wijzigen +pdfjs-editor-resizer-top-middle = + .aria-label = Midden boven – formaat wijzigen +pdfjs-editor-resizer-top-right = + .aria-label = Rechterbovenhoek – formaat wijzigen +pdfjs-editor-resizer-middle-right = + .aria-label = Midden rechts – formaat wijzigen +pdfjs-editor-resizer-bottom-right = + .aria-label = Rechterbenedenhoek – formaat wijzigen +pdfjs-editor-resizer-bottom-middle = + .aria-label = Midden onder – formaat wijzigen +pdfjs-editor-resizer-bottom-left = + .aria-label = Linkerbenedenhoek – formaat wijzigen +pdfjs-editor-resizer-middle-left = + .aria-label = Links midden – formaat wijzigen + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Markeringskleur +pdfjs-editor-colorpicker-button = + .title = Kleur wijzigen +pdfjs-editor-colorpicker-dropdown = + .aria-label = Kleurkeuzes +pdfjs-editor-colorpicker-yellow = + .title = Geel +pdfjs-editor-colorpicker-green = + .title = Groen +pdfjs-editor-colorpicker-blue = + .title = Blauw +pdfjs-editor-colorpicker-pink = + .title = Roze +pdfjs-editor-colorpicker-red = + .title = Rood + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Alles tonen +pdfjs-editor-highlight-show-all-button = + .title = Alles tonen + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alternatieve tekst (afbeeldingsbeschrijving) bewerken +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alternatieve tekst (afbeeldingsbeschrijving) toevoegen +pdfjs-editor-new-alt-text-textarea = + .placeholder = Schrijf hier uw beschrijving… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Korte beschrijving voor mensen die de afbeelding niet kunnen zien of wanneer de afbeelding niet wordt geladen. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Deze alternatieve tekst is automatisch gemaakt en is mogelijk onjuist. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Meer info +pdfjs-editor-new-alt-text-create-automatically-button-label = Alternatieve tekst automatisch aanmaken +pdfjs-editor-new-alt-text-not-now-button = Niet nu +pdfjs-editor-new-alt-text-error-title = Kan alternatieve tekst niet automatisch aanmaken +pdfjs-editor-new-alt-text-error-description = Schrijf uw eigen alternatieve tekst of probeer het later nog eens. +pdfjs-editor-new-alt-text-error-close-button = Sluiten +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = AI-model voor alternatieve tekst downloaden ({ $downloadedSize } van { $totalSize } MB) + .aria-valuetext = AI-model voor alternatieve tekst downloaden ({ $downloadedSize } van { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatieve tekst toegevoegd +pdfjs-editor-new-alt-text-added-button-label = Alternatieve tekst toegevoegd +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Alternatieve tekst ontbreekt +pdfjs-editor-new-alt-text-missing-button-label = Alternatieve tekst ontbreekt +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternatieve tekst beoordelen +pdfjs-editor-new-alt-text-to-review-button-label = Alternatieve tekst beoordelen +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Automatisch aangemaakt: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Instellingen voor alternatieve tekst van afbeeldingen +pdfjs-image-alt-text-settings-button-label = Instellingen voor alternatieve tekst van afbeeldingen +pdfjs-editor-alt-text-settings-dialog-label = Instellingen voor alternatieve tekst van afbeeldingen +pdfjs-editor-alt-text-settings-automatic-title = Automatische alternatieve tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Alternatieve tekst automatisch aanmaken +pdfjs-editor-alt-text-settings-create-model-description = Stelt beschrijvingen voor om mensen te helpen die de afbeelding niet kunnen zien of voor wie de afbeelding niet wordt geladen. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = AI-model voor alternatieve tekst ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Wordt lokaal op uw apparaat uitgevoerd, zodat uw gegevens privé blijven. Vereist voor automatische alternatieve tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Verwijderen +pdfjs-editor-alt-text-settings-download-model-button = Downloaden +pdfjs-editor-alt-text-settings-downloading-model-button = Downloaden… +pdfjs-editor-alt-text-settings-editor-title = Alternatieve-tekstbewerker +pdfjs-editor-alt-text-settings-show-dialog-button-label = Alternatieve-tekstbewerker meteen tonen bij toevoegen van een afbeelding +pdfjs-editor-alt-text-settings-show-dialog-description = Helpt u ervoor te zorgen dat al uw afbeeldingen alternatieve tekst hebben. +pdfjs-editor-alt-text-settings-close-button = Sluiten + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Markering verwijderd +pdfjs-editor-undo-bar-message-freetext = Tekst verwijderd +pdfjs-editor-undo-bar-message-ink = Tekening verwijderd +pdfjs-editor-undo-bar-message-stamp = Afbeelding verwijderd +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotatie verwijderd + *[other] { $count } annotaties verwijderd + } +pdfjs-editor-undo-bar-undo-button = + .title = Ongedaan maken +pdfjs-editor-undo-bar-undo-button-label = Ongedaan maken +pdfjs-editor-undo-bar-close-button = + .title = Sluiten +pdfjs-editor-undo-bar-close-button-label = Sluiten diff --git a/public/assets/pdfjs/locale/nn-NO/viewer.ftl b/public/assets/pdfjs/locale/nn-NO/viewer.ftl new file mode 100644 index 0000000..d617e16 --- /dev/null +++ b/public/assets/pdfjs/locale/nn-NO/viewer.ftl @@ -0,0 +1,498 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Føregåande side +pdfjs-previous-button-label = Føregåande +pdfjs-next-button = + .title = Neste side +pdfjs-next-button-label = Neste +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Side +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = av { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } av { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom ut +pdfjs-zoom-out-button-label = Zoom ut +pdfjs-zoom-in-button = + .title = Zoom inn +pdfjs-zoom-in-button-label = Zoom inn +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Byt til presentasjonsmodus +pdfjs-presentation-mode-button-label = Presentasjonsmodus +pdfjs-open-file-button = + .title = Opne fil +pdfjs-open-file-button-label = Opne +pdfjs-print-button = + .title = Skriv ut +pdfjs-print-button-label = Skriv ut +pdfjs-save-button = + .title = Lagre +pdfjs-save-button-label = Lagre +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Last ned +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Last ned +pdfjs-bookmark-button = + .title = Gjeldande side (sjå URL frå gjeldande side) +pdfjs-bookmark-button-label = Gjeldande side + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Verktøy +pdfjs-tools-button-label = Verktøy +pdfjs-first-page-button = + .title = Gå til første side +pdfjs-first-page-button-label = Gå til første side +pdfjs-last-page-button = + .title = Gå til siste side +pdfjs-last-page-button-label = Gå til siste side +pdfjs-page-rotate-cw-button = + .title = Roter med klokka +pdfjs-page-rotate-cw-button-label = Roter med klokka +pdfjs-page-rotate-ccw-button = + .title = Roter mot klokka +pdfjs-page-rotate-ccw-button-label = Roter mot klokka +pdfjs-cursor-text-select-tool-button = + .title = Aktiver tekstmarkeringsverktøy +pdfjs-cursor-text-select-tool-button-label = Tekstmarkeringsverktøy +pdfjs-cursor-hand-tool-button = + .title = Aktiver handverktøy +pdfjs-cursor-hand-tool-button-label = Handverktøy +pdfjs-scroll-page-button = + .title = Bruk siderulling +pdfjs-scroll-page-button-label = Siderulling +pdfjs-scroll-vertical-button = + .title = Bruk vertikal rulling +pdfjs-scroll-vertical-button-label = Vertikal rulling +pdfjs-scroll-horizontal-button = + .title = Bruk horisontal rulling +pdfjs-scroll-horizontal-button-label = Horisontal rulling +pdfjs-scroll-wrapped-button = + .title = Bruk fleirsiderulling +pdfjs-scroll-wrapped-button-label = Fleirsiderulling +pdfjs-spread-none-button = + .title = Vis enkeltsider +pdfjs-spread-none-button-label = Enkeltside +pdfjs-spread-odd-button = + .title = Vis oppslag med ulike sidenummer til venstre +pdfjs-spread-odd-button-label = Oppslag med framside +pdfjs-spread-even-button = + .title = Vis oppslag med like sidenummmer til venstre +pdfjs-spread-even-button-label = Oppslag utan framside + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumenteigenskapar… +pdfjs-document-properties-button-label = Dokumenteigenskapar… +pdfjs-document-properties-file-name = Filnamn: +pdfjs-document-properties-file-size = Filstorleik: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Tittel: +pdfjs-document-properties-author = Forfattar: +pdfjs-document-properties-subject = Emne: +pdfjs-document-properties-keywords = Stikkord: +pdfjs-document-properties-creation-date = Dato oppretta: +pdfjs-document-properties-modification-date = Dato endra: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Oppretta av: +pdfjs-document-properties-producer = PDF-verktøy: +pdfjs-document-properties-version = PDF-versjon: +pdfjs-document-properties-page-count = Sidetal: +pdfjs-document-properties-page-size = Sidestørrelse: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = ståande (portrait) +pdfjs-document-properties-page-size-orientation-landscape = liggande (landscape) +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Brev +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Rask nettvising: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nei +pdfjs-document-properties-close-button = Lat att + +## Print + +pdfjs-print-progress-message = Førebur dokumentet for utskrift… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Avbryt +pdfjs-printing-not-supported = Åtvaring: Utskrift er ikkje fullstendig støtta av denne nettlesaren. +pdfjs-printing-not-ready = Åtvaring: PDF ikkje fullstendig innlasta for utskrift. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Slå av/på sidestolpe +pdfjs-toggle-sidebar-notification-button = + .title = Vis/gøym sidestolpe (dokumentet inneheld oversikt/vedlegg/lag) +pdfjs-toggle-sidebar-button-label = Slå av/på sidestolpe +pdfjs-document-outline-button = + .title = Vis dokumentdisposisjonen (dobbelklikk for å utvide/gøyme alle elementa) +pdfjs-document-outline-button-label = Dokumentdisposisjon +pdfjs-attachments-button = + .title = Vis vedlegg +pdfjs-attachments-button-label = Vedlegg +pdfjs-layers-button = + .title = Vis lag (dobbeltklikk for å tilbakestille alle lag til standardtilstand) +pdfjs-layers-button-label = Lag +pdfjs-thumbs-button = + .title = Vis miniatyrbilde +pdfjs-thumbs-button-label = Miniatyrbilde +pdfjs-current-outline-item-button = + .title = Finn gjeldande disposisjonselement +pdfjs-current-outline-item-button-label = Gjeldande disposisjonselement +pdfjs-findbar-button = + .title = Finn i dokumentet +pdfjs-findbar-button-label = Finn +pdfjs-additional-layers = Ytterlegare lag + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Side { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatyrbilde av side { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Søk + .placeholder = Søk i dokument… +pdfjs-find-previous-button = + .title = Finn førre førekomst av frasen +pdfjs-find-previous-button-label = Førre +pdfjs-find-next-button = + .title = Finn neste førekomst av frasen +pdfjs-find-next-button-label = Neste +pdfjs-find-highlight-checkbox = Uthev alle +pdfjs-find-match-case-checkbox-label = Skil store/små bokstavar +pdfjs-find-match-diacritics-checkbox-label = Samsvar diakritiske teikn +pdfjs-find-entire-word-checkbox-label = Heile ord +pdfjs-find-reached-top = Nådde toppen av dokumentet, fortset frå botnen +pdfjs-find-reached-bottom = Nådde botnen av dokumentet, fortset frå toppen +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } av { $total } treff + *[other] { $current } av { $total } treff + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Meir enn { $limit } treff + *[other] Meir enn { $limit } treff + } +pdfjs-find-not-found = Fann ikkje teksten + +## Predefined zoom values + +pdfjs-page-scale-width = Sidebreidde +pdfjs-page-scale-fit = Tilpass til sida +pdfjs-page-scale-auto = Automatisk skalering +pdfjs-page-scale-actual = Verkeleg storleik +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Side { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ein feil oppstod ved lasting av PDF. +pdfjs-invalid-file-error = Ugyldig eller korrupt PDF-fil. +pdfjs-missing-file-error = Manglande PDF-fil. +pdfjs-unexpected-response-error = Uventa tenarrespons. +pdfjs-rendering-error = Ein feil oppstod under vising av sida. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } annotasjon] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Skriv inn passordet for å opne denne PDF-fila. +pdfjs-password-invalid = Ugyldig passord. Prøv på nytt. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Avbryt +pdfjs-web-fonts-disabled = Web-skrifter er slått av: Kan ikkje bruke innbundne PDF-skrifter. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Teikne +pdfjs-editor-ink-button-label = Teikne +pdfjs-editor-stamp-button = + .title = Legg til eller rediger bilde +pdfjs-editor-stamp-button-label = Legg til eller rediger bilde +pdfjs-editor-highlight-button = + .title = Markere +pdfjs-editor-highlight-button-label = Markere +pdfjs-highlight-floating-button1 = + .title = Markere + .aria-label = Markere +pdfjs-highlight-floating-button-label = Markere + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Fjern teikninga +pdfjs-editor-remove-freetext-button = + .title = Fjern tekst +pdfjs-editor-remove-stamp-button = + .title = Fjern bildet +pdfjs-editor-remove-highlight-button = + .title = Fjern utheving + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Farge +pdfjs-editor-free-text-size-input = Storleik +pdfjs-editor-ink-color-input = Farge +pdfjs-editor-ink-thickness-input = Tjukn +pdfjs-editor-ink-opacity-input = Ugjennomskinleg +pdfjs-editor-stamp-add-image-button = + .title = Legg til bilde +pdfjs-editor-stamp-add-image-button-label = Legg til bilde +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tjukn +pdfjs-editor-free-highlight-thickness-title = + .title = Endre tjukn når du markerer andre element enn tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Tekstredigering + .default-content = Begynn å skrive… +pdfjs-free-text = + .aria-label = Tekstredigering +pdfjs-free-text-default-content = Byrje å skrive… +pdfjs-ink = + .aria-label = Teikneredigering +pdfjs-ink-canvas = + .aria-label = Brukarskapt bilde + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt-tekst +pdfjs-editor-alt-text-edit-button = + .aria-label = Rediger alt-tekst tekst +pdfjs-editor-alt-text-edit-button-label = Rediger alt-tekst tekst +pdfjs-editor-alt-text-dialog-label = Vel eit alternativ +pdfjs-editor-alt-text-dialog-description = Alt-tekst (alternativ tekst) hjelper når folk ikkje kan sjå bildet eller når det ikkje vert lasta inn. +pdfjs-editor-alt-text-add-description-label = Legg til ei skildring +pdfjs-editor-alt-text-add-description-description = Gå etter 1-2 setninger som skildrar emnet, settinga eller handlingane. +pdfjs-editor-alt-text-mark-decorative-label = Merk som dekorativt +pdfjs-editor-alt-text-mark-decorative-description = Dette vert brukt til dekorative bilde, som kantlinjer eller vassmerke. +pdfjs-editor-alt-text-cancel-button = Avbryt +pdfjs-editor-alt-text-save-button = Lagre +pdfjs-editor-alt-text-decorative-tooltip = Merkt som dekorativ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Til dømes, «Ein ung mann set seg ved eit bord for å ete eit måltid» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt-tekst + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Øvste venstre hjørne – endre størrelse +pdfjs-editor-resizer-label-top-middle = Øvst i midten — endre størrelse +pdfjs-editor-resizer-label-top-right = Øvste høgre hjørne – endre størrelse +pdfjs-editor-resizer-label-middle-right = Midt til høgre – endre størrelse +pdfjs-editor-resizer-label-bottom-right = Nedste høgre hjørne – endre størrelse +pdfjs-editor-resizer-label-bottom-middle = Nedst i midten — endre størrelse +pdfjs-editor-resizer-label-bottom-left = Nedste venstre hjørne – endre størrelse +pdfjs-editor-resizer-label-middle-left = Midt til venstre — endre størrelse +pdfjs-editor-resizer-top-left = + .aria-label = Øvste venstre hjørne – endre størrelse +pdfjs-editor-resizer-top-middle = + .aria-label = Øvst i midten — endre størrelse +pdfjs-editor-resizer-top-right = + .aria-label = Øvste høgre hjørne – endre størrelse +pdfjs-editor-resizer-middle-right = + .aria-label = Midt til høgre – endre størrelse +pdfjs-editor-resizer-bottom-right = + .aria-label = Nedste høgre hjørne – endre størrelse +pdfjs-editor-resizer-bottom-middle = + .aria-label = Nedst i midten — endre størrelse +pdfjs-editor-resizer-bottom-left = + .aria-label = Nedste venstre hjørne – endre størrelse +pdfjs-editor-resizer-middle-left = + .aria-label = Midt til venstre — endre størrelse + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Uthevingsfarge +pdfjs-editor-colorpicker-button = + .title = Endre farge +pdfjs-editor-colorpicker-dropdown = + .aria-label = Fargeval +pdfjs-editor-colorpicker-yellow = + .title = Gul +pdfjs-editor-colorpicker-green = + .title = Grøn +pdfjs-editor-colorpicker-blue = + .title = Blå +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Raud + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Vis alle +pdfjs-editor-highlight-show-all-button = + .title = Vis alle + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Rediger alternativ tekst (bildeskildring) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Legg til alternativ tekst (bildeskildring) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skriv skildringa di her… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kort skildring for personar som ikkje kan sjå bildet, eller når bildet ikkje lastar inn. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Denne alternative teksten vart oppretta automatisk, og kan vere unøyaktig. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Les meir +pdfjs-editor-new-alt-text-create-automatically-button-label = Opprett alternativ tekt automatisk +pdfjs-editor-new-alt-text-not-now-button = Ikkje no +pdfjs-editor-new-alt-text-error-title = Klarte ikkje å opprette alternativ tekst automatisk +pdfjs-editor-new-alt-text-error-description = Skriv din eigen alternative tekst eller prøv igjen seinare. +pdfjs-editor-new-alt-text-error-close-button = Lat att +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Lastar ned AI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB) + .aria-valuetext = Lastar ned AI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativ tekst lagt til +pdfjs-editor-new-alt-text-added-button-label = Alternativ tekst lagt til +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Manglande alternativ tekst +pdfjs-editor-new-alt-text-missing-button-label = Manglande alternativ tekst +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Vurder alternativ tekst +pdfjs-editor-new-alt-text-to-review-button-label = Vurder alternativ tekst +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Oppretta automatisk: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Alternative tekst-innstillingar for bilde +pdfjs-image-alt-text-settings-button-label = Alternative tekst-innstillingar for bilde +pdfjs-editor-alt-text-settings-dialog-label = Alternative tekst-innstillingar for bilde +pdfjs-editor-alt-text-settings-automatic-title = Automatisk alternativ tekst +pdfjs-editor-alt-text-settings-create-model-button-label = Opprett alternativ tekt automatisk +pdfjs-editor-alt-text-settings-create-model-description = Foreslår skildringar for å hjelpe folk som ikkje kan sjå bildet eller når bildet ikkje blir lasta inn. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = AI-modell for alternativ tekst ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Køyrer lokalt på eininga di slik at dataa dine blir verande private. Påkravd for automatisk alternativ tekst. +pdfjs-editor-alt-text-settings-delete-model-button = Slett +pdfjs-editor-alt-text-settings-download-model-button = Last ned +pdfjs-editor-alt-text-settings-downloading-model-button = Lastar ned… +pdfjs-editor-alt-text-settings-editor-title = Alternativ tekst-redigerar +pdfjs-editor-alt-text-settings-show-dialog-button-label = Vis alternativ tekst-redigerar direkte når du legg til eit bilde +pdfjs-editor-alt-text-settings-show-dialog-description = Hjelper deg med å sørgje for at alle bilda dine har alternativ tekst. +pdfjs-editor-alt-text-settings-close-button = Lat att + +## "Annotations removed" bar + diff --git a/public/assets/pdfjs/locale/oc/viewer.ftl b/public/assets/pdfjs/locale/oc/viewer.ftl new file mode 100644 index 0000000..b347aef --- /dev/null +++ b/public/assets/pdfjs/locale/oc/viewer.ftl @@ -0,0 +1,409 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina precedenta +pdfjs-previous-button-label = Precedent +pdfjs-next-button = + .title = Pagina seguenta +pdfjs-next-button-label = Seguent +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = sus { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom arrièr +pdfjs-zoom-out-button-label = Zoom arrièr +pdfjs-zoom-in-button = + .title = Zoom avant +pdfjs-zoom-in-button-label = Zoom avant +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Bascular en mòde presentacion +pdfjs-presentation-mode-button-label = Mòde Presentacion +pdfjs-open-file-button = + .title = Dobrir lo fichièr +pdfjs-open-file-button-label = Dobrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Enregistrar +pdfjs-save-button-label = Enregistrar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Telecargar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Telecargar +pdfjs-bookmark-button = + .title = Pagina actuala (mostrar l’adreça de la pagina actuala) +pdfjs-bookmark-button-label = Pagina actuala + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Aisinas +pdfjs-tools-button-label = Aisinas +pdfjs-first-page-button = + .title = Anar a la primièra pagina +pdfjs-first-page-button-label = Anar a la primièra pagina +pdfjs-last-page-button = + .title = Anar a la darrièra pagina +pdfjs-last-page-button-label = Anar a la darrièra pagina +pdfjs-page-rotate-cw-button = + .title = Rotacion orària +pdfjs-page-rotate-cw-button-label = Rotacion orària +pdfjs-page-rotate-ccw-button = + .title = Rotacion antiorària +pdfjs-page-rotate-ccw-button-label = Rotacion antiorària +pdfjs-cursor-text-select-tool-button = + .title = Activar l'aisina de seleccion de tèxte +pdfjs-cursor-text-select-tool-button-label = Aisina de seleccion de tèxte +pdfjs-cursor-hand-tool-button = + .title = Activar l’aisina man +pdfjs-cursor-hand-tool-button-label = Aisina man +pdfjs-scroll-page-button = + .title = Activar lo defilament per pagina +pdfjs-scroll-page-button-label = Defilament per pagina +pdfjs-scroll-vertical-button = + .title = Utilizar lo defilament vertical +pdfjs-scroll-vertical-button-label = Defilament vertical +pdfjs-scroll-horizontal-button = + .title = Utilizar lo defilament orizontal +pdfjs-scroll-horizontal-button-label = Defilament orizontal +pdfjs-scroll-wrapped-button = + .title = Activar lo defilament continú +pdfjs-scroll-wrapped-button-label = Defilament continú +pdfjs-spread-none-button = + .title = Agropar pas las paginas doas a doas +pdfjs-spread-none-button-label = Una sola pagina +pdfjs-spread-odd-button = + .title = Mostrar doas paginas en començant per las paginas imparas a esquèrra +pdfjs-spread-odd-button-label = Dobla pagina, impara a drecha +pdfjs-spread-even-button = + .title = Mostrar doas paginas en començant per las paginas paras a esquèrra +pdfjs-spread-even-button-label = Dobla pagina, para a drecha + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Proprietats del document… +pdfjs-document-properties-button-label = Proprietats del document… +pdfjs-document-properties-file-name = Nom del fichièr : +pdfjs-document-properties-file-size = Talha del fichièr : +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } Ko ({ $size_b } octets) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } Mo ({ $size_b } octets) +pdfjs-document-properties-title = Títol : +pdfjs-document-properties-author = Autor : +pdfjs-document-properties-subject = Subjècte : +pdfjs-document-properties-keywords = Mots claus : +pdfjs-document-properties-creation-date = Data de creacion : +pdfjs-document-properties-modification-date = Data de modificacion : +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, a { $time } +pdfjs-document-properties-creator = Creator : +pdfjs-document-properties-producer = Aisina de conversion PDF : +pdfjs-document-properties-version = Version PDF : +pdfjs-document-properties-page-count = Nombre de paginas : +pdfjs-document-properties-page-size = Talha de la pagina : +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = retrach +pdfjs-document-properties-page-size-orientation-landscape = païsatge +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letra +pdfjs-document-properties-page-size-name-legal = Document juridic + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web rapida : +pdfjs-document-properties-linearized-yes = Òc +pdfjs-document-properties-linearized-no = Non +pdfjs-document-properties-close-button = Tampar + +## Print + +pdfjs-print-progress-message = Preparacion del document per l’impression… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Anullar +pdfjs-printing-not-supported = Atencion : l'impression es pas complètament gerida per aqueste navegador. +pdfjs-printing-not-ready = Atencion : lo PDF es pas entièrament cargat per lo poder imprimir. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Afichar/amagar lo panèl lateral +pdfjs-toggle-sidebar-notification-button = + .title = Afichar/amagar lo panèl lateral (lo document conten esquèmas/pèças juntas/calques) +pdfjs-toggle-sidebar-button-label = Afichar/amagar lo panèl lateral +pdfjs-document-outline-button = + .title = Mostrar los esquèmas del document (dobleclicar per espandre/reduire totes los elements) +pdfjs-document-outline-button-label = Marcapaginas del document +pdfjs-attachments-button = + .title = Visualizar las pèças juntas +pdfjs-attachments-button-label = Pèças juntas +pdfjs-layers-button = + .title = Afichar los calques (doble-clicar per reïnicializar totes los calques a l’estat per defaut) +pdfjs-layers-button-label = Calques +pdfjs-thumbs-button = + .title = Afichar las vinhetas +pdfjs-thumbs-button-label = Vinhetas +pdfjs-current-outline-item-button = + .title = Trobar l’element de plan actual +pdfjs-current-outline-item-button-label = Element de plan actual +pdfjs-findbar-button = + .title = Cercar dins lo document +pdfjs-findbar-button-label = Recercar +pdfjs-additional-layers = Calques suplementaris + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Vinheta de la pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Recercar + .placeholder = Cercar dins lo document… +pdfjs-find-previous-button = + .title = Tròba l'ocurréncia precedenta de la frasa +pdfjs-find-previous-button-label = Precedent +pdfjs-find-next-button = + .title = Tròba l'ocurréncia venenta de la frasa +pdfjs-find-next-button-label = Seguent +pdfjs-find-highlight-checkbox = Suslinhar tot +pdfjs-find-match-case-checkbox-label = Respectar la cassa +pdfjs-find-match-diacritics-checkbox-label = Respectar los diacritics +pdfjs-find-entire-word-checkbox-label = Mots entièrs +pdfjs-find-reached-top = Naut de la pagina atenh, perseguida del bas +pdfjs-find-reached-bottom = Bas de la pagina atench, perseguida al començament +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Ocurréncia { $current } de { $total } + *[other] Ocurréncia { $current } de { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mai de { $limit } ocurréncia + *[other] Mai de { $limit } ocurréncias + } +pdfjs-find-not-found = Frasa pas trobada + +## Predefined zoom values + +pdfjs-page-scale-width = Largor plena +pdfjs-page-scale-fit = Pagina entièra +pdfjs-page-scale-auto = Zoom automatic +pdfjs-page-scale-actual = Talha vertadièra +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Una error s'es producha pendent lo cargament del fichièr PDF. +pdfjs-invalid-file-error = Fichièr PDF invalid o corromput. +pdfjs-missing-file-error = Fichièr PDF mancant. +pdfjs-unexpected-response-error = Responsa de servidor imprevista. +pdfjs-rendering-error = Una error s'es producha pendent l'afichatge de la pagina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } a { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotacion { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Picatz lo senhal per dobrir aqueste fichièr PDF. +pdfjs-password-invalid = Senhal incorrècte. Tornatz ensajar. +pdfjs-password-ok-button = D'acòrdi +pdfjs-password-cancel-button = Anullar +pdfjs-web-fonts-disabled = Las polissas web son desactivadas : impossible d'utilizar las polissas integradas al PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tèxte +pdfjs-editor-free-text-button-label = Tèxte +pdfjs-editor-ink-button = + .title = Dessenhar +pdfjs-editor-ink-button-label = Dessenhar +pdfjs-editor-stamp-button = + .title = Apondre o modificar d’imatges +pdfjs-editor-stamp-button-label = Apondre o modificar d’imatges +pdfjs-editor-highlight-button = + .title = Subrelinhar +pdfjs-editor-highlight-button-label = Subrelinhar +pdfjs-highlight-floating-button1 = + .title = Subrelinhar + .aria-label = Subrelinhar +pdfjs-highlight-floating-button-label = Subrelinhar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Levar lo dessenh +pdfjs-editor-remove-freetext-button = + .title = Suprimir lo tèxte +pdfjs-editor-remove-stamp-button = + .title = Suprimir l’imatge +pdfjs-editor-remove-highlight-button = + .title = Levar lo suslinhatge + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Color +pdfjs-editor-free-text-size-input = Talha +pdfjs-editor-ink-color-input = Color +pdfjs-editor-ink-thickness-input = Espessor +pdfjs-editor-ink-opacity-input = Opacitat +pdfjs-editor-stamp-add-image-button = + .title = Apondre imatge +pdfjs-editor-stamp-add-image-button-label = Apondre imatge +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Espessor +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de tèxte + .default-content = Començatz de picar… +pdfjs-free-text = + .aria-label = Editor de tèxte +pdfjs-free-text-default-content = Començatz d’escriure… +pdfjs-ink = + .aria-label = Editor de dessenh +pdfjs-ink-canvas = + .aria-label = Imatge creat per l’utilizaire + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Tèxt alternatiu +pdfjs-editor-alt-text-edit-button-label = Modificar lo tèxt alternatiu +pdfjs-editor-alt-text-dialog-label = Causir una opcion +pdfjs-editor-alt-text-add-description-label = Apondre una descripcion +pdfjs-editor-alt-text-cancel-button = Anullar +pdfjs-editor-alt-text-save-button = Enregistrar + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Color de suslinhatge +pdfjs-editor-colorpicker-button = + .title = Cambiar de color +pdfjs-editor-colorpicker-dropdown = + .aria-label = Causida de colors +pdfjs-editor-colorpicker-yellow = + .title = Jaune +pdfjs-editor-colorpicker-green = + .title = Verd +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Ròse +pdfjs-editor-colorpicker-red = + .title = Roge + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = O afichar tot +pdfjs-editor-highlight-show-all-button = + .title = O afichar tot + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +pdfjs-editor-new-alt-text-error-close-button = Tampar + +## Image alt-text settings + +pdfjs-editor-alt-text-settings-automatic-title = Tèxte alternatiu automatic +pdfjs-editor-alt-text-settings-create-model-button-label = Crear un tèxte alternatiu automaticament +pdfjs-editor-alt-text-settings-delete-model-button = Suprimir +pdfjs-editor-alt-text-settings-download-model-button = Telecargar +pdfjs-editor-alt-text-settings-downloading-model-button = Telecargament… +pdfjs-editor-alt-text-settings-editor-title = Editor de tèxte alternatiu +pdfjs-editor-alt-text-settings-close-button = Tampar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-freetext = Tèxte suprimit +pdfjs-editor-undo-bar-message-ink = Dessenh suprimit +pdfjs-editor-undo-bar-message-stamp = Imatge suprimit +pdfjs-editor-undo-bar-undo-button = + .title = Anullar +pdfjs-editor-undo-bar-undo-button-label = Anullar +pdfjs-editor-undo-bar-close-button = + .title = Tampar +pdfjs-editor-undo-bar-close-button-label = Tampar diff --git a/public/assets/pdfjs/locale/pa-IN/viewer.ftl b/public/assets/pdfjs/locale/pa-IN/viewer.ftl new file mode 100644 index 0000000..10a6112 --- /dev/null +++ b/public/assets/pdfjs/locale/pa-IN/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = ਪਿਛਲਾ ਸਫ਼ਾ +pdfjs-previous-button-label = ਪਿੱਛੇ +pdfjs-next-button = + .title = ਅਗਲਾ ਸਫ਼ਾ +pdfjs-next-button-label = ਅੱਗੇ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ਸਫ਼ਾ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } ਵਿੱਚੋਂ +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = { $pagesCount }) ਵਿੱਚੋਂ ({ $pageNumber } +pdfjs-zoom-out-button = + .title = ਜ਼ੂਮ ਆਉਟ +pdfjs-zoom-out-button-label = ਜ਼ੂਮ ਆਉਟ +pdfjs-zoom-in-button = + .title = ਜ਼ੂਮ ਇਨ +pdfjs-zoom-in-button-label = ਜ਼ੂਮ ਇਨ +pdfjs-zoom-select = + .title = ਜ਼ੂਨ +pdfjs-presentation-mode-button = + .title = ਪਰਿਜੈਂਟੇਸ਼ਨ ਮੋਡ ਵਿੱਚ ਜਾਓ +pdfjs-presentation-mode-button-label = ਪਰਿਜੈਂਟੇਸ਼ਨ ਮੋਡ +pdfjs-open-file-button = + .title = ਫਾਈਲ ਨੂੰ ਖੋਲ੍ਹੋ +pdfjs-open-file-button-label = ਖੋਲ੍ਹੋ +pdfjs-print-button = + .title = ਪਰਿੰਟ +pdfjs-print-button-label = ਪਰਿੰਟ +pdfjs-save-button = + .title = ਸੰਭਾਲੋ +pdfjs-save-button-label = ਸੰਭਾਲੋ +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ਡਾਊਨਲੋਡ +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ਡਾਊਨਲੋਡ +pdfjs-bookmark-button = + .title = ਮੌਜੂਦਾ ਸਫ਼਼ਾ (ਮੌਜੂਦਾ ਸਫ਼ੇ ਤੋਂ URL ਵੇਖੋ) +pdfjs-bookmark-button-label = ਮੌਜੂਦਾ ਸਫ਼਼ਾ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ਟੂਲ +pdfjs-tools-button-label = ਟੂਲ +pdfjs-first-page-button = + .title = ਪਹਿਲੇ ਸਫ਼ੇ ਉੱਤੇ ਜਾਓ +pdfjs-first-page-button-label = ਪਹਿਲੇ ਸਫ਼ੇ ਉੱਤੇ ਜਾਓ +pdfjs-last-page-button = + .title = ਆਖਰੀ ਸਫ਼ੇ ਉੱਤੇ ਜਾਓ +pdfjs-last-page-button-label = ਆਖਰੀ ਸਫ਼ੇ ਉੱਤੇ ਜਾਓ +pdfjs-page-rotate-cw-button = + .title = ਸੱਜੇ ਦਾਅ ਘੁੰਮਾਓ +pdfjs-page-rotate-cw-button-label = ਸੱਜੇ ਦਾਅ ਘੁੰਮਾਓ +pdfjs-page-rotate-ccw-button = + .title = ਖੱਬੇ ਦਾਅ ਘੁੰਮਾਓ +pdfjs-page-rotate-ccw-button-label = ਖੱਬੇ ਦਾਅ ਘੁੰਮਾਓ +pdfjs-cursor-text-select-tool-button = + .title = ਲਿਖਤ ਚੋਣ ਟੂਲ ਸਮਰੱਥ ਕਰੋ +pdfjs-cursor-text-select-tool-button-label = ਲਿਖਤ ਚੋਣ ਟੂਲ +pdfjs-cursor-hand-tool-button = + .title = ਹੱਥ ਟੂਲ ਸਮਰੱਥ ਕਰੋ +pdfjs-cursor-hand-tool-button-label = ਹੱਥ ਟੂਲ +pdfjs-scroll-page-button = + .title = ਸਫ਼ਾ ਖਿਸਕਾਉਣ ਨੂੰ ਵਰਤੋਂ +pdfjs-scroll-page-button-label = ਸਫ਼ਾ ਖਿਸਕਾਉਣਾ +pdfjs-scroll-vertical-button = + .title = ਖੜ੍ਹਵੇਂ ਸਕਰਾਉਣ ਨੂੰ ਵਰਤੋਂ +pdfjs-scroll-vertical-button-label = ਖੜ੍ਹਵਾਂ ਸਰਕਾਉਣਾ +pdfjs-scroll-horizontal-button = + .title = ਲੇਟਵੇਂ ਸਰਕਾਉਣ ਨੂੰ ਵਰਤੋਂ +pdfjs-scroll-horizontal-button-label = ਲੇਟਵਾਂ ਸਰਕਾਉਣਾ +pdfjs-scroll-wrapped-button = + .title = ਸਮੇਟੇ ਸਰਕਾਉਣ ਨੂੰ ਵਰਤੋਂ +pdfjs-scroll-wrapped-button-label = ਸਮੇਟਿਆ ਸਰਕਾਉਣਾ +pdfjs-spread-none-button = + .title = ਸਫ਼ਾ ਫੈਲਾਅ ਵਿੱਚ ਸ਼ਾਮਲ ਨਾ ਹੋਵੋ +pdfjs-spread-none-button-label = ਕੋਈ ਫੈਲਾਅ ਨਹੀਂ +pdfjs-spread-odd-button = + .title = ਟਾਂਕ ਅੰਕ ਵਾਲੇ ਸਫ਼ਿਆਂ ਨਾਲ ਸ਼ੁਰੂ ਹੋਣ ਵਾਲੇ ਸਫਿਆਂ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ +pdfjs-spread-odd-button-label = ਟਾਂਕ ਫੈਲਾਅ +pdfjs-spread-even-button = + .title = ਜਿਸਤ ਅੰਕ ਵਾਲੇ ਸਫ਼ਿਆਂ ਨਾਲ ਸ਼ੁਰੂ ਹੋਣ ਵਾਲੇ ਸਫਿਆਂ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ +pdfjs-spread-even-button-label = ਜਿਸਤ ਫੈਲਾਅ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = …ਦਸਤਾਵੇਜ਼ ਦੀ ਵਿਸ਼ੇਸ਼ਤਾ +pdfjs-document-properties-button-label = …ਦਸਤਾਵੇਜ਼ ਦੀ ਵਿਸ਼ੇਸ਼ਤਾ +pdfjs-document-properties-file-name = ਫਾਈਲ ਦਾ ਨਾਂ: +pdfjs-document-properties-file-size = ਫਾਈਲ ਦਾ ਆਕਾਰ: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } ਬਾਈਟ) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } ਬਾਈਟ) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } ਬਾਈਟ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } ਬਾਈਟ) +pdfjs-document-properties-title = ਟਾਈਟਲ: +pdfjs-document-properties-author = ਲੇਖਕ: +pdfjs-document-properties-subject = ਵਿਸ਼ਾ: +pdfjs-document-properties-keywords = ਸ਼ਬਦ: +pdfjs-document-properties-creation-date = ਬਣਾਉਣ ਦੀ ਮਿਤੀ: +pdfjs-document-properties-modification-date = ਸੋਧ ਦੀ ਮਿਤੀ: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ਨਿਰਮਾਤਾ: +pdfjs-document-properties-producer = PDF ਪ੍ਰੋਡਿਊਸਰ: +pdfjs-document-properties-version = PDF ਵਰਜਨ: +pdfjs-document-properties-page-count = ਸਫ਼ੇ ਦੀ ਗਿਣਤੀ: +pdfjs-document-properties-page-size = ਸਫ਼ਾ ਆਕਾਰ: +pdfjs-document-properties-page-size-unit-inches = ਇੰਚ +pdfjs-document-properties-page-size-unit-millimeters = ਮਿਮੀ +pdfjs-document-properties-page-size-orientation-portrait = ਪੋਰਟਰੇਟ +pdfjs-document-properties-page-size-orientation-landscape = ਲੈਂਡਸਕੇਪ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = ਲੈਟਰ +pdfjs-document-properties-page-size-name-legal = ਕਨੂੰਨੀ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = ਤੇਜ਼ ਵੈੱਬ ਝਲਕ: +pdfjs-document-properties-linearized-yes = ਹਾਂ +pdfjs-document-properties-linearized-no = ਨਹੀਂ +pdfjs-document-properties-close-button = ਬੰਦ ਕਰੋ + +## Print + +pdfjs-print-progress-message = …ਪਰਿੰਟ ਕਰਨ ਲਈ ਦਸਤਾਵੇਜ਼ ਨੂੰ ਤਿਆਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ਰੱਦ ਕਰੋ +pdfjs-printing-not-supported = ਸਾਵਧਾਨ: ਇਹ ਬਰਾਊਜ਼ਰ ਪਰਿੰਟ ਕਰਨ ਲਈ ਪੂਰੀ ਤਰ੍ਹਾਂ ਸਹਾਇਕ ਨਹੀਂ ਹੈ। +pdfjs-printing-not-ready = ਸਾਵਧਾਨ: PDF ਨੂੰ ਪਰਿੰਟ ਕਰਨ ਲਈ ਪੂਰੀ ਤਰ੍ਹਾਂ ਲੋਡ ਨਹੀਂ ਹੈ। + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ਬਾਹੀ ਬਦਲੋ +pdfjs-toggle-sidebar-notification-button = + .title = ਬਾਹੀ ਨੂੰ ਬਦਲੋ (ਦਸਤਾਵੇਜ਼ ਖਾਕਾ/ਅਟੈਚਮੈਂਟ/ਪਰਤਾਂ ਰੱਖਦਾ ਹੈ) +pdfjs-toggle-sidebar-button-label = ਬਾਹੀ ਬਦਲੋ +pdfjs-document-outline-button = + .title = ਦਸਤਾਵੇਜ਼ ਖਾਕਾ ਦਿਖਾਓ (ਸਾਰੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਫੈਲਾਉਣ/ਸਮੇਟਣ ਲਈ ਦੋ ਵਾਰ ਕਲਿੱਕ ਕਰੋ) +pdfjs-document-outline-button-label = ਦਸਤਾਵੇਜ਼ ਖਾਕਾ +pdfjs-attachments-button = + .title = ਅਟੈਚਮੈਂਟ ਵੇਖਾਓ +pdfjs-attachments-button-label = ਅਟੈਚਮੈਂਟਾਂ +pdfjs-layers-button = + .title = ਪਰਤਾਂ ਵੇਖਾਓ (ਸਾਰੀਆਂ ਪਰਤਾਂ ਨੂੰ ਮੂਲ ਹਾਲਤ ਉੱਤੇ ਮੁੜ-ਸੈੱਟ ਕਰਨ ਲਈ ਦੋ ਵਾਰ ਕਲਿੱਕ ਕਰੋ) +pdfjs-layers-button-label = ਪਰਤਾਂ +pdfjs-thumbs-button = + .title = ਥੰਮਨੇਲ ਨੂੰ ਵੇਖਾਓ +pdfjs-thumbs-button-label = ਥੰਮਨੇਲ +pdfjs-current-outline-item-button = + .title = ਮੌੌਜੂਦਾ ਖਾਕਾ ਚੀਜ਼ ਲੱਭੋ +pdfjs-current-outline-item-button-label = ਮੌਜੂਦਾ ਖਾਕਾ ਚੀਜ਼ +pdfjs-findbar-button = + .title = ਦਸਤਾਵੇਜ਼ ਵਿੱਚ ਲੱਭੋ +pdfjs-findbar-button-label = ਲੱਭੋ +pdfjs-additional-layers = ਵਾਧੂ ਪਰਤਾਂ + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = ਸਫ਼ਾ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } ਸਫ਼ੇ ਦਾ ਥੰਮਨੇਲ + +## Find panel button title and messages + +pdfjs-find-input = + .title = ਲੱਭੋ + .placeholder = …ਦਸਤਾਵੇਜ਼ 'ਚ ਲੱਭੋ +pdfjs-find-previous-button = + .title = ਵਾਕ ਦੀ ਪਿਛਲੀ ਮੌਜੂਦਗੀ ਲੱਭੋ +pdfjs-find-previous-button-label = ਪਿੱਛੇ +pdfjs-find-next-button = + .title = ਵਾਕ ਦੀ ਅਗਲੀ ਮੌਜੂਦਗੀ ਲੱਭੋ +pdfjs-find-next-button-label = ਅੱਗੇ +pdfjs-find-highlight-checkbox = ਸਭ ਉਭਾਰੋ +pdfjs-find-match-case-checkbox-label = ਅੱਖਰ ਆਕਾਰ ਨੂੰ ਮਿਲਾਉ +pdfjs-find-match-diacritics-checkbox-label = ਭੇਦਸੂਚਕ ਮੇਲ +pdfjs-find-entire-word-checkbox-label = ਪੂਰੇ ਸ਼ਬਦ +pdfjs-find-reached-top = ਦਸਤਾਵੇਜ਼ ਦੇ ਉੱਤੇ ਆ ਗਏ ਹਾਂ, ਥੱਲੇ ਤੋਂ ਜਾਰੀ ਰੱਖਿਆ ਹੈ +pdfjs-find-reached-bottom = ਦਸਤਾਵੇਜ਼ ਦੇ ਅੰਤ ਉੱਤੇ ਆ ਗਏ ਹਾਂ, ਉੱਤੇ ਤੋਂ ਜਾਰੀ ਰੱਖਿਆ ਹੈ +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $total } ਵਿੱਚੋਂ { $current } ਮੇਲ + *[other] { $total } ਵਿੱਚੋਂ { $current } ਮੇਲ + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] { $limit } ਤੋਂ ਵੱਧ ਮੇਲ + *[other] { $limit } ਤੋਂ ਵੱਧ ਮੇਲ + } +pdfjs-find-not-found = ਵਾਕ ਨਹੀਂ ਲੱਭਿਆ + +## Predefined zoom values + +pdfjs-page-scale-width = ਸਫ਼ੇ ਦੀ ਚੌੜਾਈ +pdfjs-page-scale-fit = ਸਫ਼ਾ ਫਿੱਟ +pdfjs-page-scale-auto = ਆਟੋਮੈਟਿਕ ਜ਼ੂਮ ਕਰੋ +pdfjs-page-scale-actual = ਆਟੋਮੈਟਿਕ ਆਕਾਰ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = ਸਫ਼ਾ { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF ਲੋਡ ਕਰਨ ਦੇ ਦੌਰਾਨ ਗਲਤੀ ਆਈ ਹੈ। +pdfjs-invalid-file-error = ਗਲਤ ਜਾਂ ਨਿਕਾਰਾ PDF ਫਾਈਲ ਹੈ। +pdfjs-missing-file-error = ਨਾ-ਮੌਜੂਦ PDF ਫਾਈਲ। +pdfjs-unexpected-response-error = ਅਣਜਾਣ ਸਰਵਰ ਜਵਾਬ। +pdfjs-rendering-error = ਸਫ਼ਾ ਰੈਡਰ ਕਰਨ ਦੇ ਦੌਰਾਨ ਗਲਤੀ ਆਈ ਹੈ। + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ਵਿਆਖਿਆ] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = ਇਹ PDF ਫਾਈਲ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਪਾਸਵਰਡ ਦਿਉ। +pdfjs-password-invalid = ਗਲਤ ਪਾਸਵਰਡ। ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ। +pdfjs-password-ok-button = ਠੀਕ ਹੈ +pdfjs-password-cancel-button = ਰੱਦ ਕਰੋ +pdfjs-web-fonts-disabled = ਵੈਬ ਫੋਂਟ ਬੰਦ ਹਨ: ਇੰਬੈਡ PDF ਫੋਂਟ ਨੂੰ ਵਰਤਣ ਲਈ ਅਸਮਰੱਥ ਹੈ। + +## Editing + +pdfjs-editor-free-text-button = + .title = ਲਿਖਤ +pdfjs-editor-free-text-button-label = ਲਿਖਤ +pdfjs-editor-ink-button = + .title = ਵਾਹੋ +pdfjs-editor-ink-button-label = ਵਾਹੋ +pdfjs-editor-stamp-button = + .title = ਚਿੱਤਰ ਜੋੜੋ ਜਾਂ ਸੋਧੋ +pdfjs-editor-stamp-button-label = ਚਿੱਤਰ ਜੋੜੋ ਜਾਂ ਸੋਧੋ +pdfjs-editor-highlight-button = + .title = ਹਾਈਲਾਈਟ +pdfjs-editor-highlight-button-label = ਹਾਈਲਾਈਟ +pdfjs-highlight-floating-button1 = + .title = ਹਾਈਲਾਈਟ + .aria-label = ਹਾਈਲਾਈਟ +pdfjs-highlight-floating-button-label = ਹਾਈਲਾਈਟ + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = ਡਰਾਇੰਗ ਨੂੰ ਹਟਾਓ +pdfjs-editor-remove-freetext-button = + .title = ਲਿਖਤ ਨੂੰ ਹਟਾਓ +pdfjs-editor-remove-stamp-button = + .title = ਚਿੱਤਰ ਨੂੰ ਹਟਾਓ +pdfjs-editor-remove-highlight-button = + .title = ਹਾਈਲਾਈਟ ਨੂੰ ਹਟਾਓ + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = ਰੰਗ +pdfjs-editor-free-text-size-input = ਆਕਾਰ +pdfjs-editor-ink-color-input = ਰੰਗ +pdfjs-editor-ink-thickness-input = ਮੋਟਾਈ +pdfjs-editor-ink-opacity-input = ਧੁੰਦਲਾਪਨ +pdfjs-editor-stamp-add-image-button = + .title = ਚਿੱਤਰ ਜੋੜੋ +pdfjs-editor-stamp-add-image-button-label = ਚਿੱਤਰ ਜੋੜੋ +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = ਮੋਟਾਈ +pdfjs-editor-free-highlight-thickness-title = + .title = ਚੀਜ਼ਾਂ ਨੂੰ ਹੋਰ ਲਿਖਤਾਂ ਤੋਂ ਉਘਾੜਨ ਸਮੇਂ ਮੋਟਾਈ ਨੂੰ ਬਦਲੋ +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = ਲਿਖਤ ਐਡੀਟਰ + .default-content = …ਲਿਖਣਾ ਸ਼ੁਰੂ ਕਰੋ +pdfjs-free-text = + .aria-label = ਲਿਖਤ ਐਡੀਟਰ +pdfjs-free-text-default-content = …ਲਿਖਣਾ ਸ਼ੁਰੂ ਕਰੋ +pdfjs-ink = + .aria-label = ਵਹਾਉਣ ਐਡੀਟਰ +pdfjs-ink-canvas = + .aria-label = ਵਰਤੋਂਕਾਰ ਵਲੋਂ ਬਣਾਇਆ ਚਿੱਤਰ + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = ਬਦਲਵੀਂ ਲਿਖਤ +pdfjs-editor-alt-text-edit-button = + .aria-label = ਬਦਲਵੀ ਲਿਖਤ ਨੂੰ ਸੋਧੋ +pdfjs-editor-alt-text-edit-button-label = ਬਦਲਵੀ ਲਿਖਤ ਨੂੰ ਸੋਧੋ +pdfjs-editor-alt-text-dialog-label = ਚੋਣ ਕਰੋ +pdfjs-editor-alt-text-dialog-description = ਚਿੱਤਰ ਨਾ ਦਿੱਸਣ ਜਾਂ ਲੋਡ ਨਾ ਹੋਣ ਦੀ ਹਾਲਤ ਵਿੱਚ Alt ਲਿਖਤ (ਬਦਲਵੀਂ ਲਿਖਤ) ਲੋਕਾਂ ਲਈ ਮਦਦਗਾਰ ਹੁੰਦੀ ਹੈ। +pdfjs-editor-alt-text-add-description-label = ਵਰਣਨ ਜੋੜੋ +pdfjs-editor-alt-text-add-description-description = 1-2 ਵਾਕ ਰੱਖੋ, ਜੋ ਕਿ ਵਿਸ਼ੇ, ਸੈਟਿੰਗ ਜਾਂ ਕਾਰਵਾਈਆਂ ਬਾਰੇ ਦਰਸਾਉਂਦੇ ਹੋਣ। +pdfjs-editor-alt-text-mark-decorative-label = ਸਜਾਵਟ ਵਜੋਂ ਨਿਸ਼ਾਨ ਲਾਇਆ +pdfjs-editor-alt-text-mark-decorative-description = ਇਸ ਨੂੰ ਸਜਾਵਟੀ ਚਿੱਤਰਾਂ ਲਈ ਵਰਤਿਆ ਜਾਂਦਾ ਹੈ ਜਿਵੇਂ ਕਿ ਹਾਸ਼ੀਆ ਜਾਂ ਵਾਟਰਮਾਰਕ ਆਦਿ। +pdfjs-editor-alt-text-cancel-button = ਰੱਦ ਕਰੋ +pdfjs-editor-alt-text-save-button = ਸੰਭਾਲੋ +pdfjs-editor-alt-text-decorative-tooltip = ਸਜਾਵਟ ਵਜੋਂ ਨਿਸ਼ਾਨ ਲਾਓ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = ਮਿਸਾਲ ਵਜੋਂ, “ਗੱਭਰੂ ਭੋਜਨ ਲੈ ਕੇ ਮੇਜ਼ ਉੱਤੇ ਬੈਠਾ ਹੈ” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = ਬਦਲਵੀਂ ਲਿਖਤ + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = ਉੱਤੇ ਖੱਬਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-top-middle = ਉੱਤੇ ਮੱਧ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-top-right = ਉੱਤੇ ਸੱਜਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-middle-right = ਮੱਧ ਸੱਜਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-bottom-right = ਹੇਠਾਂ ਸੱਜਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-bottom-middle = ਹੇਠਾਂ ਮੱਧ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-bottom-left = ਹੇਠਾਂ ਖੱਬਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-label-middle-left = ਮੱਧ ਖੱਬਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-top-left = + .aria-label = ਉੱਤੇ ਖੱਬਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-top-middle = + .aria-label = ਉੱਤੇ ਮੱਧ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-top-right = + .aria-label = ਉੱਤੇ ਸੱਜਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-middle-right = + .aria-label = ਮੱਧ ਸੱਜਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-bottom-right = + .aria-label = ਹੇਠਾਂ ਸੱਜਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-bottom-middle = + .aria-label = ਹੇਠਾਂ ਮੱਧ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-bottom-left = + .aria-label = ਹੇਠਾਂ ਖੱਬਾ ਕੋਨਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ +pdfjs-editor-resizer-middle-left = + .aria-label = ਮੱਧ ਖੱਬਾ — ਮੁੜ-ਆਕਾਰ ਕਰੋ + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = ਹਾਈਟਲਾਈਟ ਦਾ ਰੰਗ +pdfjs-editor-colorpicker-button = + .title = ਰੰਗ ਨੂੰ ਬਦਲੋ +pdfjs-editor-colorpicker-dropdown = + .aria-label = ਰੰਗ ਚੋਣਾਂ +pdfjs-editor-colorpicker-yellow = + .title = ਪੀਲਾ +pdfjs-editor-colorpicker-green = + .title = ਹਰਾ +pdfjs-editor-colorpicker-blue = + .title = ਨੀਲਾ +pdfjs-editor-colorpicker-pink = + .title = ਗੁਲਾਬੀ +pdfjs-editor-colorpicker-red = + .title = ਲਾਲ + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = ਸਭ ਵੇਖੋ +pdfjs-editor-highlight-show-all-button = + .title = ਸਭ ਵੇਖੋ + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = ਬਦਲਵੀਂ ਲਿਖਤ (ਚਿੱਤਰ ਦਾ ਵਰਣਨ) ਨੂੰ ਸੋਧੋ +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = ਬਦਲਵੀਂ ਲਿਖਤ (ਚਿੱਤਰ ਦਾ ਵਰਣਨ) ਨੂੰ ਜੋੜੋ +pdfjs-editor-new-alt-text-textarea = + .placeholder = …ਆਪਣਾ ਵਰਣਨਾ ਇੱਥੇ ਲਿਖੋ +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = ਲੋਕ, ਜੋ ਕਿ ਚਿੱਤਰ ਨਹੀਂ ਵੇਖ ਸਕਦੇ ਜਾਂ ਜਦ ਵੀ ਚਿੱਤਰਾਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਜਾ ਸਕਦਾ, ਉਸ ਲਈ ਛੋਟਾ ਵੇਰਵਾ ਦਿਓ। +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = ਇਹ ਬਦਲਵੀਂ ਲਿਖਤ ਆਪਣੇ-ਆਪ ਤਿਆਰ ਕੀਤੀ ਗਈ ਸੀ ਅਤੇ ਗਲਤ ਵੀ ਹੋ ਸਕਦੀ ਹੈ। +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = ਹੋਰ ਜਾਣੋ +pdfjs-editor-new-alt-text-create-automatically-button-label = ਬਲਦਵੀਂ ਲਿਖਤ ਆਪਣੇ-ਆਪ ਬਣਾਓ +pdfjs-editor-new-alt-text-not-now-button = ਹੁਣੇ ਨਹੀਂ +pdfjs-editor-new-alt-text-error-title = ਬਦਲਵੀਂ ਲਿਖਤ ਆਪਣੇ-ਆਪ ਬਣਾਈ ਨਹੀਂ ਜਾ ਸਕੀ +pdfjs-editor-new-alt-text-error-description = ਆਪਣਾ ਖੁਦ ਦੀ ਬਦਲਵੀਂ ਲਿਖਤ ਲਿਖੋ ਜਾਂ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ। +pdfjs-editor-new-alt-text-error-close-button = ਬੰਦ ਕਰੋ +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = ਬਦਲਵਾਂ ਲਿਖਤ AI ਮਾਡਲ ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ ({ $totalSize } MB ਵਿੱਚੋਂ { $downloadedSize }) + .aria-valuetext = ਬਦਲਵਾਂ ਲਿਖਤ AI ਮਾਡਲ ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ ({ $totalSize } MB ਵਿੱਚੋਂ { $downloadedSize }) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = ਬਦਲਵੀਂ ਲਿਖਤ ਜੋੜੀ +pdfjs-editor-new-alt-text-added-button-label = ਬਦਲਵੀਂ ਲਿਖਤ ਜੋੜੀ +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = ਬਦਲਵਾਂ ਲਿਖਤ ਗੁੰਮ ਹੈ +pdfjs-editor-new-alt-text-missing-button-label = ਬਦਲਵਾਂ ਲਿਖਤ ਗੁੰਮ ਹੈ +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = ਬਦਲਵੀਂ ਲਿਖਤ ਦਾ ਰੀਵਿਊ ਕਰੋ +pdfjs-editor-new-alt-text-to-review-button-label = ਬਦਲਵੀਂ ਲਿਖਤ ਦਾ ਰੀਵਿਊ ਕਰੋ +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = ਆਪਣੇ-ਆਪ ਬਣਾਇਆ: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = ਚਿੱਤਰ ਬਦਲਵੀਂ ਲਿਖਤ ਦੀਆਂ ਸੈਟਿੰਗਾਂ +pdfjs-image-alt-text-settings-button-label = ਚਿੱਤਰ ਬਦਲਵੀਂ ਲਿਖਤ ਦੀਆਂ ਸੈਟਿੰਗਾਂ +pdfjs-editor-alt-text-settings-dialog-label = ਚਿੱਤਰ ਬਦਲਵੀਂ ਲਿਖਤ ਦੀਆਂ ਸੈਟਿੰਗਾਂ +pdfjs-editor-alt-text-settings-automatic-title = ਆਟੋਮਮੈਟਿਕ ਬਦਲਵੀਂ ਲਿਖਤ +pdfjs-editor-alt-text-settings-create-model-button-label = ਬਲਦਵੀਂ ਲਿਖਤ ਆਪਣੇ-ਆਪ ਬਣਾਓ +pdfjs-editor-alt-text-settings-create-model-description = ਚਿੱਤਰ ਨਾ ਵੇਖ ਸਕਣ ਵਾਲੇ ਲੋਕਾਂ ਦੀ ਮਦਦ ਜਾਂ ਜਦ ਵੀ ਚਿੱਤਰਾਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਜਾ ਸਕਦਾ, ਉਸ ਲਈ ਛੋਟਾ ਵੇਰਵਾ ਦਿਓ। +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = ਬਦਲਵੀ ਲਿਖਤ ਲਈ AI ਮਾਡਲ ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਉੱਤੇ ਲੋਕਲ ਹੀ ਚੱਲਦਾ ਹੋਣ ਕਰਕੇ ਤੁਹਾਡਾ ਡਾਟਾ ਪ੍ਰਾਈਵੇਟ ਹੀ ਰਹਿੰਦਾ ਹੈ। ਆਟੋਮੈਟਿਕ ਬਦਲਵੀਂ ਲਿਖਤ ਲਈ ਚਾਹੀਦਾ ਹੈ। +pdfjs-editor-alt-text-settings-delete-model-button = ਹਟਾਓ +pdfjs-editor-alt-text-settings-download-model-button = ਡਾਊਨਲੋਡ +pdfjs-editor-alt-text-settings-downloading-model-button = …ਨੂੰ ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ +pdfjs-editor-alt-text-settings-editor-title = ਬਦਲਵੀਂ ਲਿਖਤ ਐਡੀਟਰ +pdfjs-editor-alt-text-settings-show-dialog-button-label = ਜਦੋਂ ਵਿੱਚ ਚਿੱਤਰ ਜੋੜਿਆ ਜਾਵੇ ਤਾਂ ਫ਼ੌਰਨ ਬਦਲਵੀ ਲਿਖਤ ਸੰਪਾਦਕ ਵੇਖਾਓ +pdfjs-editor-alt-text-settings-show-dialog-description = ਤੁਹਾਡੀ ਮਦਦ ਕਰਦਾ ਹੈ ਕਿ ਤੁਹਾਡੇ ਸਾਰੇ ਚਿੱਤਰਾਂ ਲਈ ਬਦਲਵੀਂ ਲਿਖਤ ਮੌਜੂਦ ਹੋਵੇ। +pdfjs-editor-alt-text-settings-close-button = ਬੰਦ ਕਰੋ + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = ਹਾਈਲਾਈਟ ਨੂੰ ਹਟਾਇਆ ਗਿਆ +pdfjs-editor-undo-bar-message-freetext = ਲਿਖਤ ਨੂੰ ਹਟਾਇਆ ਗਿਆ +pdfjs-editor-undo-bar-message-ink = ਡਰਾਇੰਗ ਨੂੰ ਹਟਾਇਆ ਗਿਆ +pdfjs-editor-undo-bar-message-stamp = ਚਿੱਤਰ ਨੂੰ ਹਟਾਇਆ ਗਿਆ +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } ਵਿਆਖਿਆ ਨੂੰ ਹਟਾਇਆ + *[other] { $count } ਵਿਆਖਿਆਵਾਂ ਨੂੰ ਹਟਾਇਆ + } +pdfjs-editor-undo-bar-undo-button = + .title = ਵਾਪਸ +pdfjs-editor-undo-bar-undo-button-label = ਵਾਪਸ +pdfjs-editor-undo-bar-close-button = + .title = ਬੰਦ ਕਰੋ +pdfjs-editor-undo-bar-close-button-label = ਬੰਦ ਕਰੋ diff --git a/public/assets/pdfjs/locale/pl/viewer.ftl b/public/assets/pdfjs/locale/pl/viewer.ftl new file mode 100644 index 0000000..07f9416 --- /dev/null +++ b/public/assets/pdfjs/locale/pl/viewer.ftl @@ -0,0 +1,518 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Poprzednia strona +pdfjs-previous-button-label = Poprzednia +pdfjs-next-button = + .title = Następna strona +pdfjs-next-button-label = Następna +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Strona +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = z { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } z { $pagesCount }) +pdfjs-zoom-out-button = + .title = Pomniejsz +pdfjs-zoom-out-button-label = Pomniejsz +pdfjs-zoom-in-button = + .title = Powiększ +pdfjs-zoom-in-button-label = Powiększ +pdfjs-zoom-select = + .title = Skala +pdfjs-presentation-mode-button = + .title = Przełącz na tryb prezentacji +pdfjs-presentation-mode-button-label = Tryb prezentacji +pdfjs-open-file-button = + .title = Otwórz plik +pdfjs-open-file-button-label = Otwórz +pdfjs-print-button = + .title = Drukuj +pdfjs-print-button-label = Drukuj +pdfjs-save-button = + .title = Zapisz +pdfjs-save-button-label = Zapisz +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Pobierz +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Pobierz +pdfjs-bookmark-button = + .title = Bieżąca strona (adres do otwarcia na bieżącej stronie) +pdfjs-bookmark-button-label = Bieżąca strona + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Narzędzia +pdfjs-tools-button-label = Narzędzia +pdfjs-first-page-button = + .title = Przejdź do pierwszej strony +pdfjs-first-page-button-label = Przejdź do pierwszej strony +pdfjs-last-page-button = + .title = Przejdź do ostatniej strony +pdfjs-last-page-button-label = Przejdź do ostatniej strony +pdfjs-page-rotate-cw-button = + .title = Obróć zgodnie z ruchem wskazówek zegara +pdfjs-page-rotate-cw-button-label = Obróć zgodnie z ruchem wskazówek zegara +pdfjs-page-rotate-ccw-button = + .title = Obróć przeciwnie do ruchu wskazówek zegara +pdfjs-page-rotate-ccw-button-label = Obróć przeciwnie do ruchu wskazówek zegara +pdfjs-cursor-text-select-tool-button = + .title = Włącz narzędzie zaznaczania tekstu +pdfjs-cursor-text-select-tool-button-label = Narzędzie zaznaczania tekstu +pdfjs-cursor-hand-tool-button = + .title = Włącz narzędzie rączka +pdfjs-cursor-hand-tool-button-label = Narzędzie rączka +pdfjs-scroll-page-button = + .title = Przewijaj strony +pdfjs-scroll-page-button-label = Przewijanie stron +pdfjs-scroll-vertical-button = + .title = Przewijaj dokument w pionie +pdfjs-scroll-vertical-button-label = Przewijanie pionowe +pdfjs-scroll-horizontal-button = + .title = Przewijaj dokument w poziomie +pdfjs-scroll-horizontal-button-label = Przewijanie poziome +pdfjs-scroll-wrapped-button = + .title = Strony dokumentu wyświetlaj i przewijaj w kolumnach +pdfjs-scroll-wrapped-button-label = Widok dwóch stron +pdfjs-spread-none-button = + .title = Nie ustawiaj stron obok siebie +pdfjs-spread-none-button-label = Brak kolumn +pdfjs-spread-odd-button = + .title = Strony nieparzyste ustawiaj na lewo od parzystych +pdfjs-spread-odd-button-label = Nieparzyste po lewej +pdfjs-spread-even-button = + .title = Strony parzyste ustawiaj na lewo od nieparzystych +pdfjs-spread-even-button-label = Parzyste po lewej + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Właściwości dokumentu… +pdfjs-document-properties-button-label = Właściwości dokumentu… +pdfjs-document-properties-file-name = Nazwa pliku: +pdfjs-document-properties-file-size = Rozmiar pliku: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } B) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } B) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } B) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } B) +pdfjs-document-properties-title = Tytuł: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Temat: +pdfjs-document-properties-keywords = Słowa kluczowe: +pdfjs-document-properties-creation-date = Data utworzenia: +pdfjs-document-properties-modification-date = Data modyfikacji: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Utworzony przez: +pdfjs-document-properties-producer = PDF wyprodukowany przez: +pdfjs-document-properties-version = Wersja PDF: +pdfjs-document-properties-page-count = Liczba stron: +pdfjs-document-properties-page-size = Wymiary strony: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = pionowa +pdfjs-document-properties-page-size-orientation-landscape = pozioma +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = US Letter +pdfjs-document-properties-page-size-name-legal = US Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width }×{ $height } { $unit } (orientacja { $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width }×{ $height } { $unit } ({ $name }, orientacja { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Szybki podgląd w Internecie: +pdfjs-document-properties-linearized-yes = tak +pdfjs-document-properties-linearized-no = nie +pdfjs-document-properties-close-button = Zamknij + +## Print + +pdfjs-print-progress-message = Przygotowywanie dokumentu do druku… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Anuluj +pdfjs-printing-not-supported = Ostrzeżenie: drukowanie nie jest w pełni obsługiwane przez tę przeglądarkę. +pdfjs-printing-not-ready = Ostrzeżenie: dokument PDF nie jest całkowicie wczytany, więc nie można go wydrukować. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Przełącz panel boczny +pdfjs-toggle-sidebar-notification-button = + .title = Przełącz panel boczny (dokument zawiera konspekt/załączniki/warstwy) +pdfjs-toggle-sidebar-button-label = Przełącz panel boczny +pdfjs-document-outline-button = + .title = Konspekt dokumentu (podwójne kliknięcie rozwija lub zwija wszystkie pozycje) +pdfjs-document-outline-button-label = Konspekt dokumentu +pdfjs-attachments-button = + .title = Załączniki +pdfjs-attachments-button-label = Załączniki +pdfjs-layers-button = + .title = Warstwy (podwójne kliknięcie przywraca wszystkie warstwy do stanu domyślnego) +pdfjs-layers-button-label = Warstwy +pdfjs-thumbs-button = + .title = Miniatury +pdfjs-thumbs-button-label = Miniatury +pdfjs-current-outline-item-button = + .title = Znajdź bieżący element konspektu +pdfjs-current-outline-item-button-label = Bieżący element konspektu +pdfjs-findbar-button = + .title = Znajdź w dokumencie +pdfjs-findbar-button-label = Znajdź +pdfjs-additional-layers = Dodatkowe warstwy + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page }. strona +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura { $page }. strony + +## Find panel button title and messages + +pdfjs-find-input = + .title = Znajdź + .placeholder = Znajdź w dokumencie… +pdfjs-find-previous-button = + .title = Znajdź poprzednie wystąpienie tekstu +pdfjs-find-previous-button-label = Poprzednie +pdfjs-find-next-button = + .title = Znajdź następne wystąpienie tekstu +pdfjs-find-next-button-label = Następne +pdfjs-find-highlight-checkbox = Wyróżnianie wszystkich +pdfjs-find-match-case-checkbox-label = Rozróżnianie wielkości liter +pdfjs-find-match-diacritics-checkbox-label = Rozróżnianie liter diakrytyzowanych +pdfjs-find-entire-word-checkbox-label = Całe słowa +pdfjs-find-reached-top = Początek dokumentu. Wyszukiwanie od końca. +pdfjs-find-reached-bottom = Koniec dokumentu. Wyszukiwanie od początku. +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current }. z { $total } trafienia + [few] { $current }. z { $total } trafień + *[many] { $current }. z { $total } trafień + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Więcej niż { $limit } trafienie + [few] Więcej niż { $limit } trafienia + *[many] Więcej niż { $limit } trafień + } +pdfjs-find-not-found = Nie znaleziono tekstu + +## Predefined zoom values + +pdfjs-page-scale-width = Szerokość strony +pdfjs-page-scale-fit = Dopasowanie strony +pdfjs-page-scale-auto = Skala automatyczna +pdfjs-page-scale-actual = Rozmiar oryginalny +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page }. strona + +## Loading indicator messages + +pdfjs-loading-error = Podczas wczytywania dokumentu PDF wystąpił błąd. +pdfjs-invalid-file-error = Nieprawidłowy lub uszkodzony plik PDF. +pdfjs-missing-file-error = Brak pliku PDF. +pdfjs-unexpected-response-error = Nieoczekiwana odpowiedź serwera. +pdfjs-rendering-error = Podczas renderowania strony wystąpił błąd. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Przypis: { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Wprowadź hasło, aby otworzyć ten dokument PDF. +pdfjs-password-invalid = Nieprawidłowe hasło. Proszę spróbować ponownie. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Anuluj +pdfjs-web-fonts-disabled = Czcionki sieciowe są wyłączone: nie można użyć osadzonych czcionek PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Rysunek +pdfjs-editor-ink-button-label = Rysunek +pdfjs-editor-stamp-button = + .title = Dodaj lub edytuj obrazy +pdfjs-editor-stamp-button-label = Dodaj lub edytuj obrazy +pdfjs-editor-highlight-button = + .title = Wyróżnij +pdfjs-editor-highlight-button-label = Wyróżnij +pdfjs-highlight-floating-button1 = + .title = Wyróżnij + .aria-label = Wyróżnij +pdfjs-highlight-floating-button-label = Wyróżnij + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Usuń rysunek +pdfjs-editor-remove-freetext-button = + .title = Usuń tekst +pdfjs-editor-remove-stamp-button = + .title = Usuń obraz +pdfjs-editor-remove-highlight-button = + .title = Usuń wyróżnienie + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Kolor +pdfjs-editor-free-text-size-input = Rozmiar +pdfjs-editor-ink-color-input = Kolor +pdfjs-editor-ink-thickness-input = Grubość +pdfjs-editor-ink-opacity-input = Nieprzezroczystość +pdfjs-editor-stamp-add-image-button = + .title = Dodaj obraz +pdfjs-editor-stamp-add-image-button-label = Dodaj obraz +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grubość +pdfjs-editor-free-highlight-thickness-title = + .title = Zmień grubość podczas wyróżniania elementów innych niż tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Edytor tekstu + .default-content = Zacznij pisać… +pdfjs-free-text = + .aria-label = Edytor tekstu +pdfjs-free-text-default-content = Zacznij pisać… +pdfjs-ink = + .aria-label = Edytor rysunku +pdfjs-ink-canvas = + .aria-label = Obraz utworzony przez użytkownika + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Tekst alternatywny +pdfjs-editor-alt-text-edit-button = + .aria-label = Edytuj tekst alternatywny +pdfjs-editor-alt-text-edit-button-label = Edytuj tekst alternatywny +pdfjs-editor-alt-text-dialog-label = Wybierz opcję +pdfjs-editor-alt-text-dialog-description = Tekst alternatywny pomaga, kiedy ktoś nie może zobaczyć obrazu lub gdy się nie wczytuje. +pdfjs-editor-alt-text-add-description-label = Dodaj opis +pdfjs-editor-alt-text-add-description-description = Staraj się napisać 1-2 zdania opisujące temat, miejsce lub działania. +pdfjs-editor-alt-text-mark-decorative-label = Oznacz jako dekoracyjne +pdfjs-editor-alt-text-mark-decorative-description = Używane w przypadku obrazów ozdobnych, takich jak obramowania lub znaki wodne. +pdfjs-editor-alt-text-cancel-button = Anuluj +pdfjs-editor-alt-text-save-button = Zapisz +pdfjs-editor-alt-text-decorative-tooltip = Oznaczone jako dekoracyjne +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Na przykład: „Młody człowiek siada przy stole, aby zjeść posiłek” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Tekst alternatywny + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Lewy górny róg — zmień rozmiar +pdfjs-editor-resizer-label-top-middle = Górny środkowy — zmień rozmiar +pdfjs-editor-resizer-label-top-right = Prawy górny róg — zmień rozmiar +pdfjs-editor-resizer-label-middle-right = Prawy środkowy — zmień rozmiar +pdfjs-editor-resizer-label-bottom-right = Prawy dolny róg — zmień rozmiar +pdfjs-editor-resizer-label-bottom-middle = Dolny środkowy — zmień rozmiar +pdfjs-editor-resizer-label-bottom-left = Lewy dolny róg — zmień rozmiar +pdfjs-editor-resizer-label-middle-left = Lewy środkowy — zmień rozmiar +pdfjs-editor-resizer-top-left = + .aria-label = Lewy górny róg — zmień rozmiar +pdfjs-editor-resizer-top-middle = + .aria-label = Górny środkowy — zmień rozmiar +pdfjs-editor-resizer-top-right = + .aria-label = Prawy górny róg — zmień rozmiar +pdfjs-editor-resizer-middle-right = + .aria-label = Prawy środkowy — zmień rozmiar +pdfjs-editor-resizer-bottom-right = + .aria-label = Prawy dolny róg — zmień rozmiar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Dolny środkowy — zmień rozmiar +pdfjs-editor-resizer-bottom-left = + .aria-label = Lewy dolny róg — zmień rozmiar +pdfjs-editor-resizer-middle-left = + .aria-label = Lewy środkowy — zmień rozmiar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Kolor wyróżnienia +pdfjs-editor-colorpicker-button = + .title = Zmień kolor +pdfjs-editor-colorpicker-dropdown = + .aria-label = Wybór kolorów +pdfjs-editor-colorpicker-yellow = + .title = Żółty +pdfjs-editor-colorpicker-green = + .title = Zielony +pdfjs-editor-colorpicker-blue = + .title = Niebieski +pdfjs-editor-colorpicker-pink = + .title = Różowy +pdfjs-editor-colorpicker-red = + .title = Czerwony + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Pokaż wszystkie +pdfjs-editor-highlight-show-all-button = + .title = Pokaż wszystkie + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Edytuj tekst alternatywny (opis obrazu) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Dodaj tekst alternatywny (opis obrazu) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Napisz tutaj opis… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Krótki opis dla osób, które nie widzą obrazu lub kiedy obraz się nie wczytuje. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ten tekst alternatywny został utworzony automatycznie i może być niepoprawny. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Więcej informacji +pdfjs-editor-new-alt-text-create-automatically-button-label = Automatycznie utwórz tekst alternatywny +pdfjs-editor-new-alt-text-not-now-button = Nie teraz +pdfjs-editor-new-alt-text-error-title = Nie można automatycznie utworzyć tekstu alternatywnego +pdfjs-editor-new-alt-text-error-description = Proszę napisać własny tekst alternatywny lub spróbować ponownie później. +pdfjs-editor-new-alt-text-error-close-button = Zamknij +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Pobieranie modelu SI tekstu alternatywnego ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Pobieranie modelu SI tekstu alternatywnego ({ $downloadedSize } z { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Dodano tekst alternatywny +pdfjs-editor-new-alt-text-added-button-label = Dodano tekst alternatywny +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Brak tekstu alternatywnego +pdfjs-editor-new-alt-text-missing-button-label = Brak tekstu alternatywnego +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Przejrzyj tekst alternatywny +pdfjs-editor-new-alt-text-to-review-button-label = Przejrzyj tekst alternatywny +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Utworzono automatycznie: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Ustawienia tekstu alternatywnego obrazów +pdfjs-image-alt-text-settings-button-label = Ustawienia tekstu alternatywnego obrazów +pdfjs-editor-alt-text-settings-dialog-label = Ustawienia tekstu alternatywnego obrazów +pdfjs-editor-alt-text-settings-automatic-title = Automatyczny tekst alternatywny +pdfjs-editor-alt-text-settings-create-model-button-label = Automatyczne tworzenie tekstu alternatywnego +pdfjs-editor-alt-text-settings-create-model-description = Podpowiada opisy, które mogą pomóc osobom, które nie widzą obrazu lub kiedy obraz się nie wczytuje. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model SI tekstu alternatywnego ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Działa lokalnie na urządzeniu użytkownika, więc Twoje dane pozostają prywatne. Wymagane do funkcji automatycznego tekstu alternatywnego. +pdfjs-editor-alt-text-settings-delete-model-button = Usuń +pdfjs-editor-alt-text-settings-download-model-button = Pobierz +pdfjs-editor-alt-text-settings-downloading-model-button = Pobieranie… +pdfjs-editor-alt-text-settings-editor-title = Edytor tekstu alternatywnego +pdfjs-editor-alt-text-settings-show-dialog-button-label = Wyświetlanie edytora tekstu alternatywnego od razu po dodaniu obrazu +pdfjs-editor-alt-text-settings-show-dialog-description = Pomaga upewnić się, że wszystkie obrazy mają tekst alternatywny. +pdfjs-editor-alt-text-settings-close-button = Zamknij + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Usunięto wyróżnienie +pdfjs-editor-undo-bar-message-freetext = Usunięto tekst +pdfjs-editor-undo-bar-message-ink = Usunięto rysunek +pdfjs-editor-undo-bar-message-stamp = Usunięto obraz +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] Usunięto przypis + [few] Usunięto { $count } przypisy + *[many] Usunięto { $count } przypisów + } +pdfjs-editor-undo-bar-undo-button = + .title = Cofnij +pdfjs-editor-undo-bar-undo-button-label = Cofnij +pdfjs-editor-undo-bar-close-button = + .title = Zamknij +pdfjs-editor-undo-bar-close-button-label = Zamknij diff --git a/public/assets/pdfjs/locale/pt-BR/viewer.ftl b/public/assets/pdfjs/locale/pt-BR/viewer.ftl new file mode 100644 index 0000000..7da5201 --- /dev/null +++ b/public/assets/pdfjs/locale/pt-BR/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Próxima página +pdfjs-next-button-label = Próxima +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Reduzir +pdfjs-zoom-out-button-label = Reduzir +pdfjs-zoom-in-button = + .title = Ampliar +pdfjs-zoom-in-button-label = Ampliar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Mudar para o modo de apresentação +pdfjs-presentation-mode-button-label = Modo de apresentação +pdfjs-open-file-button = + .title = Abrir arquivo +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Salvar +pdfjs-save-button-label = Salvar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Baixar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Baixar +pdfjs-bookmark-button = + .title = Página atual (ver URL da página atual) +pdfjs-bookmark-button-label = Pagina atual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ferramentas +pdfjs-tools-button-label = Ferramentas +pdfjs-first-page-button = + .title = Ir para a primeira página +pdfjs-first-page-button-label = Ir para a primeira página +pdfjs-last-page-button = + .title = Ir para a última página +pdfjs-last-page-button-label = Ir para a última página +pdfjs-page-rotate-cw-button = + .title = Girar no sentido horário +pdfjs-page-rotate-cw-button-label = Girar no sentido horário +pdfjs-page-rotate-ccw-button = + .title = Girar no sentido anti-horário +pdfjs-page-rotate-ccw-button-label = Girar no sentido anti-horário +pdfjs-cursor-text-select-tool-button = + .title = Ativar a ferramenta de seleção de texto +pdfjs-cursor-text-select-tool-button-label = Ferramenta de seleção de texto +pdfjs-cursor-hand-tool-button = + .title = Ativar ferramenta de deslocamento +pdfjs-cursor-hand-tool-button-label = Ferramenta de deslocamento +pdfjs-scroll-page-button = + .title = Usar rolagem de página +pdfjs-scroll-page-button-label = Rolagem de página +pdfjs-scroll-vertical-button = + .title = Usar deslocamento vertical +pdfjs-scroll-vertical-button-label = Deslocamento vertical +pdfjs-scroll-horizontal-button = + .title = Usar deslocamento horizontal +pdfjs-scroll-horizontal-button-label = Deslocamento horizontal +pdfjs-scroll-wrapped-button = + .title = Usar deslocamento contido +pdfjs-scroll-wrapped-button-label = Deslocamento contido +pdfjs-spread-none-button = + .title = Não reagrupar páginas +pdfjs-spread-none-button-label = Não estender +pdfjs-spread-odd-button = + .title = Agrupar páginas começando em páginas com números ímpares +pdfjs-spread-odd-button-label = Estender ímpares +pdfjs-spread-even-button = + .title = Agrupar páginas começando em páginas com números pares +pdfjs-spread-even-button-label = Estender pares + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propriedades do documento… +pdfjs-document-properties-button-label = Propriedades do documento… +pdfjs-document-properties-file-name = Nome do arquivo: +pdfjs-document-properties-file-size = Tamanho do arquivo: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Assunto: +pdfjs-document-properties-keywords = Palavras-chave: +pdfjs-document-properties-creation-date = Data da criação: +pdfjs-document-properties-modification-date = Data da modificação: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Criação: +pdfjs-document-properties-producer = Criador do PDF: +pdfjs-document-properties-version = Versão do PDF: +pdfjs-document-properties-page-count = Número de páginas: +pdfjs-document-properties-page-size = Tamanho da página: +pdfjs-document-properties-page-size-unit-inches = pol. +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = retrato +pdfjs-document-properties-page-size-orientation-landscape = paisagem +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Jurídico + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Exibição web rápida: +pdfjs-document-properties-linearized-yes = Sim +pdfjs-document-properties-linearized-no = Não +pdfjs-document-properties-close-button = Fechar + +## Print + +pdfjs-print-progress-message = Preparando documento para impressão… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Aviso: a impressão não é totalmente suportada neste navegador. +pdfjs-printing-not-ready = Aviso: o PDF não está totalmente carregado para impressão. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Exibir/ocultar painel lateral +pdfjs-toggle-sidebar-notification-button = + .title = Exibir/ocultar painel lateral (documento contém estrutura/anexos/camadas) +pdfjs-toggle-sidebar-button-label = Exibir/ocultar painel lateral +pdfjs-document-outline-button = + .title = Mostrar estrutura do documento (duplo-clique expande/recolhe todos os itens) +pdfjs-document-outline-button-label = Estrutura do documento +pdfjs-attachments-button = + .title = Mostrar anexos +pdfjs-attachments-button-label = Anexos +pdfjs-layers-button = + .title = Mostrar camadas (duplo-clique redefine todas as camadas ao estado predefinido) +pdfjs-layers-button-label = Camadas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Encontrar item atual da estrutura +pdfjs-current-outline-item-button-label = Item atual da estrutura +pdfjs-findbar-button = + .title = Procurar no documento +pdfjs-findbar-button-label = Procurar +pdfjs-additional-layers = Camadas adicionais + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura da página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Procurar + .placeholder = Procurar no documento… +pdfjs-find-previous-button = + .title = Procurar a ocorrência anterior da frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Procurar a próxima ocorrência da frase +pdfjs-find-next-button-label = Próxima +pdfjs-find-highlight-checkbox = Destacar tudo +pdfjs-find-match-case-checkbox-label = Diferenciar maiúsculas/minúsculas +pdfjs-find-match-diacritics-checkbox-label = Considerar acentuação +pdfjs-find-entire-word-checkbox-label = Palavras completas +pdfjs-find-reached-top = Início do documento alcançado, continuando do fim +pdfjs-find-reached-bottom = Fim do documento alcançado, continuando do início +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } ocorrência + *[other] { $current } de { $total } ocorrências + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mais de { $limit } ocorrência + *[other] Mais de { $limit } ocorrências + } +pdfjs-find-not-found = Não encontrado + +## Predefined zoom values + +pdfjs-page-scale-width = Largura da página +pdfjs-page-scale-fit = Ajustar à janela +pdfjs-page-scale-auto = Zoom automático +pdfjs-page-scale-actual = Tamanho real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ocorreu um erro ao carregar o PDF. +pdfjs-invalid-file-error = Arquivo PDF corrompido ou inválido. +pdfjs-missing-file-error = Arquivo PDF ausente. +pdfjs-unexpected-response-error = Resposta inesperada do servidor. +pdfjs-rendering-error = Ocorreu um erro ao renderizar a página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotação { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Forneça a senha para abrir este arquivo PDF. +pdfjs-password-invalid = Senha inválida. Tente novamente. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = As fontes web estão desativadas: não foi possível usar fontes incorporadas do PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Desenho +pdfjs-editor-ink-button-label = Desenho +pdfjs-editor-stamp-button = + .title = Adicionar ou editar imagens +pdfjs-editor-stamp-button-label = Adicionar ou editar imagens +pdfjs-editor-highlight-button = + .title = Destaque +pdfjs-editor-highlight-button-label = Destaque +pdfjs-highlight-floating-button1 = + .title = Destaque + .aria-label = Destaque +pdfjs-highlight-floating-button-label = Destaque + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remover desenho +pdfjs-editor-remove-freetext-button = + .title = Remover texto +pdfjs-editor-remove-stamp-button = + .title = Remover imagem +pdfjs-editor-remove-highlight-button = + .title = Remover destaque + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Cor +pdfjs-editor-free-text-size-input = Tamanho +pdfjs-editor-ink-color-input = Cor +pdfjs-editor-ink-thickness-input = Espessura +pdfjs-editor-ink-opacity-input = Opacidade +pdfjs-editor-stamp-add-image-button = + .title = Adicionar imagem +pdfjs-editor-stamp-add-image-button-label = Adicionar imagem +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Espessura +pdfjs-editor-free-highlight-thickness-title = + .title = Mudar espessura ao destacar itens que não são texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Comece a digitar… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Comece digitando… +pdfjs-ink = + .aria-label = Editor de desenho +pdfjs-ink-canvas = + .aria-label = Imagem criada pelo usuário + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar texto alternativo +pdfjs-editor-alt-text-dialog-label = Escolha uma opção +pdfjs-editor-alt-text-dialog-description = O texto alternativo ajuda quando uma imagem não aparece ou não é carregada. +pdfjs-editor-alt-text-add-description-label = Adicionar uma descrição +pdfjs-editor-alt-text-add-description-description = Procure usar uma ou duas frases que descrevam o assunto, cenário ou ação. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativa +pdfjs-editor-alt-text-mark-decorative-description = Isto é usado em imagens ornamentais, como bordas ou marcas d'água. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Salvar +pdfjs-editor-alt-text-decorative-tooltip = Marcado como decorativa +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por exemplo, “Um jovem senta-se à mesa para comer uma refeição” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Canto superior esquerdo — redimensionar +pdfjs-editor-resizer-label-top-middle = No centro do topo — redimensionar +pdfjs-editor-resizer-label-top-right = Canto superior direito — redimensionar +pdfjs-editor-resizer-label-middle-right = No meio à direita — redimensionar +pdfjs-editor-resizer-label-bottom-right = Canto inferior direito — redimensionar +pdfjs-editor-resizer-label-bottom-middle = No centro da base — redimensionar +pdfjs-editor-resizer-label-bottom-left = Canto inferior esquerdo — redimensionar +pdfjs-editor-resizer-label-middle-left = No meio à esquerda — redimensionar +pdfjs-editor-resizer-top-left = + .aria-label = Canto superior esquerdo — redimensionar +pdfjs-editor-resizer-top-middle = + .aria-label = No centro do topo — redimensionar +pdfjs-editor-resizer-top-right = + .aria-label = Canto superior direito — redimensionar +pdfjs-editor-resizer-middle-right = + .aria-label = No meio à direita — redimensionar +pdfjs-editor-resizer-bottom-right = + .aria-label = Canto inferior direito — redimensionar +pdfjs-editor-resizer-bottom-middle = + .aria-label = No centro da base — redimensionar +pdfjs-editor-resizer-bottom-left = + .aria-label = Canto inferior esquerdo — redimensionar +pdfjs-editor-resizer-middle-left = + .aria-label = No meio à esquerda — redimensionar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Cor de destaque +pdfjs-editor-colorpicker-button = + .title = Mudar cor +pdfjs-editor-colorpicker-dropdown = + .aria-label = Opções de cores +pdfjs-editor-colorpicker-yellow = + .title = Amarelo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Vermelho + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar todos +pdfjs-editor-highlight-show-all-button = + .title = Mostrar todos + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descrição da imagem) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Adicionar texto alternativo (descrição da imagem) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escreva sua descrição aqui… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Descrição curta para pessoas que não conseguem ver a imagem ou quando a imagem não é carregada. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo foi criado automaticamente, pode não estar correto. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Saiba mais +pdfjs-editor-new-alt-text-create-automatically-button-label = Criar texto alternativo automaticamente +pdfjs-editor-new-alt-text-not-now-button = Agora não +pdfjs-editor-new-alt-text-error-title = Não foi possível criar texto alternativo automaticamente +pdfjs-editor-new-alt-text-error-description = Escreva seu próprio texto alternativo ou tente novamente mais tarde. +pdfjs-editor-new-alt-text-error-close-button = Fechar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Baixando modelo de inteligência artificial de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = Baixando modelo de inteligência artificial de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Texto alternativo adicionado +pdfjs-editor-new-alt-text-added-button-label = Texto alternativo adicionado +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Falta texto alternativo +pdfjs-editor-new-alt-text-missing-button-label = Falta texto alternativo +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Revisar texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Revisar texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Criado automaticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Configurações de texto alternativo de imagens +pdfjs-image-alt-text-settings-button-label = Configurações de texto alternativo de imagens +pdfjs-editor-alt-text-settings-dialog-label = Configurações de texto alternativo de imagens +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Criar texto alternativo automaticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugere uma descrição para ajudar pessoas que não conseguem ver a imagem ou quando a imagem não é carregada. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de inteligência artificial de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Funciona localmente no seu dispositivo para que seus dados permaneçam privativos. Necessário para texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Excluir +pdfjs-editor-alt-text-settings-download-model-button = Baixar +pdfjs-editor-alt-text-settings-downloading-model-button = Baixando… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar o editor de texto alternativo imediatamente ao adicionar uma imagem +pdfjs-editor-alt-text-settings-show-dialog-description = Ajuda a assegurar que todas as suas imagens tenham texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Fechar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Destaque removido +pdfjs-editor-undo-bar-message-freetext = Texto removido +pdfjs-editor-undo-bar-message-ink = Desenho removido +pdfjs-editor-undo-bar-message-stamp = Imagem removida +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotação removida + *[other] { $count } anotações removidas + } +pdfjs-editor-undo-bar-undo-button = + .title = Desfazer +pdfjs-editor-undo-bar-undo-button-label = Desfazer +pdfjs-editor-undo-bar-close-button = + .title = Fechar +pdfjs-editor-undo-bar-close-button-label = Fechar diff --git a/public/assets/pdfjs/locale/pt-PT/viewer.ftl b/public/assets/pdfjs/locale/pt-PT/viewer.ftl new file mode 100644 index 0000000..1829417 --- /dev/null +++ b/public/assets/pdfjs/locale/pt-PT/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Página anterior +pdfjs-previous-button-label = Anterior +pdfjs-next-button = + .title = Página seguinte +pdfjs-next-button-label = Seguinte +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Página +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Reduzir +pdfjs-zoom-out-button-label = Reduzir +pdfjs-zoom-in-button = + .title = Ampliar +pdfjs-zoom-in-button-label = Ampliar +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Trocar para o modo de apresentação +pdfjs-presentation-mode-button-label = Modo de apresentação +pdfjs-open-file-button = + .title = Abrir ficheiro +pdfjs-open-file-button-label = Abrir +pdfjs-print-button = + .title = Imprimir +pdfjs-print-button-label = Imprimir +pdfjs-save-button = + .title = Guardar +pdfjs-save-button-label = Guardar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Transferir +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Transferir +pdfjs-bookmark-button = + .title = Página atual (ver URL da página atual) +pdfjs-bookmark-button-label = Pagina atual + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Ferramentas +pdfjs-tools-button-label = Ferramentas +pdfjs-first-page-button = + .title = Ir para a primeira página +pdfjs-first-page-button-label = Ir para a primeira página +pdfjs-last-page-button = + .title = Ir para a última página +pdfjs-last-page-button-label = Ir para a última página +pdfjs-page-rotate-cw-button = + .title = Rodar à direita +pdfjs-page-rotate-cw-button-label = Rodar à direita +pdfjs-page-rotate-ccw-button = + .title = Rodar à esquerda +pdfjs-page-rotate-ccw-button-label = Rodar à esquerda +pdfjs-cursor-text-select-tool-button = + .title = Ativar ferramenta de seleção de texto +pdfjs-cursor-text-select-tool-button-label = Ferramenta de seleção de texto +pdfjs-cursor-hand-tool-button = + .title = Ativar ferramenta de mão +pdfjs-cursor-hand-tool-button-label = Ferramenta de mão +pdfjs-scroll-page-button = + .title = Utilizar deslocamento da página +pdfjs-scroll-page-button-label = Deslocamento da página +pdfjs-scroll-vertical-button = + .title = Utilizar deslocação vertical +pdfjs-scroll-vertical-button-label = Deslocação vertical +pdfjs-scroll-horizontal-button = + .title = Utilizar deslocação horizontal +pdfjs-scroll-horizontal-button-label = Deslocação horizontal +pdfjs-scroll-wrapped-button = + .title = Utilizar deslocação encapsulada +pdfjs-scroll-wrapped-button-label = Deslocação encapsulada +pdfjs-spread-none-button = + .title = Não juntar páginas dispersas +pdfjs-spread-none-button-label = Sem spreads +pdfjs-spread-odd-button = + .title = Juntar páginas dispersas a partir de páginas com números ímpares +pdfjs-spread-odd-button-label = Spreads ímpares +pdfjs-spread-even-button = + .title = Juntar páginas dispersas a partir de páginas com números pares +pdfjs-spread-even-button-label = Spreads pares + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propriedades do documento… +pdfjs-document-properties-button-label = Propriedades do documento… +pdfjs-document-properties-file-name = Nome do ficheiro: +pdfjs-document-properties-file-size = Tamanho do ficheiro: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Título: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Assunto: +pdfjs-document-properties-keywords = Palavras-chave: +pdfjs-document-properties-creation-date = Data de criação: +pdfjs-document-properties-modification-date = Data de modificação: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Criador: +pdfjs-document-properties-producer = Produtor de PDF: +pdfjs-document-properties-version = Versão do PDF: +pdfjs-document-properties-page-count = N.º de páginas: +pdfjs-document-properties-page-size = Tamanho da página: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = retrato +pdfjs-document-properties-page-size-orientation-landscape = paisagem +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Carta +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista rápida web: +pdfjs-document-properties-linearized-yes = Sim +pdfjs-document-properties-linearized-no = Não +pdfjs-document-properties-close-button = Fechar + +## Print + +pdfjs-print-progress-message = A preparar o documento para impressão… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cancelar +pdfjs-printing-not-supported = Aviso: a impressão não é totalmente suportada por este navegador. +pdfjs-printing-not-ready = Aviso: o PDF ainda não está totalmente carregado. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Alternar barra lateral +pdfjs-toggle-sidebar-notification-button = + .title = Alternar barra lateral (o documento contém contornos/anexos/camadas) +pdfjs-toggle-sidebar-button-label = Alternar barra lateral +pdfjs-document-outline-button = + .title = Mostrar esquema do documento (duplo clique para expandir/colapsar todos os itens) +pdfjs-document-outline-button-label = Esquema do documento +pdfjs-attachments-button = + .title = Mostrar anexos +pdfjs-attachments-button-label = Anexos +pdfjs-layers-button = + .title = Mostrar camadas (clique duas vezes para repor todas as camadas para o estado predefinido) +pdfjs-layers-button-label = Camadas +pdfjs-thumbs-button = + .title = Mostrar miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Encontrar o item atualmente destacado +pdfjs-current-outline-item-button-label = Item atualmente destacado +pdfjs-findbar-button = + .title = Localizar em documento +pdfjs-findbar-button-label = Localizar +pdfjs-additional-layers = Camadas adicionais + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Página { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura da página { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Localizar + .placeholder = Localizar em documento… +pdfjs-find-previous-button = + .title = Localizar ocorrência anterior da frase +pdfjs-find-previous-button-label = Anterior +pdfjs-find-next-button = + .title = Localizar ocorrência seguinte da frase +pdfjs-find-next-button-label = Seguinte +pdfjs-find-highlight-checkbox = Destacar tudo +pdfjs-find-match-case-checkbox-label = Correspondência +pdfjs-find-match-diacritics-checkbox-label = Corresponder diacríticos +pdfjs-find-entire-word-checkbox-label = Palavras completas +pdfjs-find-reached-top = Topo do documento atingido, a continuar a partir do fundo +pdfjs-find-reached-bottom = Fim do documento atingido, a continuar a partir do topo +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } de { $total } correspondência + *[other] { $current } de { $total } correspondências + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mais de { $limit } correspondência + *[other] Mais de { $limit } correspondências + } +pdfjs-find-not-found = Frase não encontrada + +## Predefined zoom values + +pdfjs-page-scale-width = Ajustar à largura +pdfjs-page-scale-fit = Ajustar à página +pdfjs-page-scale-auto = Zoom automático +pdfjs-page-scale-actual = Tamanho real +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Página { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ocorreu um erro ao carregar o PDF. +pdfjs-invalid-file-error = Ficheiro PDF inválido ou danificado. +pdfjs-missing-file-error = Ficheiro PDF inexistente. +pdfjs-unexpected-response-error = Resposta inesperada do servidor. +pdfjs-rendering-error = Ocorreu um erro ao processar a página. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotação { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Introduza a palavra-passe para abrir este ficheiro PDF. +pdfjs-password-invalid = Palavra-passe inválida. Por favor, tente novamente. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Cancelar +pdfjs-web-fonts-disabled = Os tipos de letra web estão desativados: não é possível utilizar os tipos de letra PDF embutidos. + +## Editing + +pdfjs-editor-free-text-button = + .title = Texto +pdfjs-editor-free-text-button-label = Texto +pdfjs-editor-ink-button = + .title = Desenhar +pdfjs-editor-ink-button-label = Desenhar +pdfjs-editor-stamp-button = + .title = Adicionar ou editar imagens +pdfjs-editor-stamp-button-label = Adicionar ou editar imagens +pdfjs-editor-highlight-button = + .title = Destaque +pdfjs-editor-highlight-button-label = Destaque +pdfjs-highlight-floating-button1 = + .title = Realçar + .aria-label = Realçar +pdfjs-highlight-floating-button-label = Realçar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Remover desenho +pdfjs-editor-remove-freetext-button = + .title = Remover texto +pdfjs-editor-remove-stamp-button = + .title = Remover imagem +pdfjs-editor-remove-highlight-button = + .title = Remover destaque + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Cor +pdfjs-editor-free-text-size-input = Tamanho +pdfjs-editor-ink-color-input = Cor +pdfjs-editor-ink-thickness-input = Espessura +pdfjs-editor-ink-opacity-input = Opacidade +pdfjs-editor-stamp-add-image-button = + .title = Adicionar imagem +pdfjs-editor-stamp-add-image-button-label = Adicionar imagem +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Espessura +pdfjs-editor-free-highlight-thickness-title = + .title = Alterar espessura quando destacar itens que não sejam texto +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editor de texto + .default-content = Comece a escrever… +pdfjs-free-text = + .aria-label = Editor de texto +pdfjs-free-text-default-content = Começar a digitar… +pdfjs-ink = + .aria-label = Editor de desenho +pdfjs-ink-canvas = + .aria-label = Imagem criada pelo utilizador + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Texto alternativo +pdfjs-editor-alt-text-edit-button = + .aria-label = Editar texto alternativo +pdfjs-editor-alt-text-edit-button-label = Editar texto alternativo +pdfjs-editor-alt-text-dialog-label = Escolher uma opção +pdfjs-editor-alt-text-dialog-description = O texto alternativo (texto alternativo) ajuda quando as pessoas não conseguem ver a imagem ou quando a mesma não é carregada. +pdfjs-editor-alt-text-add-description-label = Adicionar uma descrição +pdfjs-editor-alt-text-add-description-description = Aponte para 1-2 frases que descrevam o assunto, definição ou ações. +pdfjs-editor-alt-text-mark-decorative-label = Marcar como decorativa +pdfjs-editor-alt-text-mark-decorative-description = Isto é utilizado para imagens decorativas, tais como limites ou marcas d'água. +pdfjs-editor-alt-text-cancel-button = Cancelar +pdfjs-editor-alt-text-save-button = Guardar +pdfjs-editor-alt-text-decorative-tooltip = Marcada como decorativa +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Por exemplo, “Um jovem senta-se à mesa para comer uma refeição” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Texto alternativo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Canto superior esquerdo — redimensionar +pdfjs-editor-resizer-label-top-middle = Superior ao centro — redimensionar +pdfjs-editor-resizer-label-top-right = Canto superior direito — redimensionar +pdfjs-editor-resizer-label-middle-right = Centro à direita — redimensionar +pdfjs-editor-resizer-label-bottom-right = Canto inferior direito — redimensionar +pdfjs-editor-resizer-label-bottom-middle = Inferior ao centro — redimensionar +pdfjs-editor-resizer-label-bottom-left = Canto inferior esquerdo — redimensionar +pdfjs-editor-resizer-label-middle-left = Centro à esquerda — redimensionar +pdfjs-editor-resizer-top-left = + .aria-label = Canto superior esquerdo — redimensionar +pdfjs-editor-resizer-top-middle = + .aria-label = Superior ao centro — redimensionar +pdfjs-editor-resizer-top-right = + .aria-label = Canto superior direito — redimensionar +pdfjs-editor-resizer-middle-right = + .aria-label = Centro à direita — redimensionar +pdfjs-editor-resizer-bottom-right = + .aria-label = Canto inferior direito — redimensionar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Inferior ao centro — redimensionar +pdfjs-editor-resizer-bottom-left = + .aria-label = Canto inferior esquerdo — redimensionar +pdfjs-editor-resizer-middle-left = + .aria-label = Centro à esquerda — redimensionar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Cor de destaque +pdfjs-editor-colorpicker-button = + .title = Alterar cor +pdfjs-editor-colorpicker-dropdown = + .aria-label = Escolhas de cor +pdfjs-editor-colorpicker-yellow = + .title = Amarelo +pdfjs-editor-colorpicker-green = + .title = Verde +pdfjs-editor-colorpicker-blue = + .title = Azul +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Vermelho + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mostrar tudo +pdfjs-editor-highlight-show-all-button = + .title = Mostrar tudo + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Editar texto alternativo (descrição da imagem) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Adicionar texto alternativo (descrição da imagem) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Escreva a sua descrição aqui… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Descrição curta para as pessoas que não podem visualizar a imagem ou quando a imagem não carrega. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Este texto alternativo foi criado automaticamente e pode ser impreciso. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Saber mais +pdfjs-editor-new-alt-text-create-automatically-button-label = Criar texto alternativo automaticamente +pdfjs-editor-new-alt-text-not-now-button = Agora não +pdfjs-editor-new-alt-text-error-title = Não foi possível criar o texto alternativo automaticamente +pdfjs-editor-new-alt-text-error-description = Escreva o seu próprio texto alternativo ou tente novamente mais tarde. +pdfjs-editor-new-alt-text-error-close-button = Fechar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = A transferir o modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) + .aria-valuetext = A transferir o modelo de IA de texto alternativo ({ $downloadedSize } de { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Texto alternativo adicionado +pdfjs-editor-new-alt-text-added-button-label = Texto alternativo adicionado +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Texto alternativo em falta +pdfjs-editor-new-alt-text-missing-button-label = Texto alternativo em falta +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Rever texto alternativo +pdfjs-editor-new-alt-text-to-review-button-label = Rever texto alternativo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Criado automaticamente: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Definições de texto alternativo da imagem +pdfjs-image-alt-text-settings-button-label = Definições de texto alternativo da imagem +pdfjs-editor-alt-text-settings-dialog-label = Definições de texto alternativo das imagens +pdfjs-editor-alt-text-settings-automatic-title = Texto alternativo automático +pdfjs-editor-alt-text-settings-create-model-button-label = Criar texto alternativo automaticamente +pdfjs-editor-alt-text-settings-create-model-description = Sugere descrições para ajudar as pessoas que não podem visualizar a imagem ou quando a imagem não carrega. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modelo de IA de texto alternativo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = É executado localmente no seu dispositivo para que os seus dados se mantenham privados. É necessário para o texto alternativo automático. +pdfjs-editor-alt-text-settings-delete-model-button = Eliminar +pdfjs-editor-alt-text-settings-download-model-button = Transferir +pdfjs-editor-alt-text-settings-downloading-model-button = A transferir… +pdfjs-editor-alt-text-settings-editor-title = Editor de texto alternativo +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mostrar editor de texto alternativo imediatamente ao adicionar uma imagem +pdfjs-editor-alt-text-settings-show-dialog-description = Ajuda a garantir que todas as suas imagens tenham um texto alternativo. +pdfjs-editor-alt-text-settings-close-button = Fechar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Destaque removido +pdfjs-editor-undo-bar-message-freetext = Texto removido +pdfjs-editor-undo-bar-message-ink = Desenho removido +pdfjs-editor-undo-bar-message-stamp = Imagem removida +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotação removida + *[other] { $count } anotações removidas + } +pdfjs-editor-undo-bar-undo-button = + .title = Anular +pdfjs-editor-undo-bar-undo-button-label = Anular +pdfjs-editor-undo-bar-close-button = + .title = Fechar +pdfjs-editor-undo-bar-close-button-label = Fechar diff --git a/public/assets/pdfjs/locale/rm/viewer.ftl b/public/assets/pdfjs/locale/rm/viewer.ftl new file mode 100644 index 0000000..76992da --- /dev/null +++ b/public/assets/pdfjs/locale/rm/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina precedenta +pdfjs-previous-button-label = Enavos +pdfjs-next-button = + .title = Proxima pagina +pdfjs-next-button-label = Enavant +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = da { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } da { $pagesCount }) +pdfjs-zoom-out-button = + .title = Empitschnir +pdfjs-zoom-out-button-label = Empitschnir +pdfjs-zoom-in-button = + .title = Engrondir +pdfjs-zoom-in-button-label = Engrondir +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Midar en il modus da preschentaziun +pdfjs-presentation-mode-button-label = Modus da preschentaziun +pdfjs-open-file-button = + .title = Avrir datoteca +pdfjs-open-file-button-label = Avrir +pdfjs-print-button = + .title = Stampar +pdfjs-print-button-label = Stampar +pdfjs-save-button = + .title = Memorisar +pdfjs-save-button-label = Memorisar +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Telechargiar +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Telechargiar +pdfjs-bookmark-button = + .title = Pagina actuala (mussar l'URL da la pagina actuala) +pdfjs-bookmark-button-label = Pagina actuala + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Utensils +pdfjs-tools-button-label = Utensils +pdfjs-first-page-button = + .title = Siglir a l'emprima pagina +pdfjs-first-page-button-label = Siglir a l'emprima pagina +pdfjs-last-page-button = + .title = Siglir a la davosa pagina +pdfjs-last-page-button-label = Siglir a la davosa pagina +pdfjs-page-rotate-cw-button = + .title = Rotar en direcziun da l'ura +pdfjs-page-rotate-cw-button-label = Rotar en direcziun da l'ura +pdfjs-page-rotate-ccw-button = + .title = Rotar en direcziun cuntraria a l'ura +pdfjs-page-rotate-ccw-button-label = Rotar en direcziun cuntraria a l'ura +pdfjs-cursor-text-select-tool-button = + .title = Activar l'utensil per selecziunar text +pdfjs-cursor-text-select-tool-button-label = Utensil per selecziunar text +pdfjs-cursor-hand-tool-button = + .title = Activar l'utensil da maun +pdfjs-cursor-hand-tool-button-label = Utensil da maun +pdfjs-scroll-page-button = + .title = Utilisar la defilada per pagina +pdfjs-scroll-page-button-label = Defilada per pagina +pdfjs-scroll-vertical-button = + .title = Utilisar il defilar vertical +pdfjs-scroll-vertical-button-label = Defilar vertical +pdfjs-scroll-horizontal-button = + .title = Utilisar il defilar orizontal +pdfjs-scroll-horizontal-button-label = Defilar orizontal +pdfjs-scroll-wrapped-button = + .title = Utilisar il defilar en colonnas +pdfjs-scroll-wrapped-button-label = Defilar en colonnas +pdfjs-spread-none-button = + .title = Betg parallelisar las paginas +pdfjs-spread-none-button-label = Betg parallel +pdfjs-spread-odd-button = + .title = Parallelisar las paginas cun cumenzar cun paginas spèras +pdfjs-spread-odd-button-label = Parallel spèr +pdfjs-spread-even-button = + .title = Parallelisar las paginas cun cumenzar cun paginas pèras +pdfjs-spread-even-button-label = Parallel pèr + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Caracteristicas dal document… +pdfjs-document-properties-button-label = Caracteristicas dal document… +pdfjs-document-properties-file-name = Num da la datoteca: +pdfjs-document-properties-file-size = Grondezza da la datoteca: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Autur: +pdfjs-document-properties-subject = Tema: +pdfjs-document-properties-keywords = Chavazzins: +pdfjs-document-properties-creation-date = Data da creaziun: +pdfjs-document-properties-modification-date = Data da modificaziun: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } { $time } +pdfjs-document-properties-creator = Creà da: +pdfjs-document-properties-producer = Creà il PDF cun: +pdfjs-document-properties-version = Versiun da PDF: +pdfjs-document-properties-page-count = Dumber da paginas: +pdfjs-document-properties-page-size = Grondezza da la pagina: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = vertical +pdfjs-document-properties-page-size-orientation-landscape = orizontal +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Gea +pdfjs-document-properties-linearized-no = Na +pdfjs-document-properties-close-button = Serrar + +## Print + +pdfjs-print-progress-message = Preparar il document per stampar… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Interrumper +pdfjs-printing-not-supported = Attenziun: Il stampar na funcziunescha anc betg dal tut en quest navigatur. +pdfjs-printing-not-ready = Attenziun: Il PDF n'è betg chargià cumplettamain per stampar. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Activar/deactivar la trav laterala +pdfjs-toggle-sidebar-notification-button = + .title = Activar/deactivar la trav laterala (il document cuntegna structura dal document/agiuntas/nivels) +pdfjs-toggle-sidebar-button-label = Activar/deactivar la trav laterala +pdfjs-document-outline-button = + .title = Mussar la structura dal document (cliccar duas giadas per extender/cumprimer tut ils elements) +pdfjs-document-outline-button-label = Structura dal document +pdfjs-attachments-button = + .title = Mussar agiuntas +pdfjs-attachments-button-label = Agiuntas +pdfjs-layers-button = + .title = Mussar ils nivels (cliccar dubel per restaurar il stadi da standard da tut ils nivels) +pdfjs-layers-button-label = Nivels +pdfjs-thumbs-button = + .title = Mussar las miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Tschertgar l'element da structura actual +pdfjs-current-outline-item-button-label = Element da structura actual +pdfjs-findbar-button = + .title = Tschertgar en il document +pdfjs-findbar-button-label = Tschertgar +pdfjs-additional-layers = Nivels supplementars + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura da la pagina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Tschertgar + .placeholder = Tschertgar en il document… +pdfjs-find-previous-button = + .title = Tschertgar la posiziun precedenta da l'expressiun +pdfjs-find-previous-button-label = Enavos +pdfjs-find-next-button = + .title = Tschertgar la proxima posiziun da l'expressiun +pdfjs-find-next-button-label = Enavant +pdfjs-find-highlight-checkbox = Relevar tuts +pdfjs-find-match-case-checkbox-label = Resguardar maiusclas/minusclas +pdfjs-find-match-diacritics-checkbox-label = Resguardar ils segns diacritics +pdfjs-find-entire-word-checkbox-label = Pleds entirs +pdfjs-find-reached-top = Il cumenzament dal document è cuntanschì, la tschertga cuntinuescha a la fin dal document +pdfjs-find-reached-bottom = La fin dal document è cuntanschì, la tschertga cuntinuescha al cumenzament dal document +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } dad { $total } correspundenza + *[other] { $current } da { $total } correspundenzas + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Dapli che { $limit } correspundenza + *[other] Dapli che { $limit } correspundenzas + } +pdfjs-find-not-found = Impussibel da chattar l'expressiun + +## Predefined zoom values + +pdfjs-page-scale-width = Ladezza da la pagina +pdfjs-page-scale-fit = Entira pagina +pdfjs-page-scale-auto = Zoom automatic +pdfjs-page-scale-actual = Grondezza actuala +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pagina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ina errur è cumparida cun chargiar il PDF. +pdfjs-invalid-file-error = Datoteca PDF nunvalida u donnegiada. +pdfjs-missing-file-error = Datoteca PDF manconta. +pdfjs-unexpected-response-error = Resposta nunspetgada dal server. +pdfjs-rendering-error = Ina errur è cumparida cun visualisar questa pagina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Annotaziun da { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Endatescha il pled-clav per avrir questa datoteca da PDF. +pdfjs-password-invalid = Pled-clav nunvalid. Emprova anc ina giada. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Interrumper +pdfjs-web-fonts-disabled = Scrittiras dal web èn deactivadas: impussibel dad utilisar las scrittiras integradas en il PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Dissegnar +pdfjs-editor-ink-button-label = Dissegnar +pdfjs-editor-stamp-button = + .title = Agiuntar u modifitgar maletgs +pdfjs-editor-stamp-button-label = Agiuntar u modifitgar maletgs +pdfjs-editor-highlight-button = + .title = Marcar +pdfjs-editor-highlight-button-label = Marcar +pdfjs-highlight-floating-button1 = + .title = Marcar + .aria-label = Marcar +pdfjs-highlight-floating-button-label = Marcar + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Allontanar il dissegn +pdfjs-editor-remove-freetext-button = + .title = Allontanar il text +pdfjs-editor-remove-stamp-button = + .title = Allontanar la grafica +pdfjs-editor-remove-highlight-button = + .title = Allontanar l'emfasa + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colur +pdfjs-editor-free-text-size-input = Grondezza +pdfjs-editor-ink-color-input = Colur +pdfjs-editor-ink-thickness-input = Grossezza +pdfjs-editor-ink-opacity-input = Opacitad +pdfjs-editor-stamp-add-image-button = + .title = Agiuntar in maletg +pdfjs-editor-stamp-add-image-button-label = Agiuntar in maletg +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grossezza +pdfjs-editor-free-highlight-thickness-title = + .title = Midar la grossezza cun relevar elements betg textuals +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Editur da text + .default-content = Cumenza a tippar… +pdfjs-free-text = + .aria-label = Editur da text +pdfjs-free-text-default-content = Cumenzar a tippar… +pdfjs-ink = + .aria-label = Editur dissegn +pdfjs-ink-canvas = + .aria-label = Maletg creà da l'utilisader + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Text alternativ +pdfjs-editor-alt-text-edit-button = + .aria-label = Modifitgar il text alternativ +pdfjs-editor-alt-text-edit-button-label = Modifitgar il text alternativ +pdfjs-editor-alt-text-dialog-label = Tscherner ina opziun +pdfjs-editor-alt-text-dialog-description = Il text alternativ (alt text) gida en cas che persunas na vesan betg il maletg u sch'i na reussescha betg d'al chargiar. +pdfjs-editor-alt-text-add-description-label = Agiuntar ina descripziun +pdfjs-editor-alt-text-add-description-description = Scriva idealmain 1-2 frasas che descrivan l'object, la situaziun u las acziuns. +pdfjs-editor-alt-text-mark-decorative-label = Marcar sco decorativ +pdfjs-editor-alt-text-mark-decorative-description = Quai vegn duvrà per maletgs ornamentals, sco urs u filigranas. +pdfjs-editor-alt-text-cancel-button = Interrumper +pdfjs-editor-alt-text-save-button = Memorisar +pdfjs-editor-alt-text-decorative-tooltip = Marcà sco decorativ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Per exempel: «In um giuven sesa a maisa per mangiar in past» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Text alternativ + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Chantun sura a sanestra — redimensiunar +pdfjs-editor-resizer-label-top-middle = Sura amez — redimensiunar +pdfjs-editor-resizer-label-top-right = Chantun sura a dretga — redimensiunar +pdfjs-editor-resizer-label-middle-right = Da vart dretga amez — redimensiunar +pdfjs-editor-resizer-label-bottom-right = Chantun sut a dretga — redimensiunar +pdfjs-editor-resizer-label-bottom-middle = Sutvart amez — redimensiunar +pdfjs-editor-resizer-label-bottom-left = Chantun sut a sanestra — redimensiunar +pdfjs-editor-resizer-label-middle-left = Vart sanestra amez — redimensiunar +pdfjs-editor-resizer-top-left = + .aria-label = Chantun sura a sanestra — redimensiunar +pdfjs-editor-resizer-top-middle = + .aria-label = Sura amez — redimensiunar +pdfjs-editor-resizer-top-right = + .aria-label = Chantun sura a dretga — redimensiunar +pdfjs-editor-resizer-middle-right = + .aria-label = Da vart dretga amez — redimensiunar +pdfjs-editor-resizer-bottom-right = + .aria-label = Chantun sut a dretga — redimensiunar +pdfjs-editor-resizer-bottom-middle = + .aria-label = Sutvart amez — redimensiunar +pdfjs-editor-resizer-bottom-left = + .aria-label = Chantun sut a sanestra — redimensiunar +pdfjs-editor-resizer-middle-left = + .aria-label = Vart sanestra amez — redimensiunar + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Colur per l'emfasa +pdfjs-editor-colorpicker-button = + .title = Midar la colur +pdfjs-editor-colorpicker-dropdown = + .aria-label = Colurs disponiblas +pdfjs-editor-colorpicker-yellow = + .title = Mellen +pdfjs-editor-colorpicker-green = + .title = Verd +pdfjs-editor-colorpicker-blue = + .title = Blau +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Cotschen + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Mussar tut +pdfjs-editor-highlight-show-all-button = + .title = Mussar tut + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Modifitgar il text alternativ (descripziun dal maletg) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Agiuntar in text alternativ (descripziun dal maletg) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Scriva qua tia descripziun… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Curta descripziun per persunas che na vesan betg il maletg u per cass en ils quals il maletg na vegn betg chargià. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Quest text alternativ è vegnì creà automaticamain ed è eventualmain nunprecis. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Ulteriuras infurmaziuns +pdfjs-editor-new-alt-text-create-automatically-button-label = Crear automaticamain il text alternativ +pdfjs-editor-new-alt-text-not-now-button = Betg ussa +pdfjs-editor-new-alt-text-error-title = I n’è betg reussì da crear automaticamain il text alternativ +pdfjs-editor-new-alt-text-error-description = Scriva per plaschair tes agen text alternativ u emprova pli tard anc ina giada. +pdfjs-editor-new-alt-text-error-close-button = Serrar +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Telechargiar il model IA da text alternativ ({ $downloadedSize } da { $totalSize } MB) + .aria-valuetext = Telechargiar il model IA da text alternativ ({ $downloadedSize } da { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Agiuntà text alternativ +pdfjs-editor-new-alt-text-added-button-label = Text alternativ agiuntà +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Text alternativ manca +pdfjs-editor-new-alt-text-missing-button-label = Text alternativ manca +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Repassar il text alternativ +pdfjs-editor-new-alt-text-to-review-button-label = Repassar il text alternativ +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creà automaticamain: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Parameters dal text alternativ da maletgs +pdfjs-image-alt-text-settings-button-label = Parameters dal text alternativ da maletgs +pdfjs-editor-alt-text-settings-dialog-label = Parameters dal text alternativ da maletgs +pdfjs-editor-alt-text-settings-automatic-title = Text alternativ automatic +pdfjs-editor-alt-text-settings-create-model-button-label = Crear automaticamain text alternativ +pdfjs-editor-alt-text-settings-create-model-description = Propona descripziuns per gidar a persunas che na vesan betg il maletg u per cass en ils quals il maletg na vegn betg chargià. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model IA da text alternativ ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Vegn exequì localmain sin tes apparat per che tias datas restian privatas. Necessari per text alternativ automatic. +pdfjs-editor-alt-text-settings-delete-model-button = Stizzar +pdfjs-editor-alt-text-settings-download-model-button = Telechargiar +pdfjs-editor-alt-text-settings-downloading-model-button = Telechargiar… +pdfjs-editor-alt-text-settings-editor-title = Editur per text alternativ +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mussar l’editur per text alternativ directamain cun agiuntar in maletg +pdfjs-editor-alt-text-settings-show-dialog-description = Ta gida a garantir che tut tes maletgs hajan in text alternativ. +pdfjs-editor-alt-text-settings-close-button = Serrar + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Allontanà la marcaziun +pdfjs-editor-undo-bar-message-freetext = Allontanà il text +pdfjs-editor-undo-bar-message-ink = Allontanà il dissegn +pdfjs-editor-undo-bar-message-stamp = Allontanà il maletg +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } annotaziun allontanada + *[other] { $count } annotaziuns allontanadas + } +pdfjs-editor-undo-bar-undo-button = + .title = Revocar +pdfjs-editor-undo-bar-undo-button-label = Revocar +pdfjs-editor-undo-bar-close-button = + .title = Serrar +pdfjs-editor-undo-bar-close-button-label = Serrar diff --git a/public/assets/pdfjs/locale/ro/viewer.ftl b/public/assets/pdfjs/locale/ro/viewer.ftl new file mode 100644 index 0000000..7c6f0b6 --- /dev/null +++ b/public/assets/pdfjs/locale/ro/viewer.ftl @@ -0,0 +1,251 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pagina precedentă +pdfjs-previous-button-label = Înapoi +pdfjs-next-button = + .title = Pagina următoare +pdfjs-next-button-label = Înainte +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pagina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = din { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } din { $pagesCount }) +pdfjs-zoom-out-button = + .title = Micșorează +pdfjs-zoom-out-button-label = Micșorează +pdfjs-zoom-in-button = + .title = Mărește +pdfjs-zoom-in-button-label = Mărește +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Comută la modul de prezentare +pdfjs-presentation-mode-button-label = Mod de prezentare +pdfjs-open-file-button = + .title = Deschide un fișier +pdfjs-open-file-button-label = Deschide +pdfjs-print-button = + .title = Tipărește +pdfjs-print-button-label = Tipărește + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Instrumente +pdfjs-tools-button-label = Instrumente +pdfjs-first-page-button = + .title = Mergi la prima pagină +pdfjs-first-page-button-label = Mergi la prima pagină +pdfjs-last-page-button = + .title = Mergi la ultima pagină +pdfjs-last-page-button-label = Mergi la ultima pagină +pdfjs-page-rotate-cw-button = + .title = Rotește în sensul acelor de ceas +pdfjs-page-rotate-cw-button-label = Rotește în sensul acelor de ceas +pdfjs-page-rotate-ccw-button = + .title = Rotește în sens invers al acelor de ceas +pdfjs-page-rotate-ccw-button-label = Rotește în sens invers al acelor de ceas +pdfjs-cursor-text-select-tool-button = + .title = Activează instrumentul de selecție a textului +pdfjs-cursor-text-select-tool-button-label = Instrumentul de selecție a textului +pdfjs-cursor-hand-tool-button = + .title = Activează instrumentul mână +pdfjs-cursor-hand-tool-button-label = Unealta mână +pdfjs-scroll-vertical-button = + .title = Folosește derularea verticală +pdfjs-scroll-vertical-button-label = Derulare verticală +pdfjs-scroll-horizontal-button = + .title = Folosește derularea orizontală +pdfjs-scroll-horizontal-button-label = Derulare orizontală +pdfjs-scroll-wrapped-button = + .title = Folosește derularea încadrată +pdfjs-scroll-wrapped-button-label = Derulare încadrată +pdfjs-spread-none-button = + .title = Nu uni paginile broșate +pdfjs-spread-none-button-label = Fără pagini broșate +pdfjs-spread-odd-button = + .title = Unește paginile broșate începând cu cele impare +pdfjs-spread-odd-button-label = Broșare pagini impare +pdfjs-spread-even-button = + .title = Unește paginile broșate începând cu cele pare +pdfjs-spread-even-button-label = Broșare pagini pare + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Proprietățile documentului… +pdfjs-document-properties-button-label = Proprietățile documentului… +pdfjs-document-properties-file-name = Numele fișierului: +pdfjs-document-properties-file-size = Mărimea fișierului: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } byți) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byți) +pdfjs-document-properties-title = Titlu: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Subiect: +pdfjs-document-properties-keywords = Cuvinte cheie: +pdfjs-document-properties-creation-date = Data creării: +pdfjs-document-properties-modification-date = Data modificării: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Autor: +pdfjs-document-properties-producer = Producător PDF: +pdfjs-document-properties-version = Versiune PDF: +pdfjs-document-properties-page-count = Număr de pagini: +pdfjs-document-properties-page-size = Mărimea paginii: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = verticală +pdfjs-document-properties-page-size-orientation-landscape = orizontală +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Literă +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vizualizare web rapidă: +pdfjs-document-properties-linearized-yes = Da +pdfjs-document-properties-linearized-no = Nu +pdfjs-document-properties-close-button = Închide + +## Print + +pdfjs-print-progress-message = Se pregătește documentul pentru tipărire… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Renunță +pdfjs-printing-not-supported = Avertisment: Tipărirea nu este suportată în totalitate de acest browser. +pdfjs-printing-not-ready = Avertisment: PDF-ul nu este încărcat complet pentru tipărire. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Comută bara laterală +pdfjs-toggle-sidebar-button-label = Comută bara laterală +pdfjs-document-outline-button = + .title = Afișează schița documentului (dublu-clic pentru a extinde/restrânge toate elementele) +pdfjs-document-outline-button-label = Schița documentului +pdfjs-attachments-button = + .title = Afișează atașamentele +pdfjs-attachments-button-label = Atașamente +pdfjs-thumbs-button = + .title = Afișează miniaturi +pdfjs-thumbs-button-label = Miniaturi +pdfjs-findbar-button = + .title = Caută în document +pdfjs-findbar-button-label = Caută + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pagina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura paginii { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Caută + .placeholder = Caută în document… +pdfjs-find-previous-button = + .title = Mergi la apariția anterioară a textului +pdfjs-find-previous-button-label = Înapoi +pdfjs-find-next-button = + .title = Mergi la apariția următoare a textului +pdfjs-find-next-button-label = Înainte +pdfjs-find-highlight-checkbox = Evidențiază toate aparițiile +pdfjs-find-match-case-checkbox-label = Ține cont de majuscule și minuscule +pdfjs-find-entire-word-checkbox-label = Cuvinte întregi +pdfjs-find-reached-top = Am ajuns la începutul documentului, continuă de la sfârșit +pdfjs-find-reached-bottom = Am ajuns la sfârșitul documentului, continuă de la început +pdfjs-find-not-found = Nu s-a găsit textul + +## Predefined zoom values + +pdfjs-page-scale-width = Lățime pagină +pdfjs-page-scale-fit = Potrivire la pagină +pdfjs-page-scale-auto = Zoom automat +pdfjs-page-scale-actual = Mărime reală +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = A intervenit o eroare la încărcarea PDF-ului. +pdfjs-invalid-file-error = Fișier PDF nevalid sau corupt. +pdfjs-missing-file-error = Fișier PDF lipsă. +pdfjs-unexpected-response-error = Răspuns neașteptat de la server. +pdfjs-rendering-error = A intervenit o eroare la randarea paginii. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Adnotare { $type }] + +## Password + +pdfjs-password-label = Introdu parola pentru a deschide acest fișier PDF. +pdfjs-password-invalid = Parolă nevalidă. Te rugăm să încerci din nou. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Renunță +pdfjs-web-fonts-disabled = Fonturile web sunt dezactivate: nu se pot folosi fonturile PDF încorporate. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ru/viewer.ftl b/public/assets/pdfjs/locale/ru/viewer.ftl new file mode 100644 index 0000000..81c2f41 --- /dev/null +++ b/public/assets/pdfjs/locale/ru/viewer.ftl @@ -0,0 +1,518 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Предыдущая страница +pdfjs-previous-button-label = Предыдущая +pdfjs-next-button = + .title = Следующая страница +pdfjs-next-button-label = Следующая +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Страница +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = из { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } из { $pagesCount }) +pdfjs-zoom-out-button = + .title = Уменьшить +pdfjs-zoom-out-button-label = Уменьшить +pdfjs-zoom-in-button = + .title = Увеличить +pdfjs-zoom-in-button-label = Увеличить +pdfjs-zoom-select = + .title = Масштаб +pdfjs-presentation-mode-button = + .title = Перейти в режим презентации +pdfjs-presentation-mode-button-label = Режим презентации +pdfjs-open-file-button = + .title = Открыть файл +pdfjs-open-file-button-label = Открыть +pdfjs-print-button = + .title = Печать +pdfjs-print-button-label = Печать +pdfjs-save-button = + .title = Сохранить +pdfjs-save-button-label = Сохранить +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Загрузить +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Загрузить +pdfjs-bookmark-button = + .title = Текущая страница (просмотр URL-адреса с текущей страницы) +pdfjs-bookmark-button-label = Текущая страница + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Инструменты +pdfjs-tools-button-label = Инструменты +pdfjs-first-page-button = + .title = Перейти на первую страницу +pdfjs-first-page-button-label = Перейти на первую страницу +pdfjs-last-page-button = + .title = Перейти на последнюю страницу +pdfjs-last-page-button-label = Перейти на последнюю страницу +pdfjs-page-rotate-cw-button = + .title = Повернуть по часовой стрелке +pdfjs-page-rotate-cw-button-label = Повернуть по часовой стрелке +pdfjs-page-rotate-ccw-button = + .title = Повернуть против часовой стрелки +pdfjs-page-rotate-ccw-button-label = Повернуть против часовой стрелки +pdfjs-cursor-text-select-tool-button = + .title = Включить Инструмент «Выделение текста» +pdfjs-cursor-text-select-tool-button-label = Инструмент «Выделение текста» +pdfjs-cursor-hand-tool-button = + .title = Включить Инструмент «Рука» +pdfjs-cursor-hand-tool-button-label = Инструмент «Рука» +pdfjs-scroll-page-button = + .title = Использовать прокрутку страниц +pdfjs-scroll-page-button-label = Прокрутка страниц +pdfjs-scroll-vertical-button = + .title = Использовать вертикальную прокрутку +pdfjs-scroll-vertical-button-label = Вертикальная прокрутка +pdfjs-scroll-horizontal-button = + .title = Использовать горизонтальную прокрутку +pdfjs-scroll-horizontal-button-label = Горизонтальная прокрутка +pdfjs-scroll-wrapped-button = + .title = Использовать масштабируемую прокрутку +pdfjs-scroll-wrapped-button-label = Масштабируемая прокрутка +pdfjs-spread-none-button = + .title = Не использовать режим разворотов страниц +pdfjs-spread-none-button-label = Без разворотов страниц +pdfjs-spread-odd-button = + .title = Развороты начинаются с нечётных номеров страниц +pdfjs-spread-odd-button-label = Нечётные страницы слева +pdfjs-spread-even-button = + .title = Развороты начинаются с чётных номеров страниц +pdfjs-spread-even-button-label = Чётные страницы слева + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Свойства документа… +pdfjs-document-properties-button-label = Свойства документа… +pdfjs-document-properties-file-name = Имя файла: +pdfjs-document-properties-file-size = Размер файла: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } КБ ({ $b } байт) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байт) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } байт) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байт) +pdfjs-document-properties-title = Заголовок: +pdfjs-document-properties-author = Автор: +pdfjs-document-properties-subject = Тема: +pdfjs-document-properties-keywords = Ключевые слова: +pdfjs-document-properties-creation-date = Дата создания: +pdfjs-document-properties-modification-date = Дата изменения: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Приложение: +pdfjs-document-properties-producer = Производитель PDF: +pdfjs-document-properties-version = Версия PDF: +pdfjs-document-properties-page-count = Число страниц: +pdfjs-document-properties-page-size = Размер страницы: +pdfjs-document-properties-page-size-unit-inches = дюймов +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = книжная +pdfjs-document-properties-page-size-orientation-landscape = альбомная +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Быстрый просмотр в Web: +pdfjs-document-properties-linearized-yes = Да +pdfjs-document-properties-linearized-no = Нет +pdfjs-document-properties-close-button = Закрыть + +## Print + +pdfjs-print-progress-message = Подготовка документа к печати… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Отмена +pdfjs-printing-not-supported = Предупреждение: В этом браузере не полностью поддерживается печать. +pdfjs-printing-not-ready = Предупреждение: PDF не полностью загружен для печати. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Показать/скрыть боковую панель +pdfjs-toggle-sidebar-notification-button = + .title = Показать/скрыть боковую панель (документ имеет содержание/вложения/слои) +pdfjs-toggle-sidebar-button-label = Показать/скрыть боковую панель +pdfjs-document-outline-button = + .title = Показать содержание документа (двойной щелчок, чтобы развернуть/свернуть все элементы) +pdfjs-document-outline-button-label = Содержание документа +pdfjs-attachments-button = + .title = Показать вложения +pdfjs-attachments-button-label = Вложения +pdfjs-layers-button = + .title = Показать слои (дважды щёлкните, чтобы сбросить все слои к состоянию по умолчанию) +pdfjs-layers-button-label = Слои +pdfjs-thumbs-button = + .title = Показать миниатюры +pdfjs-thumbs-button-label = Миниатюры +pdfjs-current-outline-item-button = + .title = Найти текущий элемент структуры +pdfjs-current-outline-item-button-label = Текущий элемент структуры +pdfjs-findbar-button = + .title = Найти в документе +pdfjs-findbar-button-label = Найти +pdfjs-additional-layers = Дополнительные слои + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Страница { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Миниатюра страницы { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Найти + .placeholder = Найти в документе… +pdfjs-find-previous-button = + .title = Найти предыдущее вхождение фразы в текст +pdfjs-find-previous-button-label = Назад +pdfjs-find-next-button = + .title = Найти следующее вхождение фразы в текст +pdfjs-find-next-button-label = Далее +pdfjs-find-highlight-checkbox = Подсветить все +pdfjs-find-match-case-checkbox-label = С учётом регистра +pdfjs-find-match-diacritics-checkbox-label = С учётом диакритических знаков +pdfjs-find-entire-word-checkbox-label = Слова целиком +pdfjs-find-reached-top = Достигнут верх документа, продолжено снизу +pdfjs-find-reached-bottom = Достигнут конец документа, продолжено сверху +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } из { $total } совпадения + [few] { $current } из { $total } совпадений + *[many] { $current } из { $total } совпадений + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Более { $limit } совпадения + [few] Более { $limit } совпадений + *[many] Более { $limit } совпадений + } +pdfjs-find-not-found = Фраза не найдена + +## Predefined zoom values + +pdfjs-page-scale-width = По ширине страницы +pdfjs-page-scale-fit = По размеру страницы +pdfjs-page-scale-auto = Автоматически +pdfjs-page-scale-actual = Реальный размер +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Страница { $page } + +## Loading indicator messages + +pdfjs-loading-error = При загрузке PDF произошла ошибка. +pdfjs-invalid-file-error = Некорректный или повреждённый PDF-файл. +pdfjs-missing-file-error = PDF-файл отсутствует. +pdfjs-unexpected-response-error = Неожиданный ответ сервера. +pdfjs-rendering-error = При создании страницы произошла ошибка. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Аннотация { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Введите пароль, чтобы открыть этот PDF-файл. +pdfjs-password-invalid = Неверный пароль. Пожалуйста, попробуйте снова. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Отмена +pdfjs-web-fonts-disabled = Веб-шрифты отключены: не удалось задействовать встроенные PDF-шрифты. + +## Editing + +pdfjs-editor-free-text-button = + .title = Текст +pdfjs-editor-free-text-button-label = Текст +pdfjs-editor-ink-button = + .title = Рисовать +pdfjs-editor-ink-button-label = Рисовать +pdfjs-editor-stamp-button = + .title = Добавить или изменить изображения +pdfjs-editor-stamp-button-label = Добавить или изменить изображения +pdfjs-editor-highlight-button = + .title = Выделение +pdfjs-editor-highlight-button-label = Выделение +pdfjs-highlight-floating-button1 = + .title = Выделение + .aria-label = Выделение +pdfjs-highlight-floating-button-label = Выделение + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Удалить рисунок +pdfjs-editor-remove-freetext-button = + .title = Удалить текст +pdfjs-editor-remove-stamp-button = + .title = Удалить изображение +pdfjs-editor-remove-highlight-button = + .title = Удалить выделение + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Цвет +pdfjs-editor-free-text-size-input = Размер +pdfjs-editor-ink-color-input = Цвет +pdfjs-editor-ink-thickness-input = Толщина +pdfjs-editor-ink-opacity-input = Прозрачность +pdfjs-editor-stamp-add-image-button = + .title = Добавить изображение +pdfjs-editor-stamp-add-image-button-label = Добавить изображение +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Толщина +pdfjs-editor-free-highlight-thickness-title = + .title = Изменить толщину при выделении элементов, кроме текста +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Текстовый редактор + .default-content = Начните ввод... +pdfjs-free-text = + .aria-label = Текстовый редактор +pdfjs-free-text-default-content = Начните вводить… +pdfjs-ink = + .aria-label = Редактор рисования +pdfjs-ink-canvas = + .aria-label = Созданное пользователем изображение + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Альтернативный текст +pdfjs-editor-alt-text-edit-button = + .aria-label = Изменить альтернативный текст +pdfjs-editor-alt-text-edit-button-label = Изменить альтернативный текст +pdfjs-editor-alt-text-dialog-label = Выберите вариант +pdfjs-editor-alt-text-dialog-description = Альтернативный текст помогает, когда люди не видят изображение или оно не загружается. +pdfjs-editor-alt-text-add-description-label = Добавить описание +pdfjs-editor-alt-text-add-description-description = Старайтесь составлять 1–2 предложения, описывающих предмет, обстановку или действия. +pdfjs-editor-alt-text-mark-decorative-label = Отметить как декоративное +pdfjs-editor-alt-text-mark-decorative-description = Используется для декоративных изображений, таких как рамки или водяные знаки. +pdfjs-editor-alt-text-cancel-button = Отменить +pdfjs-editor-alt-text-save-button = Сохранить +pdfjs-editor-alt-text-decorative-tooltip = Помечен как декоративный +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Например: «Молодой человек садится за стол, чтобы поесть» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Альтернативный текст + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Левый верхний угол — изменить размер +pdfjs-editor-resizer-label-top-middle = Вверху посередине — изменить размер +pdfjs-editor-resizer-label-top-right = Верхний правый угол — изменить размер +pdfjs-editor-resizer-label-middle-right = В центре справа — изменить размер +pdfjs-editor-resizer-label-bottom-right = Нижний правый угол — изменить размер +pdfjs-editor-resizer-label-bottom-middle = Внизу посередине — изменить размер +pdfjs-editor-resizer-label-bottom-left = Нижний левый угол — изменить размер +pdfjs-editor-resizer-label-middle-left = В центре слева — изменить размер +pdfjs-editor-resizer-top-left = + .aria-label = Левый верхний угол — изменить размер +pdfjs-editor-resizer-top-middle = + .aria-label = Вверху посередине — изменить размер +pdfjs-editor-resizer-top-right = + .aria-label = Верхний правый угол — изменить размер +pdfjs-editor-resizer-middle-right = + .aria-label = В центре справа — изменить размер +pdfjs-editor-resizer-bottom-right = + .aria-label = Нижний правый угол — изменить размер +pdfjs-editor-resizer-bottom-middle = + .aria-label = Внизу посередине — изменить размер +pdfjs-editor-resizer-bottom-left = + .aria-label = Нижний левый угол — изменить размер +pdfjs-editor-resizer-middle-left = + .aria-label = В центре слева — изменить размер + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Цвет выделения +pdfjs-editor-colorpicker-button = + .title = Изменить цвет +pdfjs-editor-colorpicker-dropdown = + .aria-label = Выбор цвета +pdfjs-editor-colorpicker-yellow = + .title = Жёлтый +pdfjs-editor-colorpicker-green = + .title = Зелёный +pdfjs-editor-colorpicker-blue = + .title = Синий +pdfjs-editor-colorpicker-pink = + .title = Розовый +pdfjs-editor-colorpicker-red = + .title = Красный + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Показать все +pdfjs-editor-highlight-show-all-button = + .title = Показать все + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Изменить альтернативный текст (описание изображения) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Добавить альтернативный текст (описание изображения) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Напишите здесь своё описание… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Короткое описание для людей, которые не видят изображение, или если изображение не загружается. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Этот альтернативный текст был создан автоматически и может быть неточным. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Подробнее +pdfjs-editor-new-alt-text-create-automatically-button-label = Автоматически создавать альтернативный текст +pdfjs-editor-new-alt-text-not-now-button = Не сейчас +pdfjs-editor-new-alt-text-error-title = Не удалось автоматически создать альтернативный текст +pdfjs-editor-new-alt-text-error-description = Пожалуйста, напишите свой альтернативный текст или попробуйте ещё раз позже. +pdfjs-editor-new-alt-text-error-close-button = Закрыть +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Загрузка модели ИИ для альтернативного текста ({ $downloadedSize } из { $totalSize } МБ) + .aria-valuetext = Загрузка модели ИИ для альтернативного текста ({ $downloadedSize } из { $totalSize } МБ) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Альтернативный текст добавлен +pdfjs-editor-new-alt-text-added-button-label = Альтернативный текст добавлен +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Отсутствует альтернативный текст +pdfjs-editor-new-alt-text-missing-button-label = Отсутствует альтернативный текст +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Оценить альтернативный текст +pdfjs-editor-new-alt-text-to-review-button-label = Оценить альтернативный текст +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Создано автоматически: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Настройки альтернативного текста для изображения +pdfjs-image-alt-text-settings-button-label = Настройки альтернативного текста для изображения +pdfjs-editor-alt-text-settings-dialog-label = Настройки альтернативного текста для изображения +pdfjs-editor-alt-text-settings-automatic-title = Автоматический альтернативный текст +pdfjs-editor-alt-text-settings-create-model-button-label = Автоматически создавать альтернативный текст +pdfjs-editor-alt-text-settings-create-model-description = Предлагает описания, чтобы помочь людям, которые не видят изображение, или если изображение не загружается. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = ИИ-модель альтернативного текста ({ $totalSize } МБ) +pdfjs-editor-alt-text-settings-ai-model-description = Запускается локально на вашем устройстве, поэтому ваши данные остаются конфиденциальными. Требуется для автоматического альтернативного текста. +pdfjs-editor-alt-text-settings-delete-model-button = Удалить +pdfjs-editor-alt-text-settings-download-model-button = Загрузить +pdfjs-editor-alt-text-settings-downloading-model-button = Загрузка… +pdfjs-editor-alt-text-settings-editor-title = Редактор альтернативного текста +pdfjs-editor-alt-text-settings-show-dialog-button-label = Сразу показывать редактор альтернативного текста при добавлении изображения +pdfjs-editor-alt-text-settings-show-dialog-description = Помогает вам убедиться, что все ваши изображения имеют альтернативный текст. +pdfjs-editor-alt-text-settings-close-button = Закрыть + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Выделение удалено +pdfjs-editor-undo-bar-message-freetext = Текст удалён +pdfjs-editor-undo-bar-message-ink = Рисунок удалён +pdfjs-editor-undo-bar-message-stamp = Изображение удалено +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } аннотация удалена + [few] { $count } аннотации удалены + *[many] { $count } аннотаций удалены + } +pdfjs-editor-undo-bar-undo-button = + .title = Отменить +pdfjs-editor-undo-bar-undo-button-label = Отменить +pdfjs-editor-undo-bar-close-button = + .title = Закрыть +pdfjs-editor-undo-bar-close-button-label = Закрыть diff --git a/public/assets/pdfjs/locale/sat/viewer.ftl b/public/assets/pdfjs/locale/sat/viewer.ftl new file mode 100644 index 0000000..2fbbc12 --- /dev/null +++ b/public/assets/pdfjs/locale/sat/viewer.ftl @@ -0,0 +1,325 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = ᱢᱟᱲᱟᱝ ᱥᱟᱦᱴᱟ +pdfjs-previous-button-label = ᱢᱟᱲᱟᱝᱟᱜ +pdfjs-next-button = + .title = ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢ ᱥᱟᱦᱴᱟ +pdfjs-next-button-label = ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ᱥᱟᱦᱴᱟ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = ᱨᱮᱭᱟᱜ { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } ᱠᱷᱚᱱ { $pagesCount }) +pdfjs-zoom-out-button = + .title = ᱦᱤᱲᱤᱧ ᱛᱮᱭᱟᱨ +pdfjs-zoom-out-button-label = ᱦᱤᱲᱤᱧ ᱛᱮᱭᱟᱨ +pdfjs-zoom-in-button = + .title = ᱢᱟᱨᱟᱝ ᱛᱮᱭᱟᱨ +pdfjs-zoom-in-button-label = ᱢᱟᱨᱟᱝ ᱛᱮᱭᱟᱨ +pdfjs-zoom-select = + .title = ᱡᱩᱢ +pdfjs-presentation-mode-button = + .title = ᱩᱫᱩᱜ ᱥᱚᱫᱚᱨ ᱚᱵᱚᱥᱛᱟ ᱨᱮ ᱚᱛᱟᱭ ᱢᱮ +pdfjs-presentation-mode-button-label = ᱩᱫᱩᱜ ᱥᱚᱫᱚᱨ ᱚᱵᱚᱥᱛᱟ ᱨᱮ +pdfjs-open-file-button = + .title = ᱨᱮᱫ ᱡᱷᱤᱡᱽ ᱢᱮ +pdfjs-open-file-button-label = ᱡᱷᱤᱡᱽ ᱢᱮ +pdfjs-print-button = + .title = ᱪᱷᱟᱯᱟ +pdfjs-print-button-label = ᱪᱷᱟᱯᱟ +pdfjs-save-button = + .title = ᱥᱟᱺᱪᱟᱣ ᱢᱮ +pdfjs-save-button-label = ᱥᱟᱺᱪᱟᱣ ᱢᱮ +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ᱰᱟᱣᱩᱱᱞᱚᱰ +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ᱰᱟᱣᱩᱱᱞᱚᱰ +pdfjs-bookmark-button = + .title = ᱱᱤᱛᱚᱜᱟᱜ ᱥᱟᱦᱴᱟ (ᱱᱤᱛᱚᱜᱟᱜ ᱥᱟᱦᱴᱟ ᱠᱷᱚᱱ URL ᱫᱮᱠᱷᱟᱣ ᱢᱮ) +pdfjs-bookmark-button-label = ᱱᱤᱛᱚᱜᱟᱜ ᱥᱟᱦᱴᱟ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = ᱦᱟᱹᱛᱤᱭᱟᱹᱨ ᱠᱚ +pdfjs-tools-button-label = ᱦᱟᱹᱛᱤᱭᱟᱹᱨ ᱠᱚ +pdfjs-first-page-button = + .title = ᱯᱩᱭᱞᱩ ᱥᱟᱦᱴᱟ ᱥᱮᱫ ᱪᱟᱞᱟᱜ ᱢᱮ +pdfjs-first-page-button-label = ᱯᱩᱭᱞᱩ ᱥᱟᱦᱴᱟ ᱥᱮᱫ ᱪᱟᱞᱟᱜ ᱢᱮ +pdfjs-last-page-button = + .title = ᱢᱩᱪᱟᱹᱫ ᱥᱟᱦᱴᱟ ᱥᱮᱫ ᱪᱟᱞᱟᱜ ᱢᱮ +pdfjs-last-page-button-label = ᱢᱩᱪᱟᱹᱫ ᱥᱟᱦᱴᱟ ᱥᱮᱫ ᱪᱟᱞᱟᱜ ᱢᱮ +pdfjs-page-rotate-cw-button = + .title = ᱜᱷᱚᱰᱤ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱟᱹᱪᱩᱨ +pdfjs-page-rotate-cw-button-label = ᱜᱷᱚᱰᱤ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱟᱹᱪᱩᱨ +pdfjs-page-rotate-ccw-button = + .title = ᱜᱷᱚᱰᱤ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱩᱞᱴᱟᱹ ᱟᱹᱪᱩᱨ +pdfjs-page-rotate-ccw-button-label = ᱜᱷᱚᱰᱤ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱩᱞᱴᱟᱹ ᱟᱹᱪᱩᱨ +pdfjs-cursor-text-select-tool-button = + .title = ᱚᱞ ᱵᱟᱪᱷᱟᱣ ᱦᱟᱹᱛᱤᱭᱟᱨ ᱮᱢ ᱪᱷᱚᱭ ᱢᱮ +pdfjs-cursor-text-select-tool-button-label = ᱚᱞ ᱵᱟᱪᱷᱟᱣ ᱦᱟᱹᱛᱤᱭᱟᱨ +pdfjs-cursor-hand-tool-button = + .title = ᱛᱤ ᱦᱟᱹᱛᱤᱭᱟᱨ ᱮᱢ ᱪᱷᱚᱭ ᱢᱮ +pdfjs-cursor-hand-tool-button-label = ᱛᱤ ᱦᱟᱹᱛᱤᱭᱟᱨ +pdfjs-scroll-page-button = + .title = ᱥᱟᱦᱴᱟ ᱜᱩᱲᱟᱹᱣ ᱵᱮᱵᱷᱟᱨ ᱢᱮ +pdfjs-scroll-page-button-label = ᱥᱟᱦᱴᱟ ᱜᱩᱲᱟᱹᱣ +pdfjs-scroll-vertical-button = + .title = ᱥᱤᱫᱽ ᱜᱩᱲᱟᱹᱣ ᱵᱮᱵᱷᱟᱨ ᱢᱮ +pdfjs-scroll-vertical-button-label = ᱥᱤᱫᱽ ᱜᱩᱲᱟᱹᱣ +pdfjs-scroll-horizontal-button = + .title = ᱜᱤᱛᱤᱡ ᱛᱮ ᱜᱩᱲᱟᱹᱣ ᱵᱮᱵᱷᱟᱨ ᱢᱮ +pdfjs-scroll-horizontal-button-label = ᱜᱤᱛᱤᱡ ᱛᱮ ᱜᱩᱲᱟᱹᱣ +pdfjs-scroll-wrapped-button = + .title = ᱞᱤᱯᱴᱟᱹᱣ ᱜᱩᱰᱨᱟᱹᱣ ᱵᱮᱵᱷᱟᱨ ᱢᱮ +pdfjs-scroll-wrapped-button-label = ᱞᱤᱯᱴᱟᱣ ᱜᱩᱰᱨᱟᱹᱣ +pdfjs-spread-none-button = + .title = ᱟᱞᱚᱢ ᱡᱚᱲᱟᱣ ᱟ ᱥᱟᱦᱴᱟ ᱫᱚ ᱯᱟᱥᱱᱟᱣᱜᱼᱟ +pdfjs-spread-none-button-label = ᱯᱟᱥᱱᱟᱣ ᱵᱟᱹᱱᱩᱜᱼᱟ +pdfjs-spread-odd-button = + .title = ᱥᱟᱦᱴᱟ ᱯᱟᱥᱱᱟᱣ ᱡᱚᱲᱟᱣ ᱢᱮ ᱡᱟᱦᱟᱸ ᱫᱚ ᱚᱰᱼᱮᱞ ᱥᱟᱦᱴᱟᱠᱚ ᱥᱟᱞᱟᱜ ᱮᱛᱦᱚᱵᱚᱜ ᱠᱟᱱᱟ +pdfjs-spread-odd-button-label = ᱚᱰ ᱯᱟᱥᱱᱟᱣ +pdfjs-spread-even-button = + .title = ᱥᱟᱦᱴᱟ ᱯᱟᱥᱱᱟᱣ ᱡᱚᱲᱟᱣ ᱢᱮ ᱡᱟᱦᱟᱸ ᱫᱚ ᱤᱣᱮᱱᱼᱮᱞ ᱥᱟᱦᱴᱟᱠᱚ ᱥᱟᱞᱟᱜ ᱮᱛᱦᱚᱵᱚᱜ ᱠᱟᱱᱟ +pdfjs-spread-even-button-label = ᱯᱟᱥᱱᱟᱣ ᱤᱣᱮᱱ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = ᱫᱚᱞᱤᱞ ᱜᱩᱱᱠᱚ … +pdfjs-document-properties-button-label = ᱫᱚᱞᱤᱞ ᱜᱩᱱᱠᱚ … +pdfjs-document-properties-file-name = ᱨᱮᱫᱽ ᱧᱩᱛᱩᱢ : +pdfjs-document-properties-file-size = ᱨᱮᱫᱽ ᱢᱟᱯ : +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } ᱵᱟᱭᱤᱴ ᱠᱚ) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } ᱵᱟᱭᱤᱴ ᱠᱚ) +pdfjs-document-properties-title = ᱧᱩᱛᱩᱢ : +pdfjs-document-properties-author = ᱚᱱᱚᱞᱤᱭᱟᱹ : +pdfjs-document-properties-subject = ᱵᱤᱥᱚᱭ : +pdfjs-document-properties-keywords = ᱠᱟᱹᱴᱷᱤ ᱥᱟᱵᱟᱫᱽ : +pdfjs-document-properties-creation-date = ᱛᱮᱭᱟᱨ ᱢᱟᱸᱦᱤᱛ : +pdfjs-document-properties-modification-date = ᱵᱚᱫᱚᱞ ᱦᱚᱪᱚ ᱢᱟᱹᱦᱤᱛ : +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ᱵᱮᱱᱟᱣᱤᱡ : +pdfjs-document-properties-producer = PDF ᱛᱮᱭᱟᱨ ᱚᱰᱚᱠᱤᱡ : +pdfjs-document-properties-version = PDF ᱵᱷᱟᱹᱨᱥᱚᱱ : +pdfjs-document-properties-page-count = ᱥᱟᱦᱴᱟ ᱞᱮᱠᱷᱟ : +pdfjs-document-properties-page-size = ᱥᱟᱦᱴᱟ ᱢᱟᱯ : +pdfjs-document-properties-page-size-unit-inches = ᱤᱧᱪ +pdfjs-document-properties-page-size-unit-millimeters = ᱢᱤᱢᱤ +pdfjs-document-properties-page-size-orientation-portrait = ᱯᱚᱴᱨᱮᱴ +pdfjs-document-properties-page-size-orientation-landscape = ᱞᱮᱱᱰᱥᱠᱮᱯ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = ᱪᱤᱴᱷᱤ +pdfjs-document-properties-page-size-name-legal = ᱠᱟᱹᱱᱩᱱᱤ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = ᱞᱚᱜᱚᱱ ᱣᱮᱵᱽ ᱧᱮᱞ : +pdfjs-document-properties-linearized-yes = ᱦᱚᱭ +pdfjs-document-properties-linearized-no = ᱵᱟᱝ +pdfjs-document-properties-close-button = ᱵᱚᱸᱫᱚᱭ ᱢᱮ + +## Print + +pdfjs-print-progress-message = ᱪᱷᱟᱯᱟ ᱞᱟᱹᱜᱤᱫ ᱫᱚᱞᱤᱞ ᱛᱮᱭᱟᱨᱚᱜ ᱠᱟᱱᱟ … +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ᱵᱟᱹᱰᱨᱟᱹ +pdfjs-printing-not-supported = ᱦᱚᱥᱤᱭᱟᱨ : ᱪᱷᱟᱯᱟ ᱱᱚᱣᱟ ᱯᱟᱱᱛᱮᱭᱟᱜ ᱫᱟᱨᱟᱭ ᱛᱮ ᱯᱩᱨᱟᱹᱣ ᱵᱟᱭ ᱜᱚᱲᱚᱣᱟᱠᱟᱱᱟ ᱾ +pdfjs-printing-not-ready = ᱦᱩᱥᱤᱭᱟᱹᱨ : ᱪᱷᱟᱯᱟ ᱞᱟᱹᱜᱤᱫ PDF ᱯᱩᱨᱟᱹ ᱵᱟᱭ ᱞᱟᱫᱮ ᱟᱠᱟᱱᱟ ᱾ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = ᱫᱷᱟᱨᱮᱵᱟᱨ ᱥᱮᱫ ᱩᱪᱟᱹᱲᱚᱜ ᱢᱮ +pdfjs-toggle-sidebar-notification-button = + .title = ᱫᱷᱟᱨᱮᱵᱟᱨ ᱥᱮᱫ ᱩᱪᱟᱹᱲᱚᱜ ᱢᱮ (ᱫᱚᱞᱤᱞ ᱨᱮ ᱟᱣᱴᱞᱟᱭᱤᱢ ᱢᱮᱱᱟᱜᱼᱟ/ᱞᱟᱪᱷᱟᱠᱚ/ᱯᱚᱨᱚᱛᱠᱚ) +pdfjs-toggle-sidebar-button-label = ᱫᱷᱟᱨᱮᱵᱟᱨ ᱥᱮᱫ ᱩᱪᱟᱹᱲᱚᱜ ᱢᱮ +pdfjs-document-outline-button = + .title = ᱫᱚᱞᱚᱞ ᱟᱣᱴᱞᱟᱭᱤᱱ ᱫᱮᱠᱷᱟᱣ ᱢᱮ (ᱡᱷᱚᱛᱚ ᱡᱤᱱᱤᱥᱠᱚ ᱵᱟᱨ ᱡᱮᱠᱷᱟ ᱚᱛᱟ ᱠᱮᱛᱮ ᱡᱷᱟᱹᱞ/ᱦᱩᱰᱤᱧ ᱪᱷᱚᱭ ᱢᱮ) +pdfjs-document-outline-button-label = ᱫᱚᱞᱤᱞ ᱛᱮᱭᱟᱨ ᱛᱮᱫ +pdfjs-attachments-button = + .title = ᱞᱟᱴᱷᱟ ᱥᱮᱞᱮᱫ ᱠᱚ ᱩᱫᱩᱜᱽ ᱢᱮ +pdfjs-attachments-button-label = ᱞᱟᱴᱷᱟ ᱥᱮᱞᱮᱫ ᱠᱚ +pdfjs-layers-button = + .title = ᱯᱚᱨᱚᱛ ᱫᱮᱠᱷᱟᱣ ᱢᱮ (ᱢᱩᱞ ᱡᱟᱭᱜᱟ ᱛᱮ ᱡᱷᱚᱛᱚ ᱯᱚᱨᱚᱛᱠᱚ ᱨᱤᱥᱮᱴ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱨ ᱡᱮᱠᱷᱟ ᱚᱛᱚᱭ ᱢᱮ) +pdfjs-layers-button-label = ᱯᱚᱨᱚᱛᱠᱚ +pdfjs-thumbs-button = + .title = ᱪᱤᱛᱟᱹᱨ ᱟᱦᱞᱟ ᱠᱚ ᱩᱫᱩᱜᱽ ᱢᱮ +pdfjs-thumbs-button-label = ᱪᱤᱛᱟᱹᱨ ᱟᱦᱞᱟ ᱠᱚ +pdfjs-current-outline-item-button = + .title = ᱱᱤᱛᱚᱜᱟᱜ ᱟᱣᱴᱞᱟᱭᱤᱱ ᱡᱟᱱᱤᱥ ᱯᱟᱱᱛᱮ ᱢᱮ +pdfjs-current-outline-item-button-label = ᱱᱤᱛᱚᱜᱟᱜ ᱟᱣᱴᱞᱟᱭᱤᱱ ᱡᱟᱱᱤᱥ +pdfjs-findbar-button = + .title = ᱫᱚᱞᱤᱞ ᱨᱮ ᱯᱟᱱᱛᱮ +pdfjs-findbar-button-label = ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ +pdfjs-additional-layers = ᱵᱟᱹᱲᱛᱤ ᱯᱚᱨᱚᱛᱠᱚ + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } ᱥᱟᱦᱴᱟ +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } ᱥᱟᱦᱴᱟ ᱨᱮᱭᱟᱜ ᱪᱤᱛᱟᱹᱨ ᱟᱦᱞᱟ + +## Find panel button title and messages + +pdfjs-find-input = + .title = ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ + .placeholder = ᱫᱚᱞᱤᱞ ᱨᱮ ᱯᱟᱱᱛᱮ ᱢᱮ … +pdfjs-find-previous-button = + .title = ᱟᱭᱟᱛ ᱦᱤᱸᱥ ᱨᱮᱭᱟᱜ ᱯᱟᱹᱦᱤᱞ ᱥᱮᱫᱟᱜ ᱚᱰᱚᱠ ᱧᱟᱢ ᱢᱮ +pdfjs-find-previous-button-label = ᱢᱟᱲᱟᱝᱟᱜ +pdfjs-find-next-button = + .title = ᱟᱭᱟᱛ ᱦᱤᱸᱥ ᱨᱮᱭᱟᱜ ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢ ᱚᱰᱚᱠ ᱧᱟᱢ ᱢᱮ +pdfjs-find-next-button-label = ᱤᱱᱟᱹ ᱛᱟᱭᱚᱢ +pdfjs-find-highlight-checkbox = ᱡᱷᱚᱛᱚ ᱩᱫᱩᱜ ᱨᱟᱠᱟᱵ +pdfjs-find-match-case-checkbox-label = ᱡᱚᱲ ᱠᱟᱛᱷᱟ +pdfjs-find-match-diacritics-checkbox-label = ᱵᱤᱥᱮᱥᱚᱠ ᱠᱚ ᱢᱮᱲᱟᱣ ᱢᱮ +pdfjs-find-entire-word-checkbox-label = ᱡᱷᱚᱛᱚ ᱟᱹᱲᱟᱹᱠᱚ +pdfjs-find-reached-top = ᱫᱚᱞᱤᱞ ᱨᱮᱭᱟᱜ ᱪᱤᱴ ᱨᱮ ᱥᱮᱴᱮᱨ, ᱞᱟᱛᱟᱨ ᱠᱷᱚᱱ ᱞᱮᱛᱟᱲ +pdfjs-find-reached-bottom = ᱫᱚᱞᱤᱞ ᱨᱮᱭᱟᱜ ᱢᱩᱪᱟᱹᱫ ᱨᱮ ᱥᱮᱴᱮᱨ, ᱪᱚᱴ ᱠᱷᱚᱱ ᱞᱮᱛᱟᱲ +pdfjs-find-not-found = ᱛᱚᱯᱚᱞ ᱫᱚᱱᱚᱲ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ + +## Predefined zoom values + +pdfjs-page-scale-width = ᱥᱟᱦᱴᱟ ᱚᱥᱟᱨ +pdfjs-page-scale-fit = ᱥᱟᱦᱴᱟ ᱠᱷᱟᱯ +pdfjs-page-scale-auto = ᱟᱡᱼᱟᱡ ᱛᱮ ᱦᱩᱰᱤᱧ ᱞᱟᱹᱴᱩ ᱛᱮᱭᱟᱨ +pdfjs-page-scale-actual = ᱴᱷᱤᱠ ᱢᱟᱨᱟᱝ ᱛᱮᱫ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = { $page } ᱥᱟᱦᱴᱟ + +## Loading indicator messages + +pdfjs-loading-error = PDF ᱞᱟᱫᱮ ᱡᱚᱦᱚᱜ ᱢᱤᱫ ᱵᱷᱩᱞ ᱦᱩᱭ ᱮᱱᱟ ᱾ +pdfjs-invalid-file-error = ᱵᱟᱝ ᱵᱟᱛᱟᱣ ᱟᱨᱵᱟᱝᱠᱷᱟᱱ ᱰᱤᱜᱟᱹᱣ PDF ᱨᱮᱫᱽ ᱾ +pdfjs-missing-file-error = ᱟᱫᱟᱜ PDF ᱨᱮᱫᱽ ᱾ +pdfjs-unexpected-response-error = ᱵᱟᱝᱵᱩᱡᱷ ᱥᱚᱨᱵᱷᱚᱨ ᱛᱮᱞᱟ ᱾ +pdfjs-rendering-error = ᱥᱟᱦᱴᱟ ᱮᱢ ᱡᱚᱦᱚᱠ ᱢᱤᱫ ᱵᱷᱩᱞ ᱦᱩᱭ ᱮᱱᱟ ᱾ + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } ᱢᱚᱱᱛᱚ ᱮᱢ] + +## Password + +pdfjs-password-label = ᱱᱚᱶᱟ PDF ᱨᱮᱫᱽ ᱡᱷᱤᱡᱽ ᱞᱟᱹᱜᱤᱫ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱟᱫᱮᱨ ᱢᱮ ᱾ +pdfjs-password-invalid = ᱵᱷᱩᱞ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫᱽ ᱾ ᱫᱟᱭᱟᱠᱟᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾ +pdfjs-password-ok-button = ᱴᱷᱤᱠ +pdfjs-password-cancel-button = ᱵᱟᱹᱰᱨᱟᱹ +pdfjs-web-fonts-disabled = ᱣᱮᱵᱽ ᱪᱤᱠᱤ ᱵᱟᱝ ᱦᱩᱭ ᱦᱚᱪᱚ ᱠᱟᱱᱟ : ᱵᱷᱤᱛᱤᱨ ᱛᱷᱟᱯᱚᱱ PDF ᱪᱤᱠᱤ ᱵᱮᱵᱷᱟᱨ ᱵᱟᱝ ᱦᱩᱭ ᱠᱮᱭᱟ ᱾ + +## Editing + +pdfjs-editor-free-text-button = + .title = ᱚᱞ +pdfjs-editor-free-text-button-label = ᱚᱞ +pdfjs-editor-ink-button = + .title = ᱛᱮᱭᱟᱨ +pdfjs-editor-ink-button-label = ᱛᱮᱭᱟᱨ +pdfjs-editor-stamp-button = + .title = ᱪᱤᱛᱟᱹᱨᱠᱚ ᱥᱮᱞᱮᱫ ᱥᱮ ᱥᱟᱯᱲᱟᱣ ᱢᱮ +pdfjs-editor-stamp-button-label = ᱪᱤᱛᱟᱹᱨᱠᱚ ᱥᱮᱞᱮᱫ ᱥᱮ ᱥᱟᱯᱲᱟᱣ ᱢᱮ + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = ᱨᱚᱝ +pdfjs-editor-free-text-size-input = ᱢᱟᱯ +pdfjs-editor-ink-color-input = ᱨᱚᱝ +pdfjs-editor-ink-thickness-input = ᱢᱚᱴᱟ +pdfjs-editor-ink-opacity-input = ᱟᱨᱯᱟᱨ +pdfjs-editor-stamp-add-image-button = + .title = ᱪᱤᱛᱟᱹᱨ ᱥᱮᱞᱮᱫ ᱢᱮ +pdfjs-editor-stamp-add-image-button-label = ᱪᱤᱛᱟᱹᱨ ᱥᱮᱞᱮᱫ ᱢᱮ +pdfjs-free-text = + .aria-label = ᱚᱞ ᱥᱟᱯᱲᱟᱣᱤᱭᱟᱹ +pdfjs-free-text-default-content = ᱚᱞ ᱮᱛᱦᱚᱵ ᱢᱮ … +pdfjs-ink = + .aria-label = ᱛᱮᱭᱟᱨ ᱥᱟᱯᱲᱟᱣᱤᱭᱟᱹ +pdfjs-ink-canvas = + .aria-label = ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱛᱮᱭᱟᱨ ᱠᱟᱫ ᱪᱤᱛᱟᱹᱨ + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/sc/viewer.ftl b/public/assets/pdfjs/locale/sc/viewer.ftl new file mode 100644 index 0000000..1137c2b --- /dev/null +++ b/public/assets/pdfjs/locale/sc/viewer.ftl @@ -0,0 +1,367 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pàgina anteriore +pdfjs-previous-button-label = S'ischeda chi b'est primu +pdfjs-next-button = + .title = Pàgina imbeniente +pdfjs-next-button-label = Imbeniente +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pàgina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = de { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } de { $pagesCount }) +pdfjs-zoom-out-button = + .title = Impitica +pdfjs-zoom-out-button-label = Impitica +pdfjs-zoom-in-button = + .title = Ismànnia +pdfjs-zoom-in-button-label = Ismànnia +pdfjs-zoom-select = + .title = Ismànnia +pdfjs-presentation-mode-button = + .title = Cola a sa modalidade de presentatzione +pdfjs-presentation-mode-button-label = Modalidade de presentatzione +pdfjs-open-file-button = + .title = Aberi s'archìviu +pdfjs-open-file-button-label = Abertu +pdfjs-print-button = + .title = Imprenta +pdfjs-print-button-label = Imprenta +pdfjs-save-button = + .title = Sarva +pdfjs-save-button-label = Sarva +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Iscàrriga +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Iscàrriga +pdfjs-bookmark-button = + .title = Pàgina atuale (ammustra s’URL de sa pàgina atuale) +pdfjs-bookmark-button-label = Pàgina atuale + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Istrumentos +pdfjs-tools-button-label = Istrumentos +pdfjs-first-page-button = + .title = Bae a sa prima pàgina +pdfjs-first-page-button-label = Bae a sa prima pàgina +pdfjs-last-page-button = + .title = Bae a s'ùrtima pàgina +pdfjs-last-page-button-label = Bae a s'ùrtima pàgina +pdfjs-page-rotate-cw-button = + .title = Gira in sensu oràriu +pdfjs-page-rotate-cw-button-label = Gira in sensu oràriu +pdfjs-page-rotate-ccw-button = + .title = Gira in sensu anti-oràriu +pdfjs-page-rotate-ccw-button-label = Gira in sensu anti-oràriu +pdfjs-cursor-text-select-tool-button = + .title = Ativa s'aina de seletzione de testu +pdfjs-cursor-text-select-tool-button-label = Aina de seletzione de testu +pdfjs-cursor-hand-tool-button = + .title = Ativa s'aina de manu +pdfjs-cursor-hand-tool-button-label = Aina de manu +pdfjs-scroll-page-button = + .title = Imprea s'iscurrimentu de pàgina +pdfjs-scroll-page-button-label = Iscurrimentu de pàgina +pdfjs-scroll-vertical-button = + .title = Imprea s'iscurrimentu verticale +pdfjs-scroll-vertical-button-label = Iscurrimentu verticale +pdfjs-scroll-horizontal-button = + .title = Imprea s'iscurrimentu orizontale +pdfjs-scroll-horizontal-button-label = Iscurrimentu orizontale +pdfjs-scroll-wrapped-button = + .title = Imprea s'iscurrimentu continu +pdfjs-scroll-wrapped-button-label = Iscurrimentu continu + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Propiedades de su documentu… +pdfjs-document-properties-button-label = Propiedades de su documentu… +pdfjs-document-properties-file-name = Nòmine de s'archìviu: +pdfjs-document-properties-file-size = Mannària de s'archìviu: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Tìtulu: +pdfjs-document-properties-author = Autoria: +pdfjs-document-properties-subject = Ogetu: +pdfjs-document-properties-keywords = Faeddos crae: +pdfjs-document-properties-creation-date = Data de creatzione: +pdfjs-document-properties-modification-date = Data de modìfica: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Creatzione: +pdfjs-document-properties-producer = Produtore de PDF: +pdfjs-document-properties-version = Versione de PDF: +pdfjs-document-properties-page-count = Contu de pàginas: +pdfjs-document-properties-page-size = Mannària de sa pàgina: +pdfjs-document-properties-page-size-unit-inches = pòddighes +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = verticale +pdfjs-document-properties-page-size-orientation-landscape = orizontale +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Lìtera +pdfjs-document-properties-page-size-name-legal = Legale + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Visualizatzione web lestra: +pdfjs-document-properties-linearized-yes = Eja +pdfjs-document-properties-linearized-no = Nono +pdfjs-document-properties-close-button = Serra + +## Print + +pdfjs-print-progress-message = Aparitzende s'imprenta de su documentu… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Cantzella +pdfjs-printing-not-supported = Atentzione: s'imprenta no est funtzionende de su totu in custu navigadore. +pdfjs-printing-not-ready = Atentzione: su PDF no est istadu carrigadu de su totu pro s'imprenta. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Ativa/disativa sa barra laterale +pdfjs-toggle-sidebar-notification-button = + .title = Ativa/disativa sa barra laterale (su documentu cuntenet un'ischema, alligongiados o livellos) +pdfjs-toggle-sidebar-button-label = Ativa/disativa sa barra laterale +pdfjs-document-outline-button-label = Ischema de su documentu +pdfjs-attachments-button = + .title = Ammustra alligongiados +pdfjs-attachments-button-label = Alliongiados +pdfjs-layers-button = + .title = Ammustra livellos (clic dòpiu pro ripristinare totu is livellos a s'istadu predefinidu) +pdfjs-layers-button-label = Livellos +pdfjs-thumbs-button = + .title = Ammustra miniaturas +pdfjs-thumbs-button-label = Miniaturas +pdfjs-current-outline-item-button = + .title = Agata s'elementu atuale de s'ischema +pdfjs-current-outline-item-button-label = Elementu atuale de s'ischema +pdfjs-findbar-button = + .title = Agata in su documentu +pdfjs-findbar-button-label = Agata +pdfjs-additional-layers = Livellos additzionales + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pàgina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura de sa pàgina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Agata + .placeholder = Agata in su documentu… +pdfjs-find-previous-button = + .title = Agata s'ocurrèntzia pretzedente de sa fràsia +pdfjs-find-previous-button-label = S'ischeda chi b'est primu +pdfjs-find-next-button = + .title = Agata s'ocurrèntzia imbeniente de sa fràsia +pdfjs-find-next-button-label = Imbeniente +pdfjs-find-highlight-checkbox = Evidèntzia totu +pdfjs-find-match-case-checkbox-label = Distinghe intre majùsculas e minùsculas +pdfjs-find-match-diacritics-checkbox-label = Respeta is diacrìticos +pdfjs-find-entire-word-checkbox-label = Faeddos intreos +pdfjs-find-reached-top = S'est lòmpidu a su cumintzu de su documentu, si sighit dae su bàsciu +pdfjs-find-reached-bottom = Acabbu de su documentu, si sighit dae s'artu +pdfjs-find-not-found = Testu no agatadu + +## Predefined zoom values + +pdfjs-page-scale-auto = Ingrandimentu automàticu +pdfjs-page-scale-actual = Mannària reale +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Pàgina { $page } + +## Loading indicator messages + +pdfjs-loading-error = Faddina in sa càrriga de su PDF. +pdfjs-invalid-file-error = Archìviu PDF non vàlidu o corrùmpidu. +pdfjs-missing-file-error = Ammancat s'archìviu PDF. +pdfjs-unexpected-response-error = Risposta imprevista de su serbidore. +pdfjs-rendering-error = Faddina in sa visualizatzione de sa pàgina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } + +## Password + +pdfjs-password-label = Inserta sa crae pro abèrrere custu archìviu PDF. +pdfjs-password-invalid = Sa crae no est curreta. Torra a nche proare. +pdfjs-password-ok-button = Andat bene +pdfjs-password-cancel-button = Cantzella +pdfjs-web-fonts-disabled = Is tipografias web sunt disativadas: is tipografias incrustadas a su PDF non podent èssere impreadas. + +## Editing + +pdfjs-editor-free-text-button = + .title = Testu +pdfjs-editor-free-text-button-label = Testu +pdfjs-editor-ink-button = + .title = Disinnu +pdfjs-editor-ink-button-label = Disinnu +pdfjs-editor-stamp-button = + .title = Agiunghe o modìfica immàgines +pdfjs-editor-stamp-button-label = Agiunghe o modìfica immàgines +pdfjs-editor-highlight-button = + .title = Evidèntzia +pdfjs-editor-highlight-button-label = Evidèntzia +pdfjs-highlight-floating-button1 = + .title = Evidèntzia + .aria-label = Evidèntzia +pdfjs-highlight-floating-button-label = Evidèntzia + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Boga su disinnu +pdfjs-editor-remove-freetext-button = + .title = Boga su testu +pdfjs-editor-remove-stamp-button = + .title = Boga s’immàgine +pdfjs-editor-remove-highlight-button = + .title = Boga s’evidèntzia + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Colore +pdfjs-editor-free-text-size-input = Mannària +pdfjs-editor-ink-color-input = Colore +pdfjs-editor-ink-thickness-input = Grussària +pdfjs-editor-stamp-add-image-button = + .title = Agiunghe un’immàgine +pdfjs-editor-stamp-add-image-button-label = Agiunghe un’immàgine +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Grussària +pdfjs-free-text = + .aria-label = Editore de testu +pdfjs-free-text-default-content = Cumintza a iscrìere… +pdfjs-ink = + .aria-label = Editore de disinnos +pdfjs-ink-canvas = + .aria-label = Immàgine creada dae s’utente + +## Alt-text dialog + +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button-label = Testu alternativu +pdfjs-editor-alt-text-edit-button-label = Modifica su testu alternativu +pdfjs-editor-alt-text-dialog-label = Sèbera un’optzione +pdfjs-editor-alt-text-dialog-description = Su testu alternativu (“alt text”) est ùtile pro persones chi non podent bìdere s’immàgine o cando non benit carrigada. +pdfjs-editor-alt-text-add-description-label = Agiunghe una descritzione +pdfjs-editor-alt-text-cancel-button = Annulla +pdfjs-editor-alt-text-save-button = Sarva + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + +pdfjs-editor-colorpicker-button = + .title = Modifica su colore +pdfjs-editor-colorpicker-dropdown = + .aria-label = Colores a disponimentu +pdfjs-editor-colorpicker-yellow = + .title = Grogu +pdfjs-editor-colorpicker-green = + .title = Birde +pdfjs-editor-colorpicker-blue = + .title = Biaitu +pdfjs-editor-colorpicker-pink = + .title = Rosa + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button-label = Mancat su testu alternativu +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button-label = Revisiona su testu alternativu +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Creadu in automàticu: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Cunfiguratzione de su testu alternativu de is immàgines +pdfjs-image-alt-text-settings-button-label = Cunfiguratzione de su testu alternativu de is immàgines +pdfjs-editor-alt-text-settings-dialog-label = Cunfiguratzione de su testu alternativu de is immàgines +pdfjs-editor-alt-text-settings-automatic-title = Testu alternativu automàticu +pdfjs-editor-alt-text-settings-create-model-button-label = Crea testu alternativu in automàticu +pdfjs-editor-alt-text-settings-create-model-description = Cussìgiat descritziones pro agiudare a gente chi non podet bìdere s’immàgine o cando non benit carrigada. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Modellu de IA pro su testu alternativu ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Est esecutadu in locale in manera chi is datos tuos abarrent in privadu. Rechestu pro sa generatzione automàtica de testu alternativu. +pdfjs-editor-alt-text-settings-delete-model-button = Cantzella +pdfjs-editor-alt-text-settings-download-model-button = Iscàrriga +pdfjs-editor-alt-text-settings-downloading-model-button = Iscarrighende… +pdfjs-editor-alt-text-settings-editor-title = Editore de testu alternativu +pdfjs-editor-alt-text-settings-show-dialog-button-label = Mustra deretu s’editore de testu alternativu cando siat agiunta un’immàgine +pdfjs-editor-alt-text-settings-show-dialog-description = T’agiudat a assegurare chi totu is immàgines tuas tèngiant unu testu alternativu. +pdfjs-editor-alt-text-settings-close-button = Serra diff --git a/public/assets/pdfjs/locale/scn/viewer.ftl b/public/assets/pdfjs/locale/scn/viewer.ftl new file mode 100644 index 0000000..a3c7c03 --- /dev/null +++ b/public/assets/pdfjs/locale/scn/viewer.ftl @@ -0,0 +1,74 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-zoom-out-button = + .title = Cchiù nicu +pdfjs-zoom-out-button-label = Cchiù nicu +pdfjs-zoom-in-button = + .title = Cchiù granni +pdfjs-zoom-in-button-label = Cchiù granni + +## Secondary toolbar and context menu + + +## Document properties dialog + + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Vista web lesta: +pdfjs-document-properties-linearized-yes = Se + +## Print + +pdfjs-print-progress-close-button = Sfai + +## Tooltips and alt text for side panel toolbar buttons + + +## Thumbnails panel item (tooltip and alt text for images) + + +## Find panel button title and messages + + +## Predefined zoom values + +pdfjs-page-scale-width = Larghizza dâ pàggina + +## PDF page + + +## Loading indicator messages + + +## Annotations + + +## Password + +pdfjs-password-cancel-button = Sfai + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/sco/viewer.ftl b/public/assets/pdfjs/locale/sco/viewer.ftl new file mode 100644 index 0000000..6f71c47 --- /dev/null +++ b/public/assets/pdfjs/locale/sco/viewer.ftl @@ -0,0 +1,264 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Page Afore +pdfjs-previous-button-label = Previous +pdfjs-next-button = + .title = Page Efter +pdfjs-next-button-label = Neist +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Page +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = o { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } o { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zoom Oot +pdfjs-zoom-out-button-label = Zoom Oot +pdfjs-zoom-in-button = + .title = Zoom In +pdfjs-zoom-in-button-label = Zoom In +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Flit tae Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Open File +pdfjs-open-file-button-label = Open +pdfjs-print-button = + .title = Prent +pdfjs-print-button-label = Prent + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Tools +pdfjs-tools-button-label = Tools +pdfjs-first-page-button = + .title = Gang tae First Page +pdfjs-first-page-button-label = Gang tae First Page +pdfjs-last-page-button = + .title = Gang tae Lest Page +pdfjs-last-page-button-label = Gang tae Lest Page +pdfjs-page-rotate-cw-button = + .title = Rotate Clockwise +pdfjs-page-rotate-cw-button-label = Rotate Clockwise +pdfjs-page-rotate-ccw-button = + .title = Rotate Coonterclockwise +pdfjs-page-rotate-ccw-button-label = Rotate Coonterclockwise +pdfjs-cursor-text-select-tool-button = + .title = Enable Text Walin Tool +pdfjs-cursor-text-select-tool-button-label = Text Walin Tool +pdfjs-cursor-hand-tool-button = + .title = Enable Haun Tool +pdfjs-cursor-hand-tool-button-label = Haun Tool +pdfjs-scroll-vertical-button = + .title = Yaise Vertical Scrollin +pdfjs-scroll-vertical-button-label = Vertical Scrollin +pdfjs-scroll-horizontal-button = + .title = Yaise Horizontal Scrollin +pdfjs-scroll-horizontal-button-label = Horizontal Scrollin +pdfjs-scroll-wrapped-button = + .title = Yaise Wrapped Scrollin +pdfjs-scroll-wrapped-button-label = Wrapped Scrollin +pdfjs-spread-none-button = + .title = Dinnae jyn page spreids +pdfjs-spread-none-button-label = Nae Spreids +pdfjs-spread-odd-button = + .title = Jyn page spreids stertin wi odd-numbered pages +pdfjs-spread-odd-button-label = Odd Spreids +pdfjs-spread-even-button = + .title = Jyn page spreids stertin wi even-numbered pages +pdfjs-spread-even-button-label = Even Spreids + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Document Properties… +pdfjs-document-properties-button-label = Document Properties… +pdfjs-document-properties-file-name = File nemme: +pdfjs-document-properties-file-size = File size: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Title: +pdfjs-document-properties-author = Author: +pdfjs-document-properties-subject = Subjeck: +pdfjs-document-properties-keywords = Keywirds: +pdfjs-document-properties-creation-date = Date o Makkin: +pdfjs-document-properties-modification-date = Date o Chynges: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Makker: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Page Coont: +pdfjs-document-properties-page-size = Page Size: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portrait +pdfjs-document-properties-page-size-orientation-landscape = landscape +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Wab View: +pdfjs-document-properties-linearized-yes = Aye +pdfjs-document-properties-linearized-no = Naw +pdfjs-document-properties-close-button = Sneck + +## Print + +pdfjs-print-progress-message = Reddin document fur prentin… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Stap +pdfjs-printing-not-supported = Tak tent: Prentin isnae richt supportit by this stravaiger. +pdfjs-printing-not-ready = Tak tent: The PDF isnae richt loadit fur prentin. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Toggle Sidebaur +pdfjs-toggle-sidebar-notification-button = + .title = Toggle Sidebaur (document conteens ootline/attachments/layers) +pdfjs-toggle-sidebar-button-label = Toggle Sidebaur +pdfjs-document-outline-button = + .title = Kythe Document Ootline (double-click fur tae oot-fauld/in-fauld aw items) +pdfjs-document-outline-button-label = Document Ootline +pdfjs-attachments-button = + .title = Kythe Attachments +pdfjs-attachments-button-label = Attachments +pdfjs-layers-button = + .title = Kythe Layers (double-click fur tae reset aw layers tae the staunart state) +pdfjs-layers-button-label = Layers +pdfjs-thumbs-button = + .title = Kythe Thumbnails +pdfjs-thumbs-button-label = Thumbnails +pdfjs-current-outline-item-button = + .title = Find Current Ootline Item +pdfjs-current-outline-item-button-label = Current Ootline Item +pdfjs-findbar-button = + .title = Find in Document +pdfjs-findbar-button-label = Find +pdfjs-additional-layers = Mair Layers + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Page { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail o Page { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Find + .placeholder = Find in document… +pdfjs-find-previous-button = + .title = Airt oot the last time this phrase occurred +pdfjs-find-previous-button-label = Previous +pdfjs-find-next-button = + .title = Airt oot the neist time this phrase occurs +pdfjs-find-next-button-label = Neist +pdfjs-find-highlight-checkbox = Highlicht aw +pdfjs-find-match-case-checkbox-label = Match case +pdfjs-find-entire-word-checkbox-label = Hale Wirds +pdfjs-find-reached-top = Raxed tap o document, went on fae the dowp end +pdfjs-find-reached-bottom = Raxed end o document, went on fae the tap +pdfjs-find-not-found = Phrase no fund + +## Predefined zoom values + +pdfjs-page-scale-width = Page Width +pdfjs-page-scale-fit = Page Fit +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Actual Size +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Page { $page } + +## Loading indicator messages + +pdfjs-loading-error = An mishanter tuik place while loadin the PDF. +pdfjs-invalid-file-error = No suithfest or camshauchlet PDF file. +pdfjs-missing-file-error = PDF file tint. +pdfjs-unexpected-response-error = Unexpectit server repone. +pdfjs-rendering-error = A mishanter tuik place while renderin the page. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = Inpit the passwird fur tae open this PDF file. +pdfjs-password-invalid = Passwird no suithfest. Gonnae gie it anither shot. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Stap +pdfjs-web-fonts-disabled = Wab fonts are disabled: cannae yaise embeddit PDF fonts. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/si/viewer.ftl b/public/assets/pdfjs/locale/si/viewer.ftl new file mode 100644 index 0000000..0481116 --- /dev/null +++ b/public/assets/pdfjs/locale/si/viewer.ftl @@ -0,0 +1,271 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = කලින් පිටුව +pdfjs-previous-button-label = කලින් +pdfjs-next-button = + .title = ඊළඟ පිටුව +pdfjs-next-button-label = ඊළඟ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = පිටුව +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = කුඩාලනය +pdfjs-zoom-out-button-label = කුඩාලනය +pdfjs-zoom-in-button = + .title = විශාලනය +pdfjs-zoom-in-button-label = විශාලනය +pdfjs-zoom-select = + .title = විශාල කරන්න +pdfjs-presentation-mode-button = + .title = සමර්පණ ප්‍රකාරය වෙත මාරුවන්න +pdfjs-presentation-mode-button-label = සමර්පණ ප්‍රකාරය +pdfjs-open-file-button = + .title = ගොනුව අරින්න +pdfjs-open-file-button-label = අරින්න +pdfjs-print-button = + .title = මුද්‍රණය +pdfjs-print-button-label = මුද්‍රණය +pdfjs-save-button = + .title = සුරකින්න +pdfjs-save-button-label = සුරකින්න +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = බාගන්න +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = බාගන්න +pdfjs-bookmark-button-label = පවතින පිටුව + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = මෙවලම් +pdfjs-tools-button-label = මෙවලම් +pdfjs-first-page-button = + .title = මුල් පිටුවට යන්න +pdfjs-first-page-button-label = මුල් පිටුවට යන්න +pdfjs-last-page-button = + .title = අවසන් පිටුවට යන්න +pdfjs-last-page-button-label = අවසන් පිටුවට යන්න +pdfjs-cursor-text-select-tool-button = + .title = පෙළ තේරීමේ මෙවලම සබල කරන්න +pdfjs-cursor-text-select-tool-button-label = පෙළ තේරීමේ මෙවලම +pdfjs-cursor-hand-tool-button = + .title = අත් මෙවලම සබල කරන්න +pdfjs-cursor-hand-tool-button-label = අත් මෙවලම +pdfjs-scroll-page-button = + .title = පිටුව අනුචලනය භාවිතය +pdfjs-scroll-page-button-label = පිටුව අනුචලනය +pdfjs-scroll-vertical-button = + .title = සිරස් අනුචලනය භාවිතය +pdfjs-scroll-vertical-button-label = සිරස් අනුචලනය +pdfjs-scroll-horizontal-button = + .title = තිරස් අනුචලනය භාවිතය +pdfjs-scroll-horizontal-button-label = තිරස් අනුචලනය + +## Document properties dialog + +pdfjs-document-properties-button = + .title = ලේඛනයේ ගුණාංග… +pdfjs-document-properties-button-label = ලේඛනයේ ගුණාංග… +pdfjs-document-properties-file-name = ගොනුවේ නම: +pdfjs-document-properties-file-size = ගොනුවේ ප්‍රමාණය: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = කි.බ. { $size_kb } (බයිට { $size_b }) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = මෙ.බ. { $size_mb } (බයිට { $size_b }) +pdfjs-document-properties-title = සිරැසිය: +pdfjs-document-properties-author = කතෘ: +pdfjs-document-properties-subject = මාතෘකාව: +pdfjs-document-properties-keywords = මූල පද: +pdfjs-document-properties-creation-date = සෑදූ දිනය: +pdfjs-document-properties-modification-date = සංශෝධිත දිනය: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = නිර්මාතෘ: +pdfjs-document-properties-producer = පීඩීඑෆ් සම්පාදක: +pdfjs-document-properties-version = පීඩීඑෆ් අනුවාදය: +pdfjs-document-properties-page-count = පිටු ගණන: +pdfjs-document-properties-page-size = පිටුවේ තරම: +pdfjs-document-properties-page-size-unit-inches = අඟල් +pdfjs-document-properties-page-size-unit-millimeters = මි.මී. +pdfjs-document-properties-page-size-orientation-portrait = සිරස් +pdfjs-document-properties-page-size-orientation-landscape = තිරස් +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width }×{ $height }{ $unit }{ $name }{ $orientation } + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = වේගවත් වියමන දැක්ම: +pdfjs-document-properties-linearized-yes = ඔව් +pdfjs-document-properties-linearized-no = නැහැ +pdfjs-document-properties-close-button = වසන්න + +## Print + +pdfjs-print-progress-message = මුද්‍රණය සඳහා ලේඛනය සූදානම් වෙමින්… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = අවලංගු කරන්න +pdfjs-printing-not-supported = අවවාදයයි: මෙම අතිරික්සුව මුද්‍රණය සඳහා හොඳින් සහාය නොදක්වයි. +pdfjs-printing-not-ready = අවවාදයයි: මුද්‍රණයට පීඩීඑෆ් ගොනුව සම්පූර්ණයෙන් පූරණය වී නැත. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-document-outline-button-label = ලේඛනයේ වටසන +pdfjs-attachments-button = + .title = ඇමුණුම් පෙන්වන්න +pdfjs-attachments-button-label = ඇමුණුම් +pdfjs-layers-button = + .title = ස්තර පෙන්වන්න (සියළු ස්තර පෙරනිමි තත්‍වයට යළි සැකසීමට දෙවරක් ඔබන්න) +pdfjs-layers-button-label = ස්තර +pdfjs-thumbs-button = + .title = සිඟිති රූ පෙන්වන්න +pdfjs-thumbs-button-label = සිඟිති රූ +pdfjs-findbar-button = + .title = ලේඛනයෙහි සොයන්න +pdfjs-findbar-button-label = සොයන්න +pdfjs-additional-layers = අතිරේක ස්තර + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = පිටුව { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = පිටුවේ සිඟිත රූව { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = සොයන්න + .placeholder = ලේඛනයේ සොයන්න… +pdfjs-find-previous-button = + .title = මෙම වැකිකඩ කලින් යෙදුණු ස්ථානය සොයන්න +pdfjs-find-previous-button-label = කලින් +pdfjs-find-next-button = + .title = මෙම වැකිකඩ ඊළඟට යෙදෙන ස්ථානය සොයන්න +pdfjs-find-next-button-label = ඊළඟ +pdfjs-find-highlight-checkbox = සියල්ල උද්දීපනය +pdfjs-find-entire-word-checkbox-label = සමස්ත වචන +pdfjs-find-reached-top = ලේඛනයේ මුදුනට ළඟා විය, පහළ සිට ඉහළට +pdfjs-find-reached-bottom = ලේඛනයේ අවසානයට ළඟා විය, ඉහළ සිට පහළට +pdfjs-find-not-found = වැකිකඩ හමු නොවුණි + +## Predefined zoom values + +pdfjs-page-scale-width = පිටුවේ පළල +pdfjs-page-scale-auto = ස්වයංක්‍රීය විශාලනය +pdfjs-page-scale-actual = සැබෑ ප්‍රමාණය +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = පිටුව { $page } + +## Loading indicator messages + +pdfjs-loading-error = පීඩීඑෆ් පූරණය කිරීමේදී දෝෂයක් සිදු විය. +pdfjs-invalid-file-error = වලංගු නොවන හෝ හානිවූ පීඩීඑෆ් ගොනුවකි. +pdfjs-missing-file-error = මඟහැරුණු පීඩීඑෆ් ගොනුවකි. +pdfjs-unexpected-response-error = අනපේක්‍ෂිත සේවාදායක ප්‍රතිචාරයකි. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } + +## Password + +pdfjs-password-label = මෙම පීඩීඑෆ් ගොනුව විවෘත කිරීමට මුරපදය යොදන්න. +pdfjs-password-invalid = වැරදි මුරපදයකි. නැවත උත්සාහ කරන්න. +pdfjs-password-ok-button = හරි +pdfjs-password-cancel-button = අවලංගු +pdfjs-web-fonts-disabled = වියමන අකුරු අබලයි: පීඩීඑෆ් වෙත කාවැද්දූ රුවකුරු භාවිතා කළ නොහැකිය. + +## Editing + +pdfjs-editor-free-text-button = + .title = පෙළ +pdfjs-editor-free-text-button-label = පෙළ +pdfjs-editor-ink-button = + .title = අඳින්න +pdfjs-editor-ink-button-label = අඳින්න +pdfjs-editor-stamp-button = + .title = රූප සංස්කරණය හෝ එක් කරන්න +pdfjs-editor-stamp-button-label = රූප සංස්කරණය හෝ එක් කරන්න + +## Remove button for the various kind of editor. + + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = වර්ණය +pdfjs-editor-free-text-size-input = තරම +pdfjs-editor-ink-color-input = වර්ණය +pdfjs-editor-ink-thickness-input = ඝණකම +pdfjs-free-text = + .aria-label = වදන් සකසනය +pdfjs-free-text-default-content = ලිවීීම අරඹන්න… + +## Alt-text dialog + +pdfjs-editor-alt-text-mark-decorative-description = මෙය දාර හෝ දිය සලකුණු වැනි අලංකාර රූප සඳහා භාවිතා වේ. + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + + +## Color picker + + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/sk/viewer.ftl b/public/assets/pdfjs/locale/sk/viewer.ftl new file mode 100644 index 0000000..5cbbb8d --- /dev/null +++ b/public/assets/pdfjs/locale/sk/viewer.ftl @@ -0,0 +1,521 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Predchádzajúca strana +pdfjs-previous-button-label = Predchádzajúca +pdfjs-next-button = + .title = Nasledujúca strana +pdfjs-next-button-label = Nasledujúca +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Strana +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = z { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } z { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zmenšiť veľkosť +pdfjs-zoom-out-button-label = Zmenšiť veľkosť +pdfjs-zoom-in-button = + .title = Zväčšiť veľkosť +pdfjs-zoom-in-button-label = Zväčšiť veľkosť +pdfjs-zoom-select = + .title = Nastavenie veľkosti +pdfjs-presentation-mode-button = + .title = Prepnúť na režim prezentácie +pdfjs-presentation-mode-button-label = Režim prezentácie +pdfjs-open-file-button = + .title = Otvoriť súbor +pdfjs-open-file-button-label = Otvoriť +pdfjs-print-button = + .title = Tlačiť +pdfjs-print-button-label = Tlačiť +pdfjs-save-button = + .title = Uložiť +pdfjs-save-button-label = Uložiť +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Stiahnuť +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Stiahnuť +pdfjs-bookmark-button = + .title = Aktuálna stránka (zobraziť adresu URL z aktuálnej stránky) +pdfjs-bookmark-button-label = Aktuálna stránka + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Nástroje +pdfjs-tools-button-label = Nástroje +pdfjs-first-page-button = + .title = Prejsť na prvú stranu +pdfjs-first-page-button-label = Prejsť na prvú stranu +pdfjs-last-page-button = + .title = Prejsť na poslednú stranu +pdfjs-last-page-button-label = Prejsť na poslednú stranu +pdfjs-page-rotate-cw-button = + .title = Otočiť v smere hodinových ručičiek +pdfjs-page-rotate-cw-button-label = Otočiť v smere hodinových ručičiek +pdfjs-page-rotate-ccw-button = + .title = Otočiť proti smeru hodinových ručičiek +pdfjs-page-rotate-ccw-button-label = Otočiť proti smeru hodinových ručičiek +pdfjs-cursor-text-select-tool-button = + .title = Povoliť výber textu +pdfjs-cursor-text-select-tool-button-label = Výber textu +pdfjs-cursor-hand-tool-button = + .title = Povoliť nástroj ruka +pdfjs-cursor-hand-tool-button-label = Nástroj ruka +pdfjs-scroll-page-button = + .title = Použiť rolovanie po stránkach +pdfjs-scroll-page-button-label = Rolovanie po stránkach +pdfjs-scroll-vertical-button = + .title = Používať zvislé posúvanie +pdfjs-scroll-vertical-button-label = Zvislé posúvanie +pdfjs-scroll-horizontal-button = + .title = Používať vodorovné posúvanie +pdfjs-scroll-horizontal-button-label = Vodorovné posúvanie +pdfjs-scroll-wrapped-button = + .title = Použiť postupné posúvanie +pdfjs-scroll-wrapped-button-label = Postupné posúvanie +pdfjs-spread-none-button = + .title = Nezdružovať stránky +pdfjs-spread-none-button-label = Žiadne združovanie +pdfjs-spread-odd-button = + .title = Združí stránky a umiestni nepárne stránky vľavo +pdfjs-spread-odd-button-label = Združiť stránky (nepárne vľavo) +pdfjs-spread-even-button = + .title = Združí stránky a umiestni párne stránky vľavo +pdfjs-spread-even-button-label = Združiť stránky (párne vľavo) + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Vlastnosti dokumentu… +pdfjs-document-properties-button-label = Vlastnosti dokumentu… +pdfjs-document-properties-file-name = Názov súboru: +pdfjs-document-properties-file-size = Veľkosť súboru: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } bajtov) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtov) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } bajtov) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtov) +pdfjs-document-properties-title = Názov: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Predmet: +pdfjs-document-properties-keywords = Kľúčové slová: +pdfjs-document-properties-creation-date = Dátum vytvorenia: +pdfjs-document-properties-modification-date = Dátum úpravy: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Aplikácia: +pdfjs-document-properties-producer = Tvorca PDF: +pdfjs-document-properties-version = Verzia PDF: +pdfjs-document-properties-page-count = Počet strán: +pdfjs-document-properties-page-size = Veľkosť stránky: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = na výšku +pdfjs-document-properties-page-size-orientation-landscape = na šírku +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = List +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Rýchle zobrazovanie z webu: +pdfjs-document-properties-linearized-yes = Áno +pdfjs-document-properties-linearized-no = Nie +pdfjs-document-properties-close-button = Zavrieť + +## Print + +pdfjs-print-progress-message = Príprava dokumentu na tlač… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Zrušiť +pdfjs-printing-not-supported = Upozornenie: tlač nie je v tomto prehliadači plne podporovaná. +pdfjs-printing-not-ready = Upozornenie: súbor PDF nie je plne načítaný pre tlač. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Prepnúť bočný panel +pdfjs-toggle-sidebar-notification-button = + .title = Prepnúť bočný panel (dokument obsahuje osnovu/prílohy/vrstvy) +pdfjs-toggle-sidebar-button-label = Prepnúť bočný panel +pdfjs-document-outline-button = + .title = Zobraziť osnovu dokumentu (dvojitým kliknutím rozbalíte/zbalíte všetky položky) +pdfjs-document-outline-button-label = Osnova dokumentu +pdfjs-attachments-button = + .title = Zobraziť prílohy +pdfjs-attachments-button-label = Prílohy +pdfjs-layers-button = + .title = Zobraziť vrstvy (dvojitým kliknutím uvediete všetky vrstvy do pôvodného stavu) +pdfjs-layers-button-label = Vrstvy +pdfjs-thumbs-button = + .title = Zobraziť miniatúry +pdfjs-thumbs-button-label = Miniatúry +pdfjs-current-outline-item-button = + .title = Nájsť aktuálnu položku v osnove +pdfjs-current-outline-item-button-label = Aktuálna položka v osnove +pdfjs-findbar-button = + .title = Hľadať v dokumente +pdfjs-findbar-button-label = Hľadať +pdfjs-additional-layers = Ďalšie vrstvy + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Strana { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatúra strany { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Hľadať + .placeholder = Hľadať v dokumente… +pdfjs-find-previous-button = + .title = Vyhľadať predchádzajúci výskyt reťazca +pdfjs-find-previous-button-label = Predchádzajúce +pdfjs-find-next-button = + .title = Vyhľadať ďalší výskyt reťazca +pdfjs-find-next-button-label = Ďalšie +pdfjs-find-highlight-checkbox = Zvýrazniť všetky +pdfjs-find-match-case-checkbox-label = Rozlišovať veľkosť písmen +pdfjs-find-match-diacritics-checkbox-label = Rozlišovať diakritiku +pdfjs-find-entire-word-checkbox-label = Celé slová +pdfjs-find-reached-top = Bol dosiahnutý začiatok stránky, pokračuje sa od konca +pdfjs-find-reached-bottom = Bol dosiahnutý koniec stránky, pokračuje sa od začiatku +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Výskyt { $current } z { $total } + [few] Výskyt { $current } z { $total } + [many] Výskyt { $current } z { $total } + *[other] Výskyt { $current } z { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Viac ako { $limit } výskyt + [few] Viac ako { $limit } výskyty + [many] Viac ako { $limit } výskytov + *[other] Viac ako { $limit } výskytov + } +pdfjs-find-not-found = Výraz nebol nájdený + +## Predefined zoom values + +pdfjs-page-scale-width = Na šírku strany +pdfjs-page-scale-fit = Na veľkosť strany +pdfjs-page-scale-auto = Automatická veľkosť +pdfjs-page-scale-actual = Skutočná veľkosť +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Strana { $page } + +## Loading indicator messages + +pdfjs-loading-error = Počas načítavania dokumentu PDF sa vyskytla chyba. +pdfjs-invalid-file-error = Neplatný alebo poškodený súbor PDF. +pdfjs-missing-file-error = Chýbajúci súbor PDF. +pdfjs-unexpected-response-error = Neočakávaná odpoveď zo servera. +pdfjs-rendering-error = Pri vykresľovaní stránky sa vyskytla chyba. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotácia typu { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Ak chcete otvoriť tento súbor PDF, zadajte jeho heslo. +pdfjs-password-invalid = Heslo nie je platné. Skúste to znova. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Zrušiť +pdfjs-web-fonts-disabled = Webové písma sú vypnuté: nie je možné použiť písma vložené do súboru PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Kresliť +pdfjs-editor-ink-button-label = Kresliť +pdfjs-editor-stamp-button = + .title = Pridať alebo upraviť obrázky +pdfjs-editor-stamp-button-label = Pridať alebo upraviť obrázky +pdfjs-editor-highlight-button = + .title = Zvýrazniť +pdfjs-editor-highlight-button-label = Zvýrazniť +pdfjs-highlight-floating-button1 = + .title = Zvýrazniť + .aria-label = Zvýrazniť +pdfjs-highlight-floating-button-label = Zvýrazniť + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Odstrániť kresbu +pdfjs-editor-remove-freetext-button = + .title = Odstrániť text +pdfjs-editor-remove-stamp-button = + .title = Odstrániť obrázok +pdfjs-editor-remove-highlight-button = + .title = Odstrániť zvýraznenie + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Farba +pdfjs-editor-free-text-size-input = Veľkosť +pdfjs-editor-ink-color-input = Farba +pdfjs-editor-ink-thickness-input = Hrúbka +pdfjs-editor-ink-opacity-input = Priehľadnosť +pdfjs-editor-stamp-add-image-button = + .title = Pridať obrázok +pdfjs-editor-stamp-add-image-button-label = Pridať obrázok +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Hrúbka +pdfjs-editor-free-highlight-thickness-title = + .title = Zmeňte hrúbku pre zvýrazňovanie iných položiek ako textu +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Textový editor + .default-content = Začnite písať… +pdfjs-free-text = + .aria-label = Textový editor +pdfjs-free-text-default-content = Začnite písať… +pdfjs-ink = + .aria-label = Editor kreslenia +pdfjs-ink-canvas = + .aria-label = Obrázok vytvorený používateľom + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatívny text +pdfjs-editor-alt-text-edit-button = + .aria-label = Upraviť alternatívny text +pdfjs-editor-alt-text-edit-button-label = Upraviť alternatívny text +pdfjs-editor-alt-text-dialog-label = Vyberte možnosť +pdfjs-editor-alt-text-dialog-description = Alternatívny text (alt text) pomáha, keď ľudia obrázok nevidia alebo sa nenačítava. +pdfjs-editor-alt-text-add-description-label = Pridať popis +pdfjs-editor-alt-text-add-description-description = Zamerajte sa na 1-2 vety, ktoré popisujú predmet, prostredie alebo akcie. +pdfjs-editor-alt-text-mark-decorative-label = Označiť ako dekoratívny +pdfjs-editor-alt-text-mark-decorative-description = Používa sa na ozdobné obrázky, ako sú okraje alebo vodoznaky. +pdfjs-editor-alt-text-cancel-button = Zrušiť +pdfjs-editor-alt-text-save-button = Uložiť +pdfjs-editor-alt-text-decorative-tooltip = Označený ako dekoratívny +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Napríklad: „Mladý muž si sadá za stôl, aby sa najedol“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatívny text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Ľavý horný roh – zmena veľkosti +pdfjs-editor-resizer-label-top-middle = Horný stred – zmena veľkosti +pdfjs-editor-resizer-label-top-right = Pravý horný roh – zmena veľkosti +pdfjs-editor-resizer-label-middle-right = Vpravo uprostred – zmena veľkosti +pdfjs-editor-resizer-label-bottom-right = Pravý dolný roh – zmena veľkosti +pdfjs-editor-resizer-label-bottom-middle = Stred dole – zmena veľkosti +pdfjs-editor-resizer-label-bottom-left = Ľavý dolný roh – zmena veľkosti +pdfjs-editor-resizer-label-middle-left = Vľavo uprostred – zmena veľkosti +pdfjs-editor-resizer-top-left = + .aria-label = Ľavý horný roh – zmena veľkosti +pdfjs-editor-resizer-top-middle = + .aria-label = Horný stred – zmena veľkosti +pdfjs-editor-resizer-top-right = + .aria-label = Pravý horný roh – zmena veľkosti +pdfjs-editor-resizer-middle-right = + .aria-label = Vpravo uprostred – zmena veľkosti +pdfjs-editor-resizer-bottom-right = + .aria-label = Pravý dolný roh – zmena veľkosti +pdfjs-editor-resizer-bottom-middle = + .aria-label = Stred dole – zmena veľkosti +pdfjs-editor-resizer-bottom-left = + .aria-label = Ľavý dolný roh – zmena veľkosti +pdfjs-editor-resizer-middle-left = + .aria-label = Vľavo uprostred – zmena veľkosti + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Farba zvýraznenia +pdfjs-editor-colorpicker-button = + .title = Zmeniť farbu +pdfjs-editor-colorpicker-dropdown = + .aria-label = Výber farieb +pdfjs-editor-colorpicker-yellow = + .title = Žltá +pdfjs-editor-colorpicker-green = + .title = Zelená +pdfjs-editor-colorpicker-blue = + .title = Modrá +pdfjs-editor-colorpicker-pink = + .title = Ružová +pdfjs-editor-colorpicker-red = + .title = Červená + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Zobraziť všetko +pdfjs-editor-highlight-show-all-button = + .title = Zobraziť všetko + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Upraviť alternatívny text (popis obrázka) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Pridať alternatívny text (popis obrázka) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Sem napíšte svoj popis… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Krátky popis pre ľudí, ktorí nevidia obrázok alebo ak sa obrázok nenačíta. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Tento alternatívny text bol vytvorený automaticky a môže byť nepresný. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Ďalšie informácie +pdfjs-editor-new-alt-text-create-automatically-button-label = Automaticky vytvoriť alternatívny text +pdfjs-editor-new-alt-text-not-now-button = Teraz nie +pdfjs-editor-new-alt-text-error-title = Alternatívny text sa nepodarilo vytvoriť automaticky +pdfjs-editor-new-alt-text-error-description = Napíšte svoj vlastný alternatívny text alebo to skúste znova neskôr. +pdfjs-editor-new-alt-text-error-close-button = Zavrieť +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) + .aria-valuetext = Sťahuje sa model AI pre alternatívne texty ({ $downloadedSize } z { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatívny text bol pridaný +pdfjs-editor-new-alt-text-added-button-label = Alternatívny text bol pridaný +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Chýbajúci alternatívny text +pdfjs-editor-new-alt-text-missing-button-label = Chýbajúci alternatívny text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Skontrolovať alternatívny text +pdfjs-editor-new-alt-text-to-review-button-label = Skontrolovať alternatívny text +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Vytvorené automaticky: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Nastavenia alternatívneho textu obrázka +pdfjs-image-alt-text-settings-button-label = Nastavenia alternatívneho textu obrázka +pdfjs-editor-alt-text-settings-dialog-label = Nastavenia alternatívneho textu obrázka +pdfjs-editor-alt-text-settings-automatic-title = Automatický alternatívny text +pdfjs-editor-alt-text-settings-create-model-button-label = Automaticky vytvoriť alternatívny text +pdfjs-editor-alt-text-settings-create-model-description = Navrhuje popisy, ktoré pomôžu ľuďom, ktorým sa obrázok nezobrazuje alebo ak sa obrázok nenačíta. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model AI pre alternatívne texty ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Beží lokálne na vašom zariadení, takže vaše dáta zostanú súkromné. Vyžaduje sa pre automatický alternatívny text. +pdfjs-editor-alt-text-settings-delete-model-button = Odstrániť +pdfjs-editor-alt-text-settings-download-model-button = Stiahnuť +pdfjs-editor-alt-text-settings-downloading-model-button = Sťahuje sa… +pdfjs-editor-alt-text-settings-editor-title = Editor alternatívneho textu +pdfjs-editor-alt-text-settings-show-dialog-button-label = Pri pridávaní obrázka ihneď zobraziť editor alternatívneho textu +pdfjs-editor-alt-text-settings-show-dialog-description = Pomáha vám zabezpečiť, aby všetky vaše obrázky mali alternatívny text. +pdfjs-editor-alt-text-settings-close-button = Zavrieť + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Zvýraznenie bolo odstránené +pdfjs-editor-undo-bar-message-freetext = Text bol odstránený +pdfjs-editor-undo-bar-message-ink = Kreslenie bolo odstránené +pdfjs-editor-undo-bar-message-stamp = Obrázok bol odstránený +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anotácia odstránená + [few] { $count } anotácie odstránené + [many] { $count } anotácií odstránených + *[other] { $count } anotácií odstránených + } +pdfjs-editor-undo-bar-undo-button = + .title = Späť +pdfjs-editor-undo-bar-undo-button-label = Späť +pdfjs-editor-undo-bar-close-button = + .title = Zavrieť +pdfjs-editor-undo-bar-close-button-label = Zavrieť diff --git a/public/assets/pdfjs/locale/skr/viewer.ftl b/public/assets/pdfjs/locale/skr/viewer.ftl new file mode 100644 index 0000000..2d0e87f --- /dev/null +++ b/public/assets/pdfjs/locale/skr/viewer.ftl @@ -0,0 +1,498 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = پچھلا ورقہ +pdfjs-previous-button-label = پچھلا +pdfjs-next-button = + .title = اڳلا ورقہ +pdfjs-next-button-label = اڳلا +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = ورقہ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } دا +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } دا { $pagesCount }) +pdfjs-zoom-out-button = + .title = زوم آؤٹ +pdfjs-zoom-out-button-label = زوم آؤٹ +pdfjs-zoom-in-button = + .title = زوم اِن +pdfjs-zoom-in-button-label = زوم اِن +pdfjs-zoom-select = + .title = زوم +pdfjs-presentation-mode-button = + .title = پریزنٹیشن موڈ تے سوئچ کرو +pdfjs-presentation-mode-button-label = پریزنٹیشن موڈ +pdfjs-open-file-button = + .title = فائل کھولو +pdfjs-open-file-button-label = کھولو +pdfjs-print-button = + .title = چھاپو +pdfjs-print-button-label = چھاپو +pdfjs-save-button = + .title = ہتھیکڑا کرو +pdfjs-save-button-label = ہتھیکڑا کرو +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ڈاؤن لوڈ +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ڈاؤن لوڈ +pdfjs-bookmark-button = + .title = موجودہ ورقہ (موجودہ ورقے کنوں یوآرایل ݙیکھو) +pdfjs-bookmark-button-label = موجودہ ورقہ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = اوزار +pdfjs-tools-button-label = اوزار +pdfjs-first-page-button = + .title = پہلے ورقے تے ونڄو +pdfjs-first-page-button-label = پہلے ورقے تے ونڄو +pdfjs-last-page-button = + .title = چھیکڑی ورقے تے ونڄو +pdfjs-last-page-button-label = چھیکڑی ورقے تے ونڄو +pdfjs-page-rotate-cw-button = + .title = گھڑی وانگوں گھماؤ +pdfjs-page-rotate-cw-button-label = گھڑی وانگوں گھماؤ +pdfjs-page-rotate-ccw-button = + .title = گھڑی تے اُپٹھ گھماؤ +pdfjs-page-rotate-ccw-button-label = گھڑی تے اُپٹھ گھماؤ +pdfjs-cursor-text-select-tool-button = + .title = متن منتخب کݨ والا آلہ فعال بݨاؤ +pdfjs-cursor-text-select-tool-button-label = متن منتخب کرݨ والا آلہ +pdfjs-cursor-hand-tool-button = + .title = ہینڈ ٹول فعال بݨاؤ +pdfjs-cursor-hand-tool-button-label = ہینڈ ٹول +pdfjs-scroll-page-button = + .title = پیج سکرولنگ استعمال کرو +pdfjs-scroll-page-button-label = پیج سکرولنگ +pdfjs-scroll-vertical-button = + .title = عمودی سکرولنگ استعمال کرو +pdfjs-scroll-vertical-button-label = عمودی سکرولنگ +pdfjs-scroll-horizontal-button = + .title = افقی سکرولنگ استعمال کرو +pdfjs-scroll-horizontal-button-label = افقی سکرولنگ +pdfjs-scroll-wrapped-button = + .title = ویڑھی ہوئی سکرولنگ استعمال کرو +pdfjs-scroll-wrapped-button-label = وہڑھی ہوئی سکرولنگ +pdfjs-spread-none-button = + .title = پیج سپریڈز وِچ شامل نہ تھیوو۔ +pdfjs-spread-none-button-label = کوئی پولھ کائنی +pdfjs-spread-odd-button = + .title = طاق نمبر والے ورقیاں دے نال شروع تھیوݨ والے پیج سپریڈز وِچ شامل تھیوو۔ +pdfjs-spread-odd-button-label = تاک پھیلاؤ +pdfjs-spread-even-button = + .title = جفت نمر والے ورقیاں نال شروع تھیوݨ والے پیج سپریڈز وِ شامل تھیوو۔ +pdfjs-spread-even-button-label = جفت پھیلاؤ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = دستاویز خواص… +pdfjs-document-properties-button-label = دستاویز خواص … +pdfjs-document-properties-file-name = فائل دا ناں: +pdfjs-document-properties-file-size = فائل دا سائز: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } بائٹاں) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } بائٹاں) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } کے بی ({ $size_b } بائٹس) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } ایم بی ({ $size_b } بائٹس) +pdfjs-document-properties-title = عنوان: +pdfjs-document-properties-author = تخلیق کار: +pdfjs-document-properties-subject = موضوع: +pdfjs-document-properties-keywords = کلیدی الفاظ: +pdfjs-document-properties-creation-date = تخلیق دی تاریخ: +pdfjs-document-properties-modification-date = ترمیم دی تاریخ: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = تخلیق کار: +pdfjs-document-properties-producer = PDF پیدا کار: +pdfjs-document-properties-version = PDF ورژن: +pdfjs-document-properties-page-count = ورقہ شماری: +pdfjs-document-properties-page-size = ورقہ دی سائز: +pdfjs-document-properties-page-size-unit-inches = وِچ +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = عمودی انداز +pdfjs-document-properties-page-size-orientation-landscape = افقى انداز +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = لیٹر +pdfjs-document-properties-page-size-name-legal = قنونی + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = تکھا ویب نظارہ: +pdfjs-document-properties-linearized-yes = جیا +pdfjs-document-properties-linearized-no = کو +pdfjs-document-properties-close-button = بند کرو + +## Print + +pdfjs-print-progress-message = چھاپݨ کیتے دستاویز تیار تھیندے پئے ہن … +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = منسوخ کرو +pdfjs-printing-not-supported = چتاوݨی: چھپائی ایں براؤزر تے پوری طراں معاونت شدہ کائنی۔ +pdfjs-printing-not-ready = چتاوݨی: PDF چھپائی کیتے پوری طراں لوڈ نئیں تھئی۔ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = سائیڈ بار ٹوگل کرو +pdfjs-toggle-sidebar-notification-button = + .title = سائیڈ بار ٹوگل کرو (دستاویز وِچ آؤٹ لائن/ منسلکات/ پرتاں شامل ہن) +pdfjs-toggle-sidebar-button-label = سائیڈ بار ٹوگل کرو +pdfjs-document-outline-button = + .title = دستاویز دا خاکہ ݙکھاؤ (تمام آئٹمز کوں پھیلاوݨ/سنگوڑݨ کیتے ڈبل کلک کرو) +pdfjs-document-outline-button-label = دستاویز آؤٹ لائن +pdfjs-attachments-button = + .title = نتھیاں ݙکھاؤ +pdfjs-attachments-button-label = منسلکات +pdfjs-layers-button = + .title = پرتاں ݙکھاؤ (تمام پرتاں کوں ڈیفالٹ حالت وِچ دوبارہ ترتیب ݙیوݨ کیتے ڈبل کلک کرو) +pdfjs-layers-button-label = پرتاں +pdfjs-thumbs-button = + .title = تھمبنیل ݙکھاؤ +pdfjs-thumbs-button-label = تھمبنیلز +pdfjs-current-outline-item-button = + .title = موجودہ آؤٹ لائن آئٹم لبھو +pdfjs-current-outline-item-button-label = موجودہ آؤٹ لائن آئٹم +pdfjs-findbar-button = + .title = دستاویز وِچ لبھو +pdfjs-findbar-button-label = لبھو +pdfjs-additional-layers = اضافی پرتاں + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = ورقہ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = ورقے دا تھمبنیل { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = لبھو + .placeholder = دستاویز وِچ لبھو … +pdfjs-find-previous-button = + .title = فقرے دا پچھلا واقعہ لبھو +pdfjs-find-previous-button-label = پچھلا +pdfjs-find-next-button = + .title = فقرے دا اڳلا واقعہ لبھو +pdfjs-find-next-button-label = اڳلا +pdfjs-find-highlight-checkbox = تمام نشابر کرو +pdfjs-find-match-case-checkbox-label = حروف مشابہ کرو +pdfjs-find-match-diacritics-checkbox-label = ڈائیکرٹکس مشابہ کرو +pdfjs-find-entire-word-checkbox-label = تمام الفاظ +pdfjs-find-reached-top = ورقے دے شروع تے پُج ڳیا، تلوں جاری کیتا ڳیا +pdfjs-find-reached-bottom = ورقے دے پاند تے پُڄ ڳیا، اُتوں شروع کیتا ڳیا +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $total } وِچوں { $current } مشابہ + *[other] { $total } وِچوں { $current } مشابے + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] { $limit } توں ودھ مماثلت۔ + *[other] { $limit } توں ودھ مماثلتاں۔ + } +pdfjs-find-not-found = فقرہ نئیں ملیا + +## Predefined zoom values + +pdfjs-page-scale-width = ورقے دی چوڑائی +pdfjs-page-scale-fit = ورقہ فٹنگ +pdfjs-page-scale-auto = آپوں آپ زوم +pdfjs-page-scale-actual = اصل میچا +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = ورقہ { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF لوڈ کریندے ویلھے نقص آ ڳیا۔ +pdfjs-invalid-file-error = غلط یا خراب شدہ PDF فائل۔ +pdfjs-missing-file-error = PDF فائل غائب ہے۔ +pdfjs-unexpected-response-error = سرور دا غیر متوقع جواب۔ +pdfjs-rendering-error = ورقہ رینڈر کریندے ویلھے ہک خرابی پیش آڳئی۔ + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } تشریح] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = ایہ PDF فائل کھولݨ کیتے پاس ورڈ درج کرو۔ +pdfjs-password-invalid = غلط پاس ورڈ: براہ مہربانی ولدا کوشش کرو۔ +pdfjs-password-ok-button = ٹھیک ہے +pdfjs-password-cancel-button = منسوخ کرو +pdfjs-web-fonts-disabled = ویب فونٹس غیر فعال ہن: ایمبیڈڈ PDF فونٹس استعمال کرݨ کنوں قاصر ہن + +## Editing + +pdfjs-editor-free-text-button = + .title = متن +pdfjs-editor-free-text-button-label = متن +pdfjs-editor-ink-button = + .title = چھکو +pdfjs-editor-ink-button-label = چھکو +pdfjs-editor-stamp-button = + .title = تصویراں کوں شامل کرو یا ترمیم کرو +pdfjs-editor-stamp-button-label = تصویراں کوں شامل کرو یا ترمیم کرو +pdfjs-editor-highlight-button = + .title = نمایاں کرو +pdfjs-editor-highlight-button-label = نمایاں کرو +pdfjs-highlight-floating-button1 = + .title = نمایاں کرو + .aria-label = نمایاں کرو +pdfjs-highlight-floating-button-label = نمایاں کرو + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = ڈرائینگ ہٹاؤ +pdfjs-editor-remove-freetext-button = + .title = متن ہٹاؤ +pdfjs-editor-remove-stamp-button = + .title = تصویر ہٹاؤ +pdfjs-editor-remove-highlight-button = + .title = نمایاں ہٹاؤ + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = رنگ +pdfjs-editor-free-text-size-input = سائز +pdfjs-editor-ink-color-input = رنگ +pdfjs-editor-ink-thickness-input = ٹھولھ +pdfjs-editor-ink-opacity-input = دھندلاپن +pdfjs-editor-stamp-add-image-button = + .title = تصویر شامل کرو +pdfjs-editor-stamp-add-image-button-label = تصویر شامل کرو +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = مُٹاݨ +pdfjs-editor-free-highlight-thickness-title = + .title = متن توں ان٘ج ٻئے شئیں کوں نمایاں کرݨ ویلے مُٹاݨ کوں بدلو +pdfjs-free-text = + .aria-label = ٹیکسٹ ایڈیٹر +pdfjs-free-text-default-content = ٹائپنگ شروع کرو … +pdfjs-ink = + .aria-label = ڈرا ایڈیٹر +pdfjs-ink-canvas = + .aria-label = صارف دی بݨائی ہوئی تصویر + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alt متن +pdfjs-editor-alt-text-edit-button-label = alt متن وِچ ترمیم کرو +pdfjs-editor-alt-text-dialog-label = ہِک اختیار چُݨو +pdfjs-editor-alt-text-dialog-description = Alt متن (متبادل متن) اِیں ویلے مَدَت کرین٘دا ہِے جہڑیلے لوک تصویر کوں نِھیں ݙیکھ سڳدے یا جہڑیلے اِیہ لوڈ کائنی تِھین٘دا۔ +pdfjs-editor-alt-text-add-description-label = تفصیل شامل کرو +pdfjs-editor-alt-text-add-description-description = 1-2 جملیاں دا مقصد جہڑے موضوع، ترتیب، یا اعمال کوں بیان کرین٘دے ہِن۔ +pdfjs-editor-alt-text-mark-decorative-label = آرائشی طور تے نشان زد کرو +pdfjs-editor-alt-text-mark-decorative-description = اِیہ آرائشی تصویراں کِیتے استعمال تِھین٘دا ہِے، جیویں بارڈر یا واٹر مارکس۔ +pdfjs-editor-alt-text-cancel-button = منسوخ +pdfjs-editor-alt-text-save-button = محفوظ +pdfjs-editor-alt-text-decorative-tooltip = آرائشی دے طور تے نشان زد تِھی ڳِیا +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = مثال دے طور تے، "ہِک جؤان کھاݨاں کھاوݨ کِیتے میز اُتّے ٻیٹھا ہِے" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alt متن + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = اُتلی کَھٻّی نُکّڑ — سائز بدلو +pdfjs-editor-resizer-label-top-middle = اُتلا وِچلا — سائز بدلو +pdfjs-editor-resizer-label-top-right = اُتلی سَڄّی نُکَّڑ — سائز بدلو +pdfjs-editor-resizer-label-middle-right = وِچلا سڄّا — سائز بدلو +pdfjs-editor-resizer-label-bottom-right = تلوِیں سَڄّی نُکَّڑ — سائز بدلو +pdfjs-editor-resizer-label-bottom-middle = تلواں وِچلا — سائز بدلو +pdfjs-editor-resizer-label-bottom-left = تلوِیں کَھٻّی نُکّڑ — سائز بدلو +pdfjs-editor-resizer-label-middle-left = وِچلا کَھٻّا — سائز بدلو +pdfjs-editor-resizer-top-left = + .aria-label = اُتلی کَھٻّی نُکّڑ — سائز بدلو +pdfjs-editor-resizer-top-middle = + .aria-label = اُتلا وِچلا — سائز بدلو +pdfjs-editor-resizer-top-right = + .aria-label = اُتلی سَڄّی نُکَّڑ — سائز بدلو +pdfjs-editor-resizer-middle-right = + .aria-label = وِچلا سڄّا — سائز بدلو +pdfjs-editor-resizer-bottom-right = + .aria-label = تلوِیں سَڄّی نُکَّڑ — سائز بدلو +pdfjs-editor-resizer-bottom-middle = + .aria-label = تلواں وِچلا — سائز بدلو +pdfjs-editor-resizer-bottom-left = + .aria-label = تلوِیں کَھٻّی نُکّڑ — سائز بدلو +pdfjs-editor-resizer-middle-left = + .aria-label = وِچلا کَھٻّا — سائز بدلو + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = نشابر رنگ +pdfjs-editor-colorpicker-button = + .title = رنگ بدلو +pdfjs-editor-colorpicker-dropdown = + .aria-label = رنگ اختیارات +pdfjs-editor-colorpicker-yellow = + .title = پیلا +pdfjs-editor-colorpicker-green = + .title = ساوا +pdfjs-editor-colorpicker-blue = + .title = نیلا +pdfjs-editor-colorpicker-pink = + .title = گلابی +pdfjs-editor-colorpicker-red = + .title = لال + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = سارے ݙکھاؤ +pdfjs-editor-highlight-show-all-button = + .title = سارے ݙکھاؤ + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = آلٹ عبارت وچ تبدیلی کرو (تصویر تفصیل) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = آلٹ عبارت شامل کرو (تصویر تفصیل) +pdfjs-editor-new-alt-text-textarea = + .placeholder = اتھ آپݨی وضاحت لکھو۔۔۔ +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = اُنہاں لوکاں کیتے مختصر تفصیل جہڑے تصویر کائنی ݙیکھ سڳدے یا ڄݙݨ تصویر لوڈ کائبی تھیندی۔ +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = آلٹ عبارت خودکار تخلیق تھئی ہے تے غلط تھی سڳدی ہے۔ +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = ٻیا سِکھو +pdfjs-editor-new-alt-text-create-automatically-button-label = آلٹ عبارت خودکار بݨاؤ +pdfjs-editor-new-alt-text-not-now-button = ہݨ کائناں +pdfjs-editor-new-alt-text-error-title = آلٹ عبارت خودکار نہ بݨاؤ +pdfjs-editor-new-alt-text-error-description = سوہݨا، آپݨی آلٹ عبارت لکھو یا ولدا بعد وچ کوشش کرو۔ +pdfjs-editor-new-alt-text-error-close-button = بند کرو +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = آلٹ عبارت اے آئی ماڈل({ $totalSize }ایم بی دے { $downloadedSize }) ڈاؤن لوڈ تھیندا پئے + .aria-valuetext = آلٹ عبارت اے آئی ماڈل({ $totalSize }ایم بی دے { $downloadedSize }) ڈاؤن لوڈ تھیندا پئے +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = آلٹ عبارت شامل تھی ڳئی +pdfjs-editor-new-alt-text-added-button-label = آلٹ عبارت شامل تھی ڳئی +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = متبادل عبارت غائب ہے +pdfjs-editor-new-alt-text-missing-button-label = متبادل عبارت غائب ہے +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = alt متن تے نظرثانی کرو +pdfjs-editor-new-alt-text-to-review-button-label = alt متن تے نظرثانی کرو +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = خودکار تخلیق تھئی: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = تصویر آلٹ عبارت ترتیباں +pdfjs-image-alt-text-settings-button-label = تصویر آلٹ عبارت ترتیباں +pdfjs-editor-alt-text-settings-dialog-label = تصویر آلٹ عبارت ترتیباں +pdfjs-editor-alt-text-settings-automatic-title = خودکار آلٹ عبارت +pdfjs-editor-alt-text-settings-create-model-button-label = آلٹ عبارت خودکار بݨاؤ +pdfjs-editor-alt-text-settings-create-model-description = اُنہاں لوکاں دی مدد کیتے تفصیل تجویز کرو جہڑے تصویر کائنی ݙیکھ سڳدے یا ڄݙݨ تصویر لوڈ کائبی تھیندی۔ +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = آلٹ عبارت اے آئی ماڈل ({ $totalSize } ایم بی) +pdfjs-editor-alt-text-settings-ai-model-description = تہاݙی ڈیوائس تے مقامی طور تے چلدا ہے تاں جو تہاݙا ڈیٹا نجی رہوے۔ خودکار آلٹ عبارت کیتے ضروری ہے۔ +pdfjs-editor-alt-text-settings-delete-model-button = مٹاؤ +pdfjs-editor-alt-text-settings-download-model-button = ڈاؤن لوڈ +pdfjs-editor-alt-text-settings-downloading-model-button = ڈاؤن لوڈ تھیندا پئے … +pdfjs-editor-alt-text-settings-editor-title = متبادل ٹیکسٹ ایڈیٹر +pdfjs-editor-alt-text-settings-show-dialog-button-label = تصویر شامل کرݨ ویلے فوری طور تے آلٹ ٹیکسٹ ایڈیٹر ݙکھاؤ +pdfjs-editor-alt-text-settings-show-dialog-description = ایہ تہاکوں یقینی بݨاوݨ وچ مدد کریندے جو تہاݙیاں ساریاں تصویراں وچ آلٹ عبارت ہے۔ +pdfjs-editor-alt-text-settings-close-button = بند کرو + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-undo-button = + .title = کیتا اݨ کیتا +pdfjs-editor-undo-bar-undo-button-label = کیتا اݨ کیتا +pdfjs-editor-undo-bar-close-button = + .title = بند کرو +pdfjs-editor-undo-bar-close-button-label = بند کرو diff --git a/public/assets/pdfjs/locale/sl/viewer.ftl b/public/assets/pdfjs/locale/sl/viewer.ftl new file mode 100644 index 0000000..4e004bd --- /dev/null +++ b/public/assets/pdfjs/locale/sl/viewer.ftl @@ -0,0 +1,521 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Prejšnja stran +pdfjs-previous-button-label = Nazaj +pdfjs-next-button = + .title = Naslednja stran +pdfjs-next-button-label = Naprej +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Stran +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = od { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } od { $pagesCount }) +pdfjs-zoom-out-button = + .title = Pomanjšaj +pdfjs-zoom-out-button-label = Pomanjšaj +pdfjs-zoom-in-button = + .title = Povečaj +pdfjs-zoom-in-button-label = Povečaj +pdfjs-zoom-select = + .title = Povečava +pdfjs-presentation-mode-button = + .title = Preklopi v način predstavitve +pdfjs-presentation-mode-button-label = Način predstavitve +pdfjs-open-file-button = + .title = Odpri datoteko +pdfjs-open-file-button-label = Odpri +pdfjs-print-button = + .title = Natisni +pdfjs-print-button-label = Natisni +pdfjs-save-button = + .title = Shrani +pdfjs-save-button-label = Shrani +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Prenesi +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Prenesi +pdfjs-bookmark-button = + .title = Trenutna stran (prikaži URL, ki vodi do trenutne strani) +pdfjs-bookmark-button-label = Na trenutno stran + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Orodja +pdfjs-tools-button-label = Orodja +pdfjs-first-page-button = + .title = Pojdi na prvo stran +pdfjs-first-page-button-label = Pojdi na prvo stran +pdfjs-last-page-button = + .title = Pojdi na zadnjo stran +pdfjs-last-page-button-label = Pojdi na zadnjo stran +pdfjs-page-rotate-cw-button = + .title = Zavrti v smeri urnega kazalca +pdfjs-page-rotate-cw-button-label = Zavrti v smeri urnega kazalca +pdfjs-page-rotate-ccw-button = + .title = Zavrti v nasprotni smeri urnega kazalca +pdfjs-page-rotate-ccw-button-label = Zavrti v nasprotni smeri urnega kazalca +pdfjs-cursor-text-select-tool-button = + .title = Omogoči orodje za izbor besedila +pdfjs-cursor-text-select-tool-button-label = Orodje za izbor besedila +pdfjs-cursor-hand-tool-button = + .title = Omogoči roko +pdfjs-cursor-hand-tool-button-label = Roka +pdfjs-scroll-page-button = + .title = Uporabi drsenje po strani +pdfjs-scroll-page-button-label = Drsenje po strani +pdfjs-scroll-vertical-button = + .title = Uporabi navpično drsenje +pdfjs-scroll-vertical-button-label = Navpično drsenje +pdfjs-scroll-horizontal-button = + .title = Uporabi vodoravno drsenje +pdfjs-scroll-horizontal-button-label = Vodoravno drsenje +pdfjs-scroll-wrapped-button = + .title = Uporabi ovito drsenje +pdfjs-scroll-wrapped-button-label = Ovito drsenje +pdfjs-spread-none-button = + .title = Ne združuj razponov strani +pdfjs-spread-none-button-label = Brez razponov +pdfjs-spread-odd-button = + .title = Združuj razpone strani z začetkom pri lihih straneh +pdfjs-spread-odd-button-label = Lihi razponi +pdfjs-spread-even-button = + .title = Združuj razpone strani z začetkom pri sodih straneh +pdfjs-spread-even-button-label = Sodi razponi + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Lastnosti dokumenta … +pdfjs-document-properties-button-label = Lastnosti dokumenta … +pdfjs-document-properties-file-name = Ime datoteke: +pdfjs-document-properties-file-size = Velikost datoteke: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bajtov) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajtov) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajtov) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajtov) +pdfjs-document-properties-title = Ime: +pdfjs-document-properties-author = Avtor: +pdfjs-document-properties-subject = Tema: +pdfjs-document-properties-keywords = Ključne besede: +pdfjs-document-properties-creation-date = Datum nastanka: +pdfjs-document-properties-modification-date = Datum spremembe: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Ustvaril: +pdfjs-document-properties-producer = Izdelovalec PDF: +pdfjs-document-properties-version = Različica PDF: +pdfjs-document-properties-page-count = Število strani: +pdfjs-document-properties-page-size = Velikost strani: +pdfjs-document-properties-page-size-unit-inches = palcev +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = pokončno +pdfjs-document-properties-page-size-orientation-landscape = ležeče +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Pismo +pdfjs-document-properties-page-size-name-legal = Pravno + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Hitri spletni ogled: +pdfjs-document-properties-linearized-yes = Da +pdfjs-document-properties-linearized-no = Ne +pdfjs-document-properties-close-button = Zapri + +## Print + +pdfjs-print-progress-message = Priprava dokumenta na tiskanje … +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress } % +pdfjs-print-progress-close-button = Prekliči +pdfjs-printing-not-supported = Opozorilo: ta brskalnik ne podpira vseh možnosti tiskanja. +pdfjs-printing-not-ready = Opozorilo: PDF ni v celoti naložen za tiskanje. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Preklopi stransko vrstico +pdfjs-toggle-sidebar-notification-button = + .title = Preklopi stransko vrstico (dokument vsebuje oris/priponke/plasti) +pdfjs-toggle-sidebar-button-label = Preklopi stransko vrstico +pdfjs-document-outline-button = + .title = Prikaži oris dokumenta (dvokliknite za razširitev/strnitev vseh predmetov) +pdfjs-document-outline-button-label = Oris dokumenta +pdfjs-attachments-button = + .title = Prikaži priponke +pdfjs-attachments-button-label = Priponke +pdfjs-layers-button = + .title = Prikaži plasti (dvokliknite za ponastavitev vseh plasti na privzeto stanje) +pdfjs-layers-button-label = Plasti +pdfjs-thumbs-button = + .title = Prikaži sličice +pdfjs-thumbs-button-label = Sličice +pdfjs-current-outline-item-button = + .title = Najdi trenutni predmet orisa +pdfjs-current-outline-item-button-label = Trenutni predmet orisa +pdfjs-findbar-button = + .title = Iskanje po dokumentu +pdfjs-findbar-button-label = Najdi +pdfjs-additional-layers = Dodatne plasti + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Stran { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Sličica strani { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Najdi + .placeholder = Najdi v dokumentu … +pdfjs-find-previous-button = + .title = Najdi prejšnjo ponovitev iskanega +pdfjs-find-previous-button-label = Najdi nazaj +pdfjs-find-next-button = + .title = Najdi naslednjo ponovitev iskanega +pdfjs-find-next-button-label = Najdi naprej +pdfjs-find-highlight-checkbox = Označi vse +pdfjs-find-match-case-checkbox-label = Razlikuj velike/male črke +pdfjs-find-match-diacritics-checkbox-label = Razlikuj diakritične znake +pdfjs-find-entire-word-checkbox-label = Cele besede +pdfjs-find-reached-top = Dosežen začetek dokumenta iz smeri konca +pdfjs-find-reached-bottom = Doseženo konec dokumenta iz smeri začetka +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] Zadetek { $current } od { $total } + [two] Zadetek { $current } od { $total } + [few] Zadetek { $current } od { $total } + *[other] Zadetek { $current } od { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Več kot { $limit } zadetek + [two] Več kot { $limit } zadetka + [few] Več kot { $limit } zadetki + *[other] Več kot { $limit } zadetkov + } +pdfjs-find-not-found = Iskanega ni mogoče najti + +## Predefined zoom values + +pdfjs-page-scale-width = Širina strani +pdfjs-page-scale-fit = Prilagodi stran +pdfjs-page-scale-auto = Samodejno +pdfjs-page-scale-actual = Dejanska velikost +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale } % + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Stran { $page } + +## Loading indicator messages + +pdfjs-loading-error = Med nalaganjem datoteke PDF je prišlo do napake. +pdfjs-invalid-file-error = Neveljavna ali pokvarjena datoteka PDF. +pdfjs-missing-file-error = Ni datoteke PDF. +pdfjs-unexpected-response-error = Nepričakovan odgovor strežnika. +pdfjs-rendering-error = Med pripravljanjem strani je prišlo do napake! + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Opomba vrste { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Vnesite geslo za odpiranje te datoteke PDF. +pdfjs-password-invalid = Neveljavno geslo. Poskusite znova. +pdfjs-password-ok-button = V redu +pdfjs-password-cancel-button = Prekliči +pdfjs-web-fonts-disabled = Spletne pisave so onemogočene: vgradnih pisav za PDF ni mogoče uporabiti. + +## Editing + +pdfjs-editor-free-text-button = + .title = Besedilo +pdfjs-editor-free-text-button-label = Besedilo +pdfjs-editor-ink-button = + .title = Riši +pdfjs-editor-ink-button-label = Riši +pdfjs-editor-stamp-button = + .title = Dodajanje ali urejanje slik +pdfjs-editor-stamp-button-label = Dodajanje ali urejanje slik +pdfjs-editor-highlight-button = + .title = Označevalnik +pdfjs-editor-highlight-button-label = Označevalnik +pdfjs-highlight-floating-button1 = + .title = Označi + .aria-label = Označi +pdfjs-highlight-floating-button-label = Označi + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Odstrani risbo +pdfjs-editor-remove-freetext-button = + .title = Odstrani besedilo +pdfjs-editor-remove-stamp-button = + .title = Odstrani sliko +pdfjs-editor-remove-highlight-button = + .title = Odstrani označbo + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Barva +pdfjs-editor-free-text-size-input = Velikost +pdfjs-editor-ink-color-input = Barva +pdfjs-editor-ink-thickness-input = Debelina +pdfjs-editor-ink-opacity-input = Neprosojnost +pdfjs-editor-stamp-add-image-button = + .title = Dodaj sliko +pdfjs-editor-stamp-add-image-button-label = Dodaj sliko +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Debelina +pdfjs-editor-free-highlight-thickness-title = + .title = Spremeni debelino pri označevanju nebesedilnih elementov +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Urejevalnik besedila + .default-content = Začnite tipkati … +pdfjs-free-text = + .aria-label = Urejevalnik besedila +pdfjs-free-text-default-content = Začnite tipkati … +pdfjs-ink = + .aria-label = Urejevalnik risanja +pdfjs-ink-canvas = + .aria-label = Uporabnikova slika + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Nadomestno besedilo +pdfjs-editor-alt-text-edit-button = + .aria-label = Uredi nadomestno besedilo +pdfjs-editor-alt-text-edit-button-label = Uredi nadomestno besedilo +pdfjs-editor-alt-text-dialog-label = Izberite možnost +pdfjs-editor-alt-text-dialog-description = Nadomestno besedilo se prikaže tistim, ki ne vidijo slike, ali če se ta ne naloži. +pdfjs-editor-alt-text-add-description-label = Dodaj opis +pdfjs-editor-alt-text-add-description-description = Poskušajte v enem ali dveh stavkih opisati motiv, okolje ali dejanja. +pdfjs-editor-alt-text-mark-decorative-label = Označi kot okrasno +pdfjs-editor-alt-text-mark-decorative-description = Uporablja se za slike, ki služijo samo okrasu, na primer obrobe ali vodne žige. +pdfjs-editor-alt-text-cancel-button = Prekliči +pdfjs-editor-alt-text-save-button = Shrani +pdfjs-editor-alt-text-decorative-tooltip = Označeno kot okrasno +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Na primer: "Mladenič sedi za mizo pri jedi" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Nadomestno besedilo + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Zgornji levi kot – spremeni velikost +pdfjs-editor-resizer-label-top-middle = Zgoraj na sredini – spremeni velikost +pdfjs-editor-resizer-label-top-right = Zgornji desni kot – spremeni velikost +pdfjs-editor-resizer-label-middle-right = Desno na sredini – spremeni velikost +pdfjs-editor-resizer-label-bottom-right = Spodnji desni kot – spremeni velikost +pdfjs-editor-resizer-label-bottom-middle = Spodaj na sredini – spremeni velikost +pdfjs-editor-resizer-label-bottom-left = Spodnji levi kot – spremeni velikost +pdfjs-editor-resizer-label-middle-left = Levo na sredini – spremeni velikost +pdfjs-editor-resizer-top-left = + .aria-label = Zgornji levi kot – spremeni velikost +pdfjs-editor-resizer-top-middle = + .aria-label = Zgoraj na sredini – spremeni velikost +pdfjs-editor-resizer-top-right = + .aria-label = Zgornji desni kot – spremeni velikost +pdfjs-editor-resizer-middle-right = + .aria-label = Desno na sredini – spremeni velikost +pdfjs-editor-resizer-bottom-right = + .aria-label = Spodnji desni kot – spremeni velikost +pdfjs-editor-resizer-bottom-middle = + .aria-label = Spodaj na sredini – spremeni velikost +pdfjs-editor-resizer-bottom-left = + .aria-label = Spodnji levi kot – spremeni velikost +pdfjs-editor-resizer-middle-left = + .aria-label = Levo na sredini – spremeni velikost + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Barva označbe +pdfjs-editor-colorpicker-button = + .title = Spremeni barvo +pdfjs-editor-colorpicker-dropdown = + .aria-label = Izbira barve +pdfjs-editor-colorpicker-yellow = + .title = Rumena +pdfjs-editor-colorpicker-green = + .title = Zelena +pdfjs-editor-colorpicker-blue = + .title = Modra +pdfjs-editor-colorpicker-pink = + .title = Roza +pdfjs-editor-colorpicker-red = + .title = Rdeča + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Prikaži vse +pdfjs-editor-highlight-show-all-button = + .title = Prikaži vse + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Uredi nadomestno besedilo (opis slike) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Dodaj nadomestno besedilo (opis slike) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Tukaj napišite svoj opis … +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kratek opis za ljudi, ki ne morejo videti slike, ali za primer, ko se slika ne naloži. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = To nadomestno besedilo je bilo ustvarjeno samodejno in je lahko netočno. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Več o tem +pdfjs-editor-new-alt-text-create-automatically-button-label = Samodejno ustvari nadomestno besedilo +pdfjs-editor-new-alt-text-not-now-button = Ne zdaj +pdfjs-editor-new-alt-text-error-title = Nadomestnega besedila ni bilo mogoče samodejno ustvariti +pdfjs-editor-new-alt-text-error-description = Sestavite svoje nadomestno besedilo ali poskusite znova pozneje. +pdfjs-editor-new-alt-text-error-close-button = Zapri +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Prenašanje modela UI za nadomestno besedilo ({ $downloadedSize } od { $totalSize } MB) + .aria-valuetext = Prenašanje modela UI za nadomestno besedilo ({ $downloadedSize } od { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Nadomestno besedilo dodano +pdfjs-editor-new-alt-text-added-button-label = Nadomestno besedilo dodano +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Nadomestno besedilo manjka +pdfjs-editor-new-alt-text-missing-button-label = Nadomestno besedilo manjka +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Oceni nadomestno besedilo +pdfjs-editor-new-alt-text-to-review-button-label = Oceni nadomestno besedilo +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Samodejno ustvarjeno: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Nastavitve nadomestnega besedila slike +pdfjs-image-alt-text-settings-button-label = Nastavitve nadomestnega besedila slike +pdfjs-editor-alt-text-settings-dialog-label = Nastavitve nadomestnega besedila slike +pdfjs-editor-alt-text-settings-automatic-title = Samodejno nadomestno besedilo +pdfjs-editor-alt-text-settings-create-model-button-label = Samodejno ustvari nadomestno besedilo +pdfjs-editor-alt-text-settings-create-model-description = Predlaga opise za pomoč ljudem, ki ne morejo videti slike, ali za primer, ko se slika ne naloži. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model UI za nadomestno besedilo ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Izvaja se lokalno na vaši napravi, tako da vaši podatki ostajajo zasebni. Zahtevano za samodejno nadomestno besedilo. +pdfjs-editor-alt-text-settings-delete-model-button = Izbriši +pdfjs-editor-alt-text-settings-download-model-button = Prenesi +pdfjs-editor-alt-text-settings-downloading-model-button = Prenašanje ... +pdfjs-editor-alt-text-settings-editor-title = Urejevalnik nadomestnega besedila +pdfjs-editor-alt-text-settings-show-dialog-button-label = Ob dodajanju slike takoj prikaži urejevalnik nadomestnega besedila +pdfjs-editor-alt-text-settings-show-dialog-description = Pomaga vam zagotoviti, da imajo vse vaše slike nadomestno besedilo. +pdfjs-editor-alt-text-settings-close-button = Zapri + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Označba odstranjena +pdfjs-editor-undo-bar-message-freetext = Besedilo odstranjeno +pdfjs-editor-undo-bar-message-ink = Risba odstranjena +pdfjs-editor-undo-bar-message-stamp = Slika odstranjena +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } označba odstranjena + [two] { $count } označbi odstranjeni + [few] { $count } označbe odstranjene + *[other] { $count } označb odstranjenih + } +pdfjs-editor-undo-bar-undo-button = + .title = Razveljavi +pdfjs-editor-undo-bar-undo-button-label = Razveljavi +pdfjs-editor-undo-bar-close-button = + .title = Zapri +pdfjs-editor-undo-bar-close-button-label = Zapri diff --git a/public/assets/pdfjs/locale/son/viewer.ftl b/public/assets/pdfjs/locale/son/viewer.ftl new file mode 100644 index 0000000..fa4f6b1 --- /dev/null +++ b/public/assets/pdfjs/locale/son/viewer.ftl @@ -0,0 +1,206 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Moo bisante +pdfjs-previous-button-label = Bisante +pdfjs-next-button = + .title = Jinehere moo +pdfjs-next-button-label = Jine +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Moo +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } ra +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } ka hun { $pagesCount }) ra +pdfjs-zoom-out-button = + .title = Nakasandi +pdfjs-zoom-out-button-label = Nakasandi +pdfjs-zoom-in-button = + .title = Bebbeerandi +pdfjs-zoom-in-button-label = Bebbeerandi +pdfjs-zoom-select = + .title = Bebbeerandi +pdfjs-presentation-mode-button = + .title = Bere cebeyan alhaali +pdfjs-presentation-mode-button-label = Cebeyan alhaali +pdfjs-open-file-button = + .title = Tuku feeri +pdfjs-open-file-button-label = Feeri +pdfjs-print-button = + .title = Kar +pdfjs-print-button-label = Kar + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Goyjinawey +pdfjs-tools-button-label = Goyjinawey +pdfjs-first-page-button = + .title = Koy moo jinaa ga +pdfjs-first-page-button-label = Koy moo jinaa ga +pdfjs-last-page-button = + .title = Koy moo koraa ga +pdfjs-last-page-button-label = Koy moo koraa ga +pdfjs-page-rotate-cw-button = + .title = Kuubi kanbe guma here +pdfjs-page-rotate-cw-button-label = Kuubi kanbe guma here +pdfjs-page-rotate-ccw-button = + .title = Kuubi kanbe wowa here +pdfjs-page-rotate-ccw-button-label = Kuubi kanbe wowa here + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Takadda mayrawey… +pdfjs-document-properties-button-label = Takadda mayrawey… +pdfjs-document-properties-file-name = Tuku maa: +pdfjs-document-properties-file-size = Tuku adadu: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = KB { $size_kb } (cebsu-ize { $size_b }) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = MB { $size_mb } (cebsu-ize { $size_b }) +pdfjs-document-properties-title = Tiiramaa: +pdfjs-document-properties-author = Hantumkaw: +pdfjs-document-properties-subject = Dalil: +pdfjs-document-properties-keywords = Kufalkalimawey: +pdfjs-document-properties-creation-date = Teeyan han: +pdfjs-document-properties-modification-date = Barmayan han: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Teekaw: +pdfjs-document-properties-producer = PDF berandikaw: +pdfjs-document-properties-version = PDF dumi: +pdfjs-document-properties-page-count = Moo hinna: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = Daabu + +## Print + +pdfjs-print-progress-message = Goo ma takaddaa soolu k'a kar se… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Naŋ +pdfjs-printing-not-supported = Yaamar: Karyan ši tee ka timme nda ceecikaa woo. +pdfjs-printing-not-ready = Yaamar: PDF ši zunbu ka timme karyan še. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Kanjari ceraw zuu +pdfjs-toggle-sidebar-button-label = Kanjari ceraw zuu +pdfjs-document-outline-button = + .title = Takaddaa korfur alhaaloo cebe (naagu cee hinka ka haya-izey kul hayandi/kankamandi) +pdfjs-document-outline-button-label = Takadda filla-boŋ +pdfjs-attachments-button = + .title = Hangarey cebe +pdfjs-attachments-button-label = Hangarey +pdfjs-thumbs-button = + .title = Kabeboy biyey cebe +pdfjs-thumbs-button-label = Kabeboy biyey +pdfjs-findbar-button = + .title = Ceeci takaddaa ra +pdfjs-findbar-button-label = Ceeci + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } moo +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Kabeboy bii { $page } moo še + +## Find panel button title and messages + +pdfjs-find-input = + .title = Ceeci + .placeholder = Ceeci takaddaa ra… +pdfjs-find-previous-button = + .title = Kalimaɲaŋoo bangayri bisantaa ceeci +pdfjs-find-previous-button-label = Bisante +pdfjs-find-next-button = + .title = Kalimaɲaŋoo hiino bangayroo ceeci +pdfjs-find-next-button-label = Jine +pdfjs-find-highlight-checkbox = Ikul šilbay +pdfjs-find-match-case-checkbox-label = Harfu-beeriyan hawgay +pdfjs-find-reached-top = A too moŋoo boŋoo, koy jine ka šinitin nda cewoo +pdfjs-find-reached-bottom = A too moɲoo cewoo, koy jine šintioo ga +pdfjs-find-not-found = Kalimaɲaa mana duwandi + +## Predefined zoom values + +pdfjs-page-scale-width = Mooo hayyan +pdfjs-page-scale-fit = Moo sawayan +pdfjs-page-scale-auto = Boŋše azzaati barmayyan +pdfjs-page-scale-actual = Adadu cimi +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Firka bangay kaŋ PDF goo ma zumandi. +pdfjs-invalid-file-error = PDF tuku laala wala laybante. +pdfjs-missing-file-error = PDF tuku kumante. +pdfjs-unexpected-response-error = Manti feršikaw tuuruyan maatante. +pdfjs-rendering-error = Firka bangay kaŋ moɲoo goo ma willandi. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = { $type } maasa-caw] + +## Password + +pdfjs-password-label = Šennikufal dam ka PDF tukoo woo feeri. +pdfjs-password-invalid = Šennikufal laalo. Ceeci koyne taare. +pdfjs-password-ok-button = Ayyo +pdfjs-password-cancel-button = Naŋ +pdfjs-web-fonts-disabled = Interneti šigirawey kay: ši hin ka goy nda PDF šigira hurantey. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/sq/viewer.ftl b/public/assets/pdfjs/locale/sq/viewer.ftl new file mode 100644 index 0000000..2b1c91a --- /dev/null +++ b/public/assets/pdfjs/locale/sq/viewer.ftl @@ -0,0 +1,506 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Faqja e Mëparshme +pdfjs-previous-button-label = E mëparshmja +pdfjs-next-button = + .title = Faqja Pasuese +pdfjs-next-button-label = Pasuesja +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Faqe +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = nga { $pagesCount } gjithsej +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } nga { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zvogëlojeni +pdfjs-zoom-out-button-label = Zvogëlojeni +pdfjs-zoom-in-button = + .title = Zmadhojeni +pdfjs-zoom-in-button-label = Zmadhojini +pdfjs-zoom-select = + .title = Zmadhim/Zvogëlim +pdfjs-presentation-mode-button = + .title = Kalo te Mënyra Paraqitje +pdfjs-presentation-mode-button-label = Mënyra Paraqitje +pdfjs-open-file-button = + .title = Hapni Kartelë +pdfjs-open-file-button-label = Hape +pdfjs-print-button = + .title = Shtypje +pdfjs-print-button-label = Shtype +pdfjs-save-button = + .title = Ruaje +pdfjs-save-button-label = Ruaje +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Shkarkojeni +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Shkarkoje +pdfjs-bookmark-button = + .title = Faqja e Tanishme (Shihni URL nga Faqja e Tanishme) +pdfjs-bookmark-button-label = Faqja e Tanishme + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Mjete +pdfjs-tools-button-label = Mjete +pdfjs-first-page-button = + .title = Kaloni te Faqja e Parë +pdfjs-first-page-button-label = Kaloni te Faqja e Parë +pdfjs-last-page-button = + .title = Kaloni te Faqja e Fundit +pdfjs-last-page-button-label = Kaloni te Faqja e Fundit +pdfjs-page-rotate-cw-button = + .title = Rrotullojeni Në Kahun Orar +pdfjs-page-rotate-cw-button-label = Rrotulloje Në Kahun Orar +pdfjs-page-rotate-ccw-button = + .title = Rrotullojeni Në Kahun Kundërorar +pdfjs-page-rotate-ccw-button-label = Rrotulloje Në Kahun Kundërorar +pdfjs-cursor-text-select-tool-button = + .title = Aktivizo Mjet Përzgjedhjeje Teksti +pdfjs-cursor-text-select-tool-button-label = Mjet Përzgjedhjeje Teksti +pdfjs-cursor-hand-tool-button = + .title = Aktivizo Mjetin Dorë +pdfjs-cursor-hand-tool-button-label = Mjeti Dorë +pdfjs-scroll-page-button = + .title = Përdor Rrëshqitje Në Faqe +pdfjs-scroll-page-button-label = Rrëshqitje Në Faqe +pdfjs-scroll-vertical-button = + .title = Përdor Rrëshqitje Vertikale +pdfjs-scroll-vertical-button-label = Rrëshqitje Vertikale +pdfjs-scroll-horizontal-button = + .title = Përdor Rrëshqitje Horizontale +pdfjs-scroll-horizontal-button-label = Rrëshqitje Horizontale +pdfjs-scroll-wrapped-button = + .title = Përdor Rrëshqitje Me Mbështjellje +pdfjs-scroll-wrapped-button-label = Rrëshqitje Me Mbështjellje + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Veti Dokumenti… +pdfjs-document-properties-button-label = Veti Dokumenti… +pdfjs-document-properties-file-name = Emër kartele: +pdfjs-document-properties-file-size = Madhësi kartele: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bajte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bajte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bajte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bajte) +pdfjs-document-properties-title = Titull: +pdfjs-document-properties-author = Autor: +pdfjs-document-properties-subject = Subjekt: +pdfjs-document-properties-keywords = Fjalëkyçe: +pdfjs-document-properties-creation-date = Datë Krijimi: +pdfjs-document-properties-modification-date = Datë Ndryshimi: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Krijues: +pdfjs-document-properties-producer = Prodhues PDF-je: +pdfjs-document-properties-version = Version PDF-je: +pdfjs-document-properties-page-count = Numër Faqesh: +pdfjs-document-properties-page-size = Madhësi Faqeje: +pdfjs-document-properties-page-size-unit-inches = inç +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = portret +pdfjs-document-properties-page-size-orientation-landscape = së gjeri +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Parje e Shpjetë në Web: +pdfjs-document-properties-linearized-yes = Po +pdfjs-document-properties-linearized-no = Jo +pdfjs-document-properties-close-button = Mbylleni + +## Print + +pdfjs-print-progress-message = Po përgatitet dokumenti për shtypje… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Anuloje +pdfjs-printing-not-supported = Kujdes: Shtypja s’mbulohet plotësisht nga ky shfletues. +pdfjs-printing-not-ready = Kujdes: PDF-ja s’është ngarkuar plotësisht që ta shtypni. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Shfaqni/Fshihni Anështyllën +pdfjs-toggle-sidebar-notification-button = + .title = Hap/Mbyll Anështylë (dokumenti përmban përvijim/nashkëngjitje/shtresa) +pdfjs-toggle-sidebar-button-label = Shfaq/Fshih Anështyllën +pdfjs-document-outline-button = + .title = Shfaqni Përvijim Dokumenti (dyklikoni që të shfaqen/fshihen krejt elementët) +pdfjs-document-outline-button-label = Përvijim Dokumenti +pdfjs-attachments-button = + .title = Shfaqni Bashkëngjitje +pdfjs-attachments-button-label = Bashkëngjitje +pdfjs-layers-button = + .title = Shfaq Shtresa (dyklikoni që të rikthehen krejt shtresat në gjendjen e tyre parazgjedhje) +pdfjs-layers-button-label = Shtresa +pdfjs-thumbs-button = + .title = Shfaqni Miniatura +pdfjs-thumbs-button-label = Miniatura +pdfjs-current-outline-item-button = + .title = Gjej Objektin e Tanishëm të Përvijuar +pdfjs-current-outline-item-button-label = Objekt i Tanishëm i Përvijuar +pdfjs-findbar-button = + .title = Gjeni në Dokument +pdfjs-findbar-button-label = Gjej +pdfjs-additional-layers = Shtresa Shtesë + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Faqja { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniaturë e Faqes { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Gjej + .placeholder = Gjeni në dokument… +pdfjs-find-previous-button = + .title = Gjeni hasjen e mëparshme të togfjalëshit +pdfjs-find-previous-button-label = E mëparshmja +pdfjs-find-next-button = + .title = Gjeni hasjen pasuese të togfjalëshit +pdfjs-find-next-button-label = Pasuesja +pdfjs-find-highlight-checkbox = Theksoji të tëra +pdfjs-find-match-case-checkbox-label = Siç Është Shkruar +pdfjs-find-match-diacritics-checkbox-label = Me Përputhje Me Shenjat Diakritike +pdfjs-find-entire-word-checkbox-label = Fjalë të Plota +pdfjs-find-reached-top = U mbërrit në krye të dokumentit, vazhduar prej fundit +pdfjs-find-reached-bottom = U mbërrit në fund të dokumentit, vazhduar prej kreut +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } nga { $total } përputhje + *[other] { $current } nga { $total } përputhje + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Më tepër se { $limit } përputhje + *[other] Më tepër se { $limit } përputhje + } +pdfjs-find-not-found = Togfjalësh që s’gjendet + +## Predefined zoom values + +pdfjs-page-scale-width = Gjerësi Faqeje +pdfjs-page-scale-fit = Sa Nxë Faqja +pdfjs-page-scale-auto = Zoom i Vetvetishëm +pdfjs-page-scale-actual = Madhësia Faktike +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Faqja { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ndodhi një gabim gjatë ngarkimit të PDF-së. +pdfjs-invalid-file-error = Kartelë PDF e pavlefshme ose e dëmtuar. +pdfjs-missing-file-error = Kartelë PDF që mungon. +pdfjs-unexpected-response-error = Përgjigje shërbyesi e papritur. +pdfjs-rendering-error = Ndodhi një gabim gjatë riprodhimit të faqes. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Nënvizim { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Jepni fjalëkalimin që të hapet kjo kartelë PDF. +pdfjs-password-invalid = Fjalëkalim i pavlefshëm. Ju lutemi, riprovoni. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Anuloje +pdfjs-web-fonts-disabled = Shkronjat Web janë të çaktivizuara: s’arrihet të përdoren shkronja të trupëzuara në PDF. + +## Editing + +pdfjs-editor-free-text-button = + .title = Tekst +pdfjs-editor-free-text-button-label = Tekst +pdfjs-editor-ink-button = + .title = Vizatoni +pdfjs-editor-ink-button-label = Vizatoni +pdfjs-editor-stamp-button = + .title = Shtoni ose përpunoni figura +pdfjs-editor-stamp-button-label = Shtoni ose përpunoni figura +pdfjs-editor-highlight-button = + .title = Theksim +pdfjs-editor-highlight-button-label = Theksoje +pdfjs-highlight-floating-button1 = + .title = Theksim + .aria-label = Theksim +pdfjs-highlight-floating-button-label = Theksim + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Hiq vizatim +pdfjs-editor-remove-freetext-button = + .title = Hiq tekst +pdfjs-editor-remove-stamp-button = + .title = Hiq figurë +pdfjs-editor-remove-highlight-button = + .title = Hiqe theksimin + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Ngjyrë +pdfjs-editor-free-text-size-input = Madhësi +pdfjs-editor-ink-color-input = Ngjyrë +pdfjs-editor-ink-thickness-input = Trashësi +pdfjs-editor-ink-opacity-input = Patejdukshmëri +pdfjs-editor-stamp-add-image-button = + .title = Shtoni figurë +pdfjs-editor-stamp-add-image-button-label = Shtoni figurë +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Trashësi +pdfjs-editor-free-highlight-thickness-title = + .title = Ndryshoni trashësinë kur theksoni objekte tjetër nga tekst +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Përpunues Tekstesh + .default-content = Filloni të shtypni… +pdfjs-free-text = + .aria-label = Përpunues Tekstesh +pdfjs-free-text-default-content = Filloni të shtypni… +pdfjs-ink = + .aria-label = Përpunues Vizatimesh +pdfjs-ink-canvas = + .aria-label = Figurë e krijuar nga përdoruesi + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Tekst alternativ +pdfjs-editor-alt-text-edit-button = + .aria-label = Përpunoni tekst alternativ +pdfjs-editor-alt-text-edit-button-label = Përpunoni tekst alternativ +pdfjs-editor-alt-text-dialog-label = Zgjidhni një mundësi +pdfjs-editor-alt-text-dialog-description = Teksti alt (tekst alternativ) vjen në ndihmë kur njerëzit s’mund të shohin figurën, ose kur ajo nuk ngarkohet. +pdfjs-editor-alt-text-add-description-label = Shtoni një përshkrim +pdfjs-editor-alt-text-add-description-description = Synoni për 1-2 togfjalësha që përshkruajnë subjektin, rrethanat apo veprimet. +pdfjs-editor-alt-text-mark-decorative-label = Vëri shenjë si dekorative +pdfjs-editor-alt-text-mark-decorative-description = Kjo përdoret për figura zbukuruese, fjala vjen, anë, ose watermark-e. +pdfjs-editor-alt-text-cancel-button = Anuloje +pdfjs-editor-alt-text-save-button = Ruaje +pdfjs-editor-alt-text-decorative-tooltip = Iu vu shenjë si dekorative +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Për shembull, “Një djalosh ulet në një tryezë të hajë” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Tekst alternativ + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Cepi i sipërm majtas — ripërmasojeni +pdfjs-editor-resizer-label-top-middle = Mesi i pjesës sipër — ripërmasojeni +pdfjs-editor-resizer-label-top-right = Cepi i sipërm djathtas — ripërmasojeni +pdfjs-editor-resizer-label-middle-right = Djathtas në mes — ripërmasojeni +pdfjs-editor-resizer-label-bottom-right = Cepi i poshtëm djathtas — ripërmasojeni +pdfjs-editor-resizer-label-bottom-middle = Mesi i pjesës poshtë — ripërmasojeni +pdfjs-editor-resizer-label-bottom-left = Cepi i poshtëm — ripërmasojeni +pdfjs-editor-resizer-label-middle-left = Majtas në mes — ripërmasojeni +pdfjs-editor-resizer-top-left = + .aria-label = Cepi i sipërm majtas — ripërmasojeni +pdfjs-editor-resizer-top-middle = + .aria-label = Mesi i pjesës sipër — ripërmasojeni +pdfjs-editor-resizer-top-right = + .aria-label = Cepi i sipërm djathtas — ripërmasojeni +pdfjs-editor-resizer-middle-right = + .aria-label = Djathtas në mes — ripërmasojeni +pdfjs-editor-resizer-bottom-right = + .aria-label = Cepi i poshtëm djathtas — ripërmasojeni +pdfjs-editor-resizer-bottom-middle = + .aria-label = Mesi i pjesës poshtë — ripërmasojeni +pdfjs-editor-resizer-bottom-left = + .aria-label = Cepi i poshtëm — ripërmasojeni +pdfjs-editor-resizer-middle-left = + .aria-label = Majtas në mes — ripërmasojeni + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Ngjyrë theksimi +pdfjs-editor-colorpicker-button = + .title = Ndryshoni ngjyrë +pdfjs-editor-colorpicker-dropdown = + .aria-label = Zgjedhje ngjyre +pdfjs-editor-colorpicker-yellow = + .title = E verdhë +pdfjs-editor-colorpicker-green = + .title = E gjelbër +pdfjs-editor-colorpicker-blue = + .title = Blu +pdfjs-editor-colorpicker-pink = + .title = Rozë +pdfjs-editor-colorpicker-red = + .title = E kuqe + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Shfaqi krejt +pdfjs-editor-highlight-show-all-button = + .title = Shfaqi krejt + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Përpunoni tekst alternativ (përshkrim figure) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Shtoni tekst alternativ (përshkrim figure) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Shkruani këtu përshkrimin tuaj… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Përshkrim i shkurtër për persona që s’munden të shohin figurën, ose për kur figura nuk ngarkohet dot. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ky tekst alternativ qe krijuar automatikisht dhe mund të jetë i pasaktë. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Mësoni më tepër +pdfjs-editor-new-alt-text-create-automatically-button-label = Krijo automatikisht tekst alternativ +pdfjs-editor-new-alt-text-not-now-button = Jo tani +pdfjs-editor-new-alt-text-error-title = S’u krijua dot automatikisht tekst alternativ +pdfjs-editor-new-alt-text-error-description = Ju lutemi, shkruani tekstin tuaj alternativ, ose riprovoni më vonë. +pdfjs-editor-new-alt-text-error-close-button = Mbylle +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Po shkarkohet model IA teksti alternativ ({ $downloadedSize } nga { $totalSize } MB) + .aria-valuetext = Po shkarkohet model IA teksti alternativ ({ $downloadedSize } nga { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = U shtua tekst alternativ +pdfjs-editor-new-alt-text-added-button-label = U shtua tekst alternativ +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Mungon tekst alternativ +pdfjs-editor-new-alt-text-missing-button-label = Mungon tekst alternativ +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Shqyrtoni tekst alternativ +pdfjs-editor-new-alt-text-to-review-button-label = Shqyrtoni tekst alternativ +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Krijuar automatikisht: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Rregullime teksti alternativ figure +pdfjs-image-alt-text-settings-button-label = Rregullime teksti alternativ figure +pdfjs-editor-alt-text-settings-dialog-label = Rregullime teksti alternativ figure +pdfjs-editor-alt-text-settings-automatic-title = Tekst alternativ i automatizuar +pdfjs-editor-alt-text-settings-create-model-button-label = Krijo automatikisht tekst alternativ +pdfjs-editor-alt-text-settings-create-model-description = Sugjeron përshkrime, për të ndihmuar persona që s’munden të shohin figurën, ose për kur figura nuk ngarkohet dot. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Model IA teksti alternativ ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Xhiron lokalisht në pajisjen tuaj, pra të dhënat tuaja mbeten private. E domosdoshme për tekst të automatizuar alternativ. +pdfjs-editor-alt-text-settings-delete-model-button = Fshije +pdfjs-editor-alt-text-settings-download-model-button = Shkarkoje +pdfjs-editor-alt-text-settings-downloading-model-button = Po shkarkohet… +pdfjs-editor-alt-text-settings-editor-title = Përpunues teksti alternativ +pdfjs-editor-alt-text-settings-show-dialog-button-label = Shfaq menjëherë përpunues teksti alternativ, kur shtohet një figurë +pdfjs-editor-alt-text-settings-show-dialog-description = Ju ndihmon të siguroheni se krejt figurat tuaja kanë tekst alternativ. +pdfjs-editor-alt-text-settings-close-button = Mbylle + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = U hoq theksimi +pdfjs-editor-undo-bar-message-freetext = U hoq tekst +pdfjs-editor-undo-bar-message-ink = U hoq vizatim +pdfjs-editor-undo-bar-message-stamp = U hoq figurë +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] U hoq { $count } shënim + *[other] U hoqën { $count } shënime + } +pdfjs-editor-undo-bar-undo-button = + .title = Zhbëje +pdfjs-editor-undo-bar-undo-button-label = Zhbëje +pdfjs-editor-undo-bar-close-button = + .title = Mbylle +pdfjs-editor-undo-bar-close-button-label = Mbylle diff --git a/public/assets/pdfjs/locale/sr/viewer.ftl b/public/assets/pdfjs/locale/sr/viewer.ftl new file mode 100644 index 0000000..e125dfb --- /dev/null +++ b/public/assets/pdfjs/locale/sr/viewer.ftl @@ -0,0 +1,421 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Претходна страница +pdfjs-previous-button-label = Претходна +pdfjs-next-button = + .title = Следећа страница +pdfjs-next-button-label = Следећа +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Страница +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = од { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } од { $pagesCount }) +pdfjs-zoom-out-button = + .title = Умањи +pdfjs-zoom-out-button-label = Умањи +pdfjs-zoom-in-button = + .title = Увеличај +pdfjs-zoom-in-button-label = Увеличај +pdfjs-zoom-select = + .title = Увеличавање +pdfjs-presentation-mode-button = + .title = Промени на приказ у режиму презентације +pdfjs-presentation-mode-button-label = Режим презентације +pdfjs-open-file-button = + .title = Отвори датотеку +pdfjs-open-file-button-label = Отвори +pdfjs-print-button = + .title = Штампај +pdfjs-print-button-label = Штампај +pdfjs-save-button = + .title = Сачувај +pdfjs-save-button-label = Сачувај +pdfjs-bookmark-button = + .title = Тренутна страница (погледајте URL са тренутне странице) +pdfjs-bookmark-button-label = Тренутна страница + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Алатке +pdfjs-tools-button-label = Алатке +pdfjs-first-page-button = + .title = Иди на прву страницу +pdfjs-first-page-button-label = Иди на прву страницу +pdfjs-last-page-button = + .title = Иди на последњу страницу +pdfjs-last-page-button-label = Иди на последњу страницу +pdfjs-page-rotate-cw-button = + .title = Ротирај у смеру казаљке на сату +pdfjs-page-rotate-cw-button-label = Ротирај у смеру казаљке на сату +pdfjs-page-rotate-ccw-button = + .title = Ротирај у смеру супротном од казаљке на сату +pdfjs-page-rotate-ccw-button-label = Ротирај у смеру супротном од казаљке на сату +pdfjs-cursor-text-select-tool-button = + .title = Омогући алат за селектовање текста +pdfjs-cursor-text-select-tool-button-label = Алат за селектовање текста +pdfjs-cursor-hand-tool-button = + .title = Омогући алат за померање +pdfjs-cursor-hand-tool-button-label = Алат за померање +pdfjs-scroll-page-button = + .title = Користи скроловање по омоту +pdfjs-scroll-page-button-label = Скроловање странице +pdfjs-scroll-vertical-button = + .title = Користи вертикално скроловање +pdfjs-scroll-vertical-button-label = Вертикално скроловање +pdfjs-scroll-horizontal-button = + .title = Користи хоризонтално скроловање +pdfjs-scroll-horizontal-button-label = Хоризонтално скроловање +pdfjs-scroll-wrapped-button = + .title = Користи скроловање по омоту +pdfjs-scroll-wrapped-button-label = Скроловање по омоту +pdfjs-spread-none-button = + .title = Немој спајати ширења страница +pdfjs-spread-none-button-label = Без распростирања +pdfjs-spread-odd-button = + .title = Споји ширења страница које почињу непарним бројем +pdfjs-spread-odd-button-label = Непарна распростирања +pdfjs-spread-even-button = + .title = Споји ширења страница које почињу парним бројем +pdfjs-spread-even-button-label = Парна распростирања + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Параметри документа… +pdfjs-document-properties-button-label = Параметри документа… +pdfjs-document-properties-file-name = Име датотеке: +pdfjs-document-properties-file-size = Величина датотеке: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } B) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } B) +pdfjs-document-properties-title = Наслов: +pdfjs-document-properties-author = Аутор: +pdfjs-document-properties-subject = Тема: +pdfjs-document-properties-keywords = Кључне речи: +pdfjs-document-properties-creation-date = Датум креирања: +pdfjs-document-properties-modification-date = Датум модификације: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Стваралац: +pdfjs-document-properties-producer = PDF произвођач: +pdfjs-document-properties-version = PDF верзија: +pdfjs-document-properties-page-count = Број страница: +pdfjs-document-properties-page-size = Величина странице: +pdfjs-document-properties-page-size-unit-inches = ин +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = усправно +pdfjs-document-properties-page-size-orientation-landscape = водоравно +pdfjs-document-properties-page-size-name-a-three = А3 +pdfjs-document-properties-page-size-name-a-four = А4 +pdfjs-document-properties-page-size-name-letter = Слово +pdfjs-document-properties-page-size-name-legal = Права + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Брз веб приказ: +pdfjs-document-properties-linearized-yes = Да +pdfjs-document-properties-linearized-no = Не +pdfjs-document-properties-close-button = Затвори + +## Print + +pdfjs-print-progress-message = Припремам документ за штампање… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Откажи +pdfjs-printing-not-supported = Упозорење: Штампање није у потпуности подржано у овом прегледачу. +pdfjs-printing-not-ready = Упозорење: PDF није у потпуности учитан за штампу. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Прикажи/сакриј бочни панел +pdfjs-toggle-sidebar-notification-button = + .title = Прикажи/сакриј бочни панел (документ садржи контуру/прилоге/слојеве) +pdfjs-toggle-sidebar-button-label = Прикажи/сакриј бочни панел +pdfjs-document-outline-button = + .title = Прикажи структуру документа (двоструким кликом проширујете/скупљате све ставке) +pdfjs-document-outline-button-label = Контура документа +pdfjs-attachments-button = + .title = Прикажи прилоге +pdfjs-attachments-button-label = Прилози +pdfjs-layers-button = + .title = Прикажи слојеве (дупли клик за враћање свих слојева у подразумевано стање) +pdfjs-layers-button-label = Слојеви +pdfjs-thumbs-button = + .title = Прикажи сличице +pdfjs-thumbs-button-label = Сличице +pdfjs-current-outline-item-button = + .title = Пронађите тренутни елемент структуре +pdfjs-current-outline-item-button-label = Тренутна контура +pdfjs-findbar-button = + .title = Пронађи у документу +pdfjs-findbar-button-label = Пронађи +pdfjs-additional-layers = Додатни слојеви + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Страница { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Сличица од странице { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Пронађи + .placeholder = Пронађи у документу… +pdfjs-find-previous-button = + .title = Пронађи претходно појављивање фразе +pdfjs-find-previous-button-label = Претходна +pdfjs-find-next-button = + .title = Пронађи следеће појављивање фразе +pdfjs-find-next-button-label = Следећа +pdfjs-find-highlight-checkbox = Истакнути све +pdfjs-find-match-case-checkbox-label = Подударања +pdfjs-find-match-diacritics-checkbox-label = Дијакритика +pdfjs-find-entire-word-checkbox-label = Целе речи +pdfjs-find-reached-top = Достигнут врх документа, наставио са дна +pdfjs-find-reached-bottom = Достигнуто дно документа, наставио са врха +pdfjs-find-not-found = Фраза није пронађена + +## Predefined zoom values + +pdfjs-page-scale-width = Ширина странице +pdfjs-page-scale-fit = Прилагоди страницу +pdfjs-page-scale-auto = Аутоматско увеличавање +pdfjs-page-scale-actual = Стварна величина +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Страница { $page } + +## Loading indicator messages + +pdfjs-loading-error = Дошло је до грешке приликом учитавања PDF-а. +pdfjs-invalid-file-error = PDF датотека је неважећа или је оштећена. +pdfjs-missing-file-error = Недостаје PDF датотека. +pdfjs-unexpected-response-error = Неочекиван одговор од сервера. +pdfjs-rendering-error = Дошло је до грешке приликом рендеровања ове странице. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } коментар] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Унесите лозинку да бисте отворили овај PDF докуменат. +pdfjs-password-invalid = Неисправна лозинка. Покушајте поново. +pdfjs-password-ok-button = У реду +pdfjs-password-cancel-button = Откажи +pdfjs-web-fonts-disabled = Веб фонтови су онемогућени: не могу користити уграђене PDF фонтове. + +## Editing + +pdfjs-editor-free-text-button = + .title = Текст +pdfjs-editor-free-text-button-label = Текст +pdfjs-editor-ink-button = + .title = Цртај +pdfjs-editor-ink-button-label = Цртај +pdfjs-editor-stamp-button = + .title = Додај или уреди слике +pdfjs-editor-stamp-button-label = Додај или уреди слике +pdfjs-editor-highlight-button = + .title = Означи +pdfjs-editor-highlight-button-label = Означи +pdfjs-highlight-floating-button1 = + .title = Означи + .aria-label = Означи +pdfjs-highlight-floating-button-label = Означи + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Уклони цртеж +pdfjs-editor-remove-freetext-button = + .title = Уклони текст +pdfjs-editor-remove-stamp-button = + .title = Уклони слику +pdfjs-editor-remove-highlight-button = + .title = Уклони ознаку + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Боја +pdfjs-editor-free-text-size-input = Величина +pdfjs-editor-ink-color-input = Боја +pdfjs-editor-ink-thickness-input = Дебљина +pdfjs-editor-ink-opacity-input = Опацитет +pdfjs-editor-stamp-add-image-button = + .title = Додај слику +pdfjs-editor-stamp-add-image-button-label = Додај слику +pdfjs-editor-free-highlight-thickness-title = + .title = Промени дебљину при означавању других ставки сем текста +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Уређивач текста + .default-content = Почни куцати… +pdfjs-free-text = + .aria-label = Уређивач текста +pdfjs-free-text-default-content = Почни куцање… +pdfjs-ink = + .aria-label = Уређивач цртежа +pdfjs-ink-canvas = + .aria-label = Кориснички направљена слика + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Алтернативни текст +pdfjs-editor-alt-text-edit-button = + .aria-label = Уреди алтернативни текст +pdfjs-editor-alt-text-edit-button-label = Уреди алтернативни текст +pdfjs-editor-alt-text-dialog-label = Одабери опцију +pdfjs-editor-alt-text-dialog-description = Алтернативни текст помаже слепим и слабовидим особама или када се слика не учита. +pdfjs-editor-alt-text-add-description-label = Додај опис +pdfjs-editor-alt-text-add-description-description = Сажмите у 1-2 реченице које описују предмет, окружење или радње. +pdfjs-editor-alt-text-mark-decorative-label = Означи као украсно +pdfjs-editor-alt-text-mark-decorative-description = Ово је за украсне слике, као што су ивице или водени печати. +pdfjs-editor-alt-text-cancel-button = Откажи +pdfjs-editor-alt-text-save-button = Сачувај +pdfjs-editor-alt-text-decorative-tooltip = Означено као украсно +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = На пример: „Младић седа за сто да једе“ +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Алтернативни текст + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Горњи леви угао — промени величину +pdfjs-editor-resizer-label-top-middle = Средина горе — промени величину +pdfjs-editor-resizer-label-top-right = Горњи десни угао — промени величину +pdfjs-editor-resizer-label-middle-right = Средина десно — промени величину +pdfjs-editor-resizer-label-bottom-right = Доњи десни угао — промени величину +pdfjs-editor-resizer-label-bottom-middle = Средина доле — промени величину +pdfjs-editor-resizer-label-bottom-left = Доњи леви угао — промени величину +pdfjs-editor-resizer-label-middle-left = Средина лево — промени величину +pdfjs-editor-resizer-top-left = + .aria-label = Горњи леви угао — промени величину +pdfjs-editor-resizer-top-middle = + .aria-label = Средина горе — промени величину +pdfjs-editor-resizer-top-right = + .aria-label = Горњи десни угао — промени величину +pdfjs-editor-resizer-middle-right = + .aria-label = Средина десно — промени величину +pdfjs-editor-resizer-bottom-right = + .aria-label = Доњи десни угао — промени величину +pdfjs-editor-resizer-bottom-middle = + .aria-label = Средина доле — промени величину +pdfjs-editor-resizer-bottom-left = + .aria-label = Доњи леви угао — промени величину +pdfjs-editor-resizer-middle-left = + .aria-label = Средина лево — промени величину + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Боја означавања +pdfjs-editor-colorpicker-button = + .title = Промени боју +pdfjs-editor-colorpicker-dropdown = + .aria-label = Избор боја +pdfjs-editor-colorpicker-yellow = + .title = Жута +pdfjs-editor-colorpicker-green = + .title = Зелена +pdfjs-editor-colorpicker-blue = + .title = Плава +pdfjs-editor-colorpicker-pink = + .title = Розе +pdfjs-editor-colorpicker-red = + .title = Црвена + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Прикажи све +pdfjs-editor-highlight-show-all-button = + .title = Прикажи све + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Уреди алтернативни текст (опис слике) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Додај алтернативни текст (опис слике) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Напиши опис овде… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Кратак опис за слепе и слабовиде људе или када се слика не успе учитати. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Овај алтернативни текст је направљен аутоматски и може бити нетачан. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Сазнајте више +pdfjs-editor-new-alt-text-create-automatically-button-label = Прави алтернативни текст аутоматски +pdfjs-editor-new-alt-text-not-now-button = Не сада + +## Image alt-text settings + diff --git a/public/assets/pdfjs/locale/sv-SE/viewer.ftl b/public/assets/pdfjs/locale/sv-SE/viewer.ftl new file mode 100644 index 0000000..6c4c610 --- /dev/null +++ b/public/assets/pdfjs/locale/sv-SE/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Föregående sida +pdfjs-previous-button-label = Föregående +pdfjs-next-button = + .title = Nästa sida +pdfjs-next-button-label = Nästa +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Sida +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = av { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } av { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zooma ut +pdfjs-zoom-out-button-label = Zooma ut +pdfjs-zoom-in-button = + .title = Zooma in +pdfjs-zoom-in-button-label = Zooma in +pdfjs-zoom-select = + .title = Zoom +pdfjs-presentation-mode-button = + .title = Byt till presentationsläge +pdfjs-presentation-mode-button-label = Presentationsläge +pdfjs-open-file-button = + .title = Öppna fil +pdfjs-open-file-button-label = Öppna +pdfjs-print-button = + .title = Skriv ut +pdfjs-print-button-label = Skriv ut +pdfjs-save-button = + .title = Spara +pdfjs-save-button-label = Spara +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Hämta +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Hämta +pdfjs-bookmark-button = + .title = Aktuell sida (Visa URL från aktuell sida) +pdfjs-bookmark-button-label = Aktuell sida + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Verktyg +pdfjs-tools-button-label = Verktyg +pdfjs-first-page-button = + .title = Gå till första sidan +pdfjs-first-page-button-label = Gå till första sidan +pdfjs-last-page-button = + .title = Gå till sista sidan +pdfjs-last-page-button-label = Gå till sista sidan +pdfjs-page-rotate-cw-button = + .title = Rotera medurs +pdfjs-page-rotate-cw-button-label = Rotera medurs +pdfjs-page-rotate-ccw-button = + .title = Rotera moturs +pdfjs-page-rotate-ccw-button-label = Rotera moturs +pdfjs-cursor-text-select-tool-button = + .title = Aktivera textmarkeringsverktyg +pdfjs-cursor-text-select-tool-button-label = Textmarkeringsverktyg +pdfjs-cursor-hand-tool-button = + .title = Aktivera handverktyg +pdfjs-cursor-hand-tool-button-label = Handverktyg +pdfjs-scroll-page-button = + .title = Använd sidrullning +pdfjs-scroll-page-button-label = Sidrullning +pdfjs-scroll-vertical-button = + .title = Använd vertikal rullning +pdfjs-scroll-vertical-button-label = Vertikal rullning +pdfjs-scroll-horizontal-button = + .title = Använd horisontell rullning +pdfjs-scroll-horizontal-button-label = Horisontell rullning +pdfjs-scroll-wrapped-button = + .title = Använd överlappande rullning +pdfjs-scroll-wrapped-button-label = Överlappande rullning +pdfjs-spread-none-button = + .title = Visa enkelsidor +pdfjs-spread-none-button-label = Enkelsidor +pdfjs-spread-odd-button = + .title = Visa uppslag med olika sidnummer till vänster +pdfjs-spread-odd-button-label = Uppslag med framsida +pdfjs-spread-even-button = + .title = Visa uppslag med lika sidnummer till vänster +pdfjs-spread-even-button-label = Uppslag utan framsida + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Dokumentegenskaper… +pdfjs-document-properties-button-label = Dokumentegenskaper… +pdfjs-document-properties-file-name = Filnamn: +pdfjs-document-properties-file-size = Filstorlek: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } kB ({ $b } byte) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } byte) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } kB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Titel: +pdfjs-document-properties-author = Författare: +pdfjs-document-properties-subject = Ämne: +pdfjs-document-properties-keywords = Nyckelord: +pdfjs-document-properties-creation-date = Skapades: +pdfjs-document-properties-modification-date = Ändrades: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Skapare: +pdfjs-document-properties-producer = PDF-producent: +pdfjs-document-properties-version = PDF-version: +pdfjs-document-properties-page-count = Sidantal: +pdfjs-document-properties-page-size = Pappersstorlek: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = porträtt +pdfjs-document-properties-page-size-orientation-landscape = landskap +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Snabb webbvisning: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Nej +pdfjs-document-properties-close-button = Stäng + +## Print + +pdfjs-print-progress-message = Förbereder sidor för utskrift… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Avbryt +pdfjs-printing-not-supported = Varning: Utskrifter stöds inte helt av den här webbläsaren. +pdfjs-printing-not-ready = Varning: PDF:en är inte klar för utskrift. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Visa/dölj sidofält +pdfjs-toggle-sidebar-notification-button = + .title = Växla sidofält (dokumentet innehåller dokumentstruktur/bilagor/lager) +pdfjs-toggle-sidebar-button-label = Visa/dölj sidofält +pdfjs-document-outline-button = + .title = Visa dokumentdisposition (dubbelklicka för att expandera/komprimera alla objekt) +pdfjs-document-outline-button-label = Dokumentöversikt +pdfjs-attachments-button = + .title = Visa Bilagor +pdfjs-attachments-button-label = Bilagor +pdfjs-layers-button = + .title = Visa lager (dubbelklicka för att återställa alla lager till standardläge) +pdfjs-layers-button-label = Lager +pdfjs-thumbs-button = + .title = Visa miniatyrer +pdfjs-thumbs-button-label = Miniatyrer +pdfjs-current-outline-item-button = + .title = Hitta aktuellt dispositionsobjekt +pdfjs-current-outline-item-button-label = Aktuellt dispositionsobjekt +pdfjs-findbar-button = + .title = Sök i dokument +pdfjs-findbar-button-label = Sök +pdfjs-additional-layers = Ytterligare lager + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Sida { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatyr av sida { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Sök + .placeholder = Sök i dokument… +pdfjs-find-previous-button = + .title = Hitta föregående förekomst av frasen +pdfjs-find-previous-button-label = Föregående +pdfjs-find-next-button = + .title = Hitta nästa förekomst av frasen +pdfjs-find-next-button-label = Nästa +pdfjs-find-highlight-checkbox = Markera alla +pdfjs-find-match-case-checkbox-label = Matcha versal/gemen +pdfjs-find-match-diacritics-checkbox-label = Matcha diakritiska tecken +pdfjs-find-entire-word-checkbox-label = Hela ord +pdfjs-find-reached-top = Nådde början av dokumentet, började från slutet +pdfjs-find-reached-bottom = Nådde slutet på dokumentet, började från början +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } av { $total } match + *[other] { $current } av { $total } matchningar + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Mer än { $limit } matchning + *[other] Fler än { $limit } matchningar + } +pdfjs-find-not-found = Frasen hittades inte + +## Predefined zoom values + +pdfjs-page-scale-width = Sidbredd +pdfjs-page-scale-fit = Anpassa sida +pdfjs-page-scale-auto = Automatisk zoom +pdfjs-page-scale-actual = Verklig storlek +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Sida { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ett fel uppstod vid laddning av PDF-filen. +pdfjs-invalid-file-error = Ogiltig eller korrupt PDF-fil. +pdfjs-missing-file-error = Saknad PDF-fil. +pdfjs-unexpected-response-error = Oväntat svar från servern. +pdfjs-rendering-error = Ett fel uppstod vid visning av sidan. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-annotering] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Skriv in lösenordet för att öppna PDF-filen. +pdfjs-password-invalid = Ogiltigt lösenord. Försök igen. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Avbryt +pdfjs-web-fonts-disabled = Webbtypsnitt är inaktiverade: kan inte använda inbäddade PDF-typsnitt. + +## Editing + +pdfjs-editor-free-text-button = + .title = Text +pdfjs-editor-free-text-button-label = Text +pdfjs-editor-ink-button = + .title = Rita +pdfjs-editor-ink-button-label = Rita +pdfjs-editor-stamp-button = + .title = Lägg till eller redigera bilder +pdfjs-editor-stamp-button-label = Lägg till eller redigera bilder +pdfjs-editor-highlight-button = + .title = Markera +pdfjs-editor-highlight-button-label = Markera +pdfjs-highlight-floating-button1 = + .title = Markera + .aria-label = Markera +pdfjs-highlight-floating-button-label = Markera + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Ta bort ritning +pdfjs-editor-remove-freetext-button = + .title = Ta bort text +pdfjs-editor-remove-stamp-button = + .title = Ta bort bild +pdfjs-editor-remove-highlight-button = + .title = Ta bort markering + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Färg +pdfjs-editor-free-text-size-input = Storlek +pdfjs-editor-ink-color-input = Färg +pdfjs-editor-ink-thickness-input = Tjocklek +pdfjs-editor-ink-opacity-input = Opacitet +pdfjs-editor-stamp-add-image-button = + .title = Lägg till bild +pdfjs-editor-stamp-add-image-button-label = Lägg till bild +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Tjocklek +pdfjs-editor-free-highlight-thickness-title = + .title = Ändra tjocklek när du markerar andra objekt än text +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Textredigerare + .default-content = Börja skriva… +pdfjs-free-text = + .aria-label = Textredigerare +pdfjs-free-text-default-content = Börja skriva… +pdfjs-ink = + .aria-label = Ritredigerare +pdfjs-ink-canvas = + .aria-label = Användarskapad bild + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternativ text +pdfjs-editor-alt-text-edit-button = + .aria-label = Redigera alternativ text +pdfjs-editor-alt-text-edit-button-label = Redigera alternativ text +pdfjs-editor-alt-text-dialog-label = Välj ett alternativ +pdfjs-editor-alt-text-dialog-description = Alt text (alternativ text) hjälper till när människor inte kan se bilden eller när den inte laddas. +pdfjs-editor-alt-text-add-description-label = Lägg till en beskrivning +pdfjs-editor-alt-text-add-description-description = Sikta på 1-2 meningar som beskriver ämnet, miljön eller handlingen. +pdfjs-editor-alt-text-mark-decorative-label = Markera som dekorativ +pdfjs-editor-alt-text-mark-decorative-description = Detta används för dekorativa bilder, som kanter eller vattenstämplar. +pdfjs-editor-alt-text-cancel-button = Avbryt +pdfjs-editor-alt-text-save-button = Spara +pdfjs-editor-alt-text-decorative-tooltip = Märkt som dekorativ +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Till exempel, "En ung man sätter sig vid ett bord för att äta en måltid" +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternativ text + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Det övre vänstra hörnet — ändra storlek +pdfjs-editor-resizer-label-top-middle = Överst i mitten — ändra storlek +pdfjs-editor-resizer-label-top-right = Det övre högra hörnet — ändra storlek +pdfjs-editor-resizer-label-middle-right = Mitten höger — ändra storlek +pdfjs-editor-resizer-label-bottom-right = Nedre högra hörnet — ändra storlek +pdfjs-editor-resizer-label-bottom-middle = Nedre mitten — ändra storlek +pdfjs-editor-resizer-label-bottom-left = Nedre vänstra hörnet — ändra storlek +pdfjs-editor-resizer-label-middle-left = Mitten till vänster — ändra storlek +pdfjs-editor-resizer-top-left = + .aria-label = Det övre vänstra hörnet — ändra storlek +pdfjs-editor-resizer-top-middle = + .aria-label = Överst i mitten — ändra storlek +pdfjs-editor-resizer-top-right = + .aria-label = Det övre högra hörnet — ändra storlek +pdfjs-editor-resizer-middle-right = + .aria-label = Mitten höger — ändra storlek +pdfjs-editor-resizer-bottom-right = + .aria-label = Nedre högra hörnet — ändra storlek +pdfjs-editor-resizer-bottom-middle = + .aria-label = Nedre mitten — ändra storlek +pdfjs-editor-resizer-bottom-left = + .aria-label = Nedre vänstra hörnet — ändra storlek +pdfjs-editor-resizer-middle-left = + .aria-label = Mitten till vänster — ändra storlek + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Markeringsfärg +pdfjs-editor-colorpicker-button = + .title = Ändra färg +pdfjs-editor-colorpicker-dropdown = + .aria-label = Färgval +pdfjs-editor-colorpicker-yellow = + .title = Gul +pdfjs-editor-colorpicker-green = + .title = Grön +pdfjs-editor-colorpicker-blue = + .title = Blå +pdfjs-editor-colorpicker-pink = + .title = Rosa +pdfjs-editor-colorpicker-red = + .title = Röd + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Visa alla +pdfjs-editor-highlight-show-all-button = + .title = Visa alla + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Redigera alternativ text (bildbeskrivning) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Lägg till alternativ text (bildbeskrivning) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Skriv din beskrivning här… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Kort beskrivning för personer som inte kan se bilden eller när bilden inte laddas. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Denna alternativa text skapades automatiskt och kan vara felaktig. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Läs mer +pdfjs-editor-new-alt-text-create-automatically-button-label = Skapa alternativ text automatiskt +pdfjs-editor-new-alt-text-not-now-button = Inte nu +pdfjs-editor-new-alt-text-error-title = Det gick inte att skapa alternativ text automatiskt +pdfjs-editor-new-alt-text-error-description = Skriv din egna alternativa text eller försök igen senare. +pdfjs-editor-new-alt-text-error-close-button = Stäng +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Hämtar AI-modell med alternativ text ({ $downloadedSize } av { $totalSize } MB) + .aria-valuetext = Hämtar AI-modell med alternativ text ({ $downloadedSize } av { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternativ text tillagd +pdfjs-editor-new-alt-text-added-button-label = Alternativ text tillagd +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Saknar alternativ text +pdfjs-editor-new-alt-text-missing-button-label = Saknar alternativ text +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Granska alternativ text +pdfjs-editor-new-alt-text-to-review-button-label = Granska alternativ text +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Skapas automatiskt: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Alternativ textinställningar för bild +pdfjs-image-alt-text-settings-button-label = Alternativ textinställningar för bild +pdfjs-editor-alt-text-settings-dialog-label = Alternativ textinställningar för bild +pdfjs-editor-alt-text-settings-automatic-title = Automatisk alternativ text +pdfjs-editor-alt-text-settings-create-model-button-label = Skapa alternativ text automatiskt +pdfjs-editor-alt-text-settings-create-model-description = Föreslår beskrivningar för att hjälpa personer som inte kan se bilden eller när bilden inte laddas. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = AI-modell för alternativ text ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Körs lokalt på din enhet så att din data förblir privat. Krävs för automatisk alternativ text. +pdfjs-editor-alt-text-settings-delete-model-button = Ta bort +pdfjs-editor-alt-text-settings-download-model-button = Hämta +pdfjs-editor-alt-text-settings-downloading-model-button = Hämtar… +pdfjs-editor-alt-text-settings-editor-title = Alternativ textredigerare +pdfjs-editor-alt-text-settings-show-dialog-button-label = Visa alternativ textredigerare direkt när du lägger till en bild +pdfjs-editor-alt-text-settings-show-dialog-description = Hjälper dig att se till att alla dina bilder har alternativ text. +pdfjs-editor-alt-text-settings-close-button = Stäng + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Markering borttagen +pdfjs-editor-undo-bar-message-freetext = Text borttagen +pdfjs-editor-undo-bar-message-ink = Ritning borttagen +pdfjs-editor-undo-bar-message-stamp = Bild borttagen +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } anteckning har tagits bort + *[other] { $count } anteckningar har tagits bort + } +pdfjs-editor-undo-bar-undo-button = + .title = Ångra +pdfjs-editor-undo-bar-undo-button-label = Ångra +pdfjs-editor-undo-bar-close-button = + .title = Stäng +pdfjs-editor-undo-bar-close-button-label = Stäng diff --git a/public/assets/pdfjs/locale/szl/viewer.ftl b/public/assets/pdfjs/locale/szl/viewer.ftl new file mode 100644 index 0000000..cbf166e --- /dev/null +++ b/public/assets/pdfjs/locale/szl/viewer.ftl @@ -0,0 +1,257 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Piyrwyjszo strōna +pdfjs-previous-button-label = Piyrwyjszo +pdfjs-next-button = + .title = Nastympno strōna +pdfjs-next-button-label = Dalij +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Strōna +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = ze { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } ze { $pagesCount }) +pdfjs-zoom-out-button = + .title = Zmyńsz +pdfjs-zoom-out-button-label = Zmyńsz +pdfjs-zoom-in-button = + .title = Zwiynksz +pdfjs-zoom-in-button-label = Zwiynksz +pdfjs-zoom-select = + .title = Srogość +pdfjs-presentation-mode-button = + .title = Przełōncz na tryb prezyntacyje +pdfjs-presentation-mode-button-label = Tryb prezyntacyje +pdfjs-open-file-button = + .title = Ôdewrzij zbiōr +pdfjs-open-file-button-label = Ôdewrzij +pdfjs-print-button = + .title = Durkuj +pdfjs-print-button-label = Durkuj + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Noczynia +pdfjs-tools-button-label = Noczynia +pdfjs-first-page-button = + .title = Idź ku piyrszyj strōnie +pdfjs-first-page-button-label = Idź ku piyrszyj strōnie +pdfjs-last-page-button = + .title = Idź ku ôstatnij strōnie +pdfjs-last-page-button-label = Idź ku ôstatnij strōnie +pdfjs-page-rotate-cw-button = + .title = Zwyrtnij w prawo +pdfjs-page-rotate-cw-button-label = Zwyrtnij w prawo +pdfjs-page-rotate-ccw-button = + .title = Zwyrtnij w lewo +pdfjs-page-rotate-ccw-button-label = Zwyrtnij w lewo +pdfjs-cursor-text-select-tool-button = + .title = Załōncz noczynie ôbiyranio tekstu +pdfjs-cursor-text-select-tool-button-label = Noczynie ôbiyranio tekstu +pdfjs-cursor-hand-tool-button = + .title = Załōncz noczynie rōnczka +pdfjs-cursor-hand-tool-button-label = Noczynie rōnczka +pdfjs-scroll-vertical-button = + .title = Używej piōnowego przewijanio +pdfjs-scroll-vertical-button-label = Piōnowe przewijanie +pdfjs-scroll-horizontal-button = + .title = Używej poziōmego przewijanio +pdfjs-scroll-horizontal-button-label = Poziōme przewijanie +pdfjs-scroll-wrapped-button = + .title = Używej szichtowego przewijanio +pdfjs-scroll-wrapped-button-label = Szichtowe przewijanie +pdfjs-spread-none-button = + .title = Niy dowej strōn w widoku po dwie +pdfjs-spread-none-button-label = Po jednyj strōnie +pdfjs-spread-odd-button = + .title = Pokoż strōny po dwie; niyporziste po lewyj +pdfjs-spread-odd-button-label = Niyporziste po lewyj +pdfjs-spread-even-button = + .title = Pokoż strōny po dwie; porziste po lewyj +pdfjs-spread-even-button-label = Porziste po lewyj + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Włosności dokumyntu… +pdfjs-document-properties-button-label = Włosności dokumyntu… +pdfjs-document-properties-file-name = Miano zbioru: +pdfjs-document-properties-file-size = Srogość zbioru: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } B) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } B) +pdfjs-document-properties-title = Tytuł: +pdfjs-document-properties-author = Autōr: +pdfjs-document-properties-subject = Tymat: +pdfjs-document-properties-keywords = Kluczowe słowa: +pdfjs-document-properties-creation-date = Data zrychtowanio: +pdfjs-document-properties-modification-date = Data zmiany: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Zrychtowane ôd: +pdfjs-document-properties-producer = PDF ôd: +pdfjs-document-properties-version = Wersyjo PDF: +pdfjs-document-properties-page-count = Wielość strōn: +pdfjs-document-properties-page-size = Srogość strōny: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = piōnowo +pdfjs-document-properties-page-size-orientation-landscape = poziōmo +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Gibki necowy podglōnd: +pdfjs-document-properties-linearized-yes = Ja +pdfjs-document-properties-linearized-no = Niy +pdfjs-document-properties-close-button = Zawrzij + +## Print + +pdfjs-print-progress-message = Rychtowanie dokumyntu do durku… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Pociep +pdfjs-printing-not-supported = Pozōr: Ta przeglōndarka niy cołkiym ôbsuguje durk. +pdfjs-printing-not-ready = Pozōr: Tyn PDF niy ma za tela zaladowany do durku. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Przełōncz posek na rancie +pdfjs-toggle-sidebar-notification-button = + .title = Przełōncz posek na rancie (dokumynt mo struktura/przidowki/warstwy) +pdfjs-toggle-sidebar-button-label = Przełōncz posek na rancie +pdfjs-document-outline-button = + .title = Pokoż struktura dokumyntu (tuplowane klikniyncie rozszyrzo/swijo wszyskie elymynta) +pdfjs-document-outline-button-label = Struktura dokumyntu +pdfjs-attachments-button = + .title = Pokoż przidowki +pdfjs-attachments-button-label = Przidowki +pdfjs-layers-button = + .title = Pokoż warstwy (tuplowane klikniyncie resetuje wszyskie warstwy do bazowego stanu) +pdfjs-layers-button-label = Warstwy +pdfjs-thumbs-button = + .title = Pokoż miniatury +pdfjs-thumbs-button-label = Miniatury +pdfjs-findbar-button = + .title = Znojdź w dokumyncie +pdfjs-findbar-button-label = Znojdź +pdfjs-additional-layers = Nadbytnie warstwy + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Strōna { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Miniatura strōny { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Znojdź + .placeholder = Znojdź w dokumyncie… +pdfjs-find-previous-button = + .title = Znojdź piyrwyjsze pokozanie sie tyj frazy +pdfjs-find-previous-button-label = Piyrwyjszo +pdfjs-find-next-button = + .title = Znojdź nastympne pokozanie sie tyj frazy +pdfjs-find-next-button-label = Dalij +pdfjs-find-highlight-checkbox = Zaznacz wszysko +pdfjs-find-match-case-checkbox-label = Poznowej srogość liter +pdfjs-find-entire-word-checkbox-label = Cołke słowa +pdfjs-find-reached-top = Doszło do samego wiyrchu strōny, dalij ôd spodku +pdfjs-find-reached-bottom = Doszło do samego spodku strōny, dalij ôd wiyrchu +pdfjs-find-not-found = Fraza niy znaleziōno + +## Predefined zoom values + +pdfjs-page-scale-width = Szyrzka strōny +pdfjs-page-scale-fit = Napasowanie strōny +pdfjs-page-scale-auto = Autōmatyczno srogość +pdfjs-page-scale-actual = Aktualno srogość +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Przi ladowaniu PDFa pokozoł sie feler. +pdfjs-invalid-file-error = Zły abo felerny zbiōr PDF. +pdfjs-missing-file-error = Chybio zbioru PDF. +pdfjs-unexpected-response-error = Niyôczekowano ôdpowiydź serwera. +pdfjs-rendering-error = Przi renderowaniu strōny pokozoł sie feler. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Anotacyjo typu { $type }] + +## Password + +pdfjs-password-label = Wkludź hasło, coby ôdewrzić tyn zbiōr PDF. +pdfjs-password-invalid = Hasło je złe. Sprōbuj jeszcze roz. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Pociep +pdfjs-web-fonts-disabled = Necowe fōnty sōm zastawiōne: niy idzie użyć wkludzōnych fōntōw PDF. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/ta/viewer.ftl b/public/assets/pdfjs/locale/ta/viewer.ftl new file mode 100644 index 0000000..82cf197 --- /dev/null +++ b/public/assets/pdfjs/locale/ta/viewer.ftl @@ -0,0 +1,223 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = முந்தைய பக்கம் +pdfjs-previous-button-label = முந்தையது +pdfjs-next-button = + .title = அடுத்த பக்கம் +pdfjs-next-button-label = அடுத்து +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = பக்கம் +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } இல் +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = { $pagesCount }) இல் ({ $pageNumber } +pdfjs-zoom-out-button = + .title = சிறிதாக்கு +pdfjs-zoom-out-button-label = சிறிதாக்கு +pdfjs-zoom-in-button = + .title = பெரிதாக்கு +pdfjs-zoom-in-button-label = பெரிதாக்கு +pdfjs-zoom-select = + .title = பெரிதாக்கு +pdfjs-presentation-mode-button = + .title = விளக்ககாட்சி பயன்முறைக்கு மாறு +pdfjs-presentation-mode-button-label = விளக்ககாட்சி பயன்முறை +pdfjs-open-file-button = + .title = கோப்பினை திற +pdfjs-open-file-button-label = திற +pdfjs-print-button = + .title = அச்சிடு +pdfjs-print-button-label = அச்சிடு + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = கருவிகள் +pdfjs-tools-button-label = கருவிகள் +pdfjs-first-page-button = + .title = முதல் பக்கத்திற்கு செல்லவும் +pdfjs-first-page-button-label = முதல் பக்கத்திற்கு செல்லவும் +pdfjs-last-page-button = + .title = கடைசி பக்கத்திற்கு செல்லவும் +pdfjs-last-page-button-label = கடைசி பக்கத்திற்கு செல்லவும் +pdfjs-page-rotate-cw-button = + .title = வலஞ்சுழியாக சுழற்று +pdfjs-page-rotate-cw-button-label = வலஞ்சுழியாக சுழற்று +pdfjs-page-rotate-ccw-button = + .title = இடஞ்சுழியாக சுழற்று +pdfjs-page-rotate-ccw-button-label = இடஞ்சுழியாக சுழற்று +pdfjs-cursor-text-select-tool-button = + .title = உரைத் தெரிவு கருவியைச் செயல்படுத்து +pdfjs-cursor-text-select-tool-button-label = உரைத் தெரிவு கருவி +pdfjs-cursor-hand-tool-button = + .title = கைக் கருவிக்ச் செயற்படுத்து +pdfjs-cursor-hand-tool-button-label = கைக்குருவி + +## Document properties dialog + +pdfjs-document-properties-button = + .title = ஆவண பண்புகள்... +pdfjs-document-properties-button-label = ஆவண பண்புகள்... +pdfjs-document-properties-file-name = கோப்பு பெயர்: +pdfjs-document-properties-file-size = கோப்பின் அளவு: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } கிபை ({ $size_b } பைட்டுகள்) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } மெபை ({ $size_b } பைட்டுகள்) +pdfjs-document-properties-title = தலைப்பு: +pdfjs-document-properties-author = எழுதியவர் +pdfjs-document-properties-subject = பொருள்: +pdfjs-document-properties-keywords = முக்கிய வார்த்தைகள்: +pdfjs-document-properties-creation-date = படைத்த தேதி : +pdfjs-document-properties-modification-date = திருத்திய தேதி: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = உருவாக்குபவர்: +pdfjs-document-properties-producer = பிடிஎஃப் தயாரிப்பாளர்: +pdfjs-document-properties-version = PDF பதிப்பு: +pdfjs-document-properties-page-count = பக்க எண்ணிக்கை: +pdfjs-document-properties-page-size = பக்க அளவு: +pdfjs-document-properties-page-size-unit-inches = இதில் +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = நிலைபதிப்பு +pdfjs-document-properties-page-size-orientation-landscape = நிலைபரப்பு +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = கடிதம் +pdfjs-document-properties-page-size-name-legal = சட்டபூர்வ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-close-button = மூடுக + +## Print + +pdfjs-print-progress-message = அச்சிடுவதற்கான ஆவணம் தயாராகிறது... +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ரத்து +pdfjs-printing-not-supported = எச்சரிக்கை: இந்த உலாவி அச்சிடுதலை முழுமையாக ஆதரிக்கவில்லை. +pdfjs-printing-not-ready = எச்சரிக்கை: PDF அச்சிட முழுவதுமாக ஏற்றப்படவில்லை. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = பக்கப் பட்டியை நிலைமாற்று +pdfjs-toggle-sidebar-button-label = பக்கப் பட்டியை நிலைமாற்று +pdfjs-document-outline-button = + .title = ஆவண அடக்கத்தைக் காட்டு (இருமுறைச் சொடுக்கி அனைத்து உறுப்பிடிகளையும் விரி/சேர்) +pdfjs-document-outline-button-label = ஆவண வெளிவரை +pdfjs-attachments-button = + .title = இணைப்புகளை காண்பி +pdfjs-attachments-button-label = இணைப்புகள் +pdfjs-thumbs-button = + .title = சிறுபடங்களைக் காண்பி +pdfjs-thumbs-button-label = சிறுபடங்கள் +pdfjs-findbar-button = + .title = ஆவணத்தில் கண்டறி +pdfjs-findbar-button-label = தேடு + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = பக்கம் { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = பக்கத்தின் சிறுபடம் { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = கண்டுபிடி + .placeholder = ஆவணத்தில் கண்டறி… +pdfjs-find-previous-button = + .title = இந்த சொற்றொடரின் முந்தைய நிகழ்வை தேடு +pdfjs-find-previous-button-label = முந்தையது +pdfjs-find-next-button = + .title = இந்த சொற்றொடரின் அடுத்த நிகழ்வை தேடு +pdfjs-find-next-button-label = அடுத்து +pdfjs-find-highlight-checkbox = அனைத்தையும் தனிப்படுத்து +pdfjs-find-match-case-checkbox-label = பேரெழுத்தாக்கத்தை உணர் +pdfjs-find-reached-top = ஆவணத்தின் மேல் பகுதியை அடைந்தது, அடிப்பக்கத்திலிருந்து தொடர்ந்தது +pdfjs-find-reached-bottom = ஆவணத்தின் முடிவை அடைந்தது, மேலிருந்து தொடர்ந்தது +pdfjs-find-not-found = சொற்றொடர் காணவில்லை + +## Predefined zoom values + +pdfjs-page-scale-width = பக்க அகலம் +pdfjs-page-scale-fit = பக்கப் பொருத்தம் +pdfjs-page-scale-auto = தானியக்க பெரிதாக்கல் +pdfjs-page-scale-actual = உண்மையான அளவு +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF ஐ ஏற்றும் போது ஒரு பிழை ஏற்பட்டது. +pdfjs-invalid-file-error = செல்லுபடியாகாத அல்லது சிதைந்த PDF கோப்பு. +pdfjs-missing-file-error = PDF கோப்பு காணவில்லை. +pdfjs-unexpected-response-error = சேவகன் பதில் எதிர்பாரதது. +pdfjs-rendering-error = இந்தப் பக்கத்தை காட்சிப்படுத்தும் போது ஒரு பிழை ஏற்பட்டது. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } விளக்கம்] + +## Password + +pdfjs-password-label = இந்த PDF கோப்பை திறக்க கடவுச்சொல்லை உள்ளிடவும். +pdfjs-password-invalid = செல்லுபடியாகாத கடவுச்சொல், தயை செய்து மீண்டும் முயற்சி செய்க. +pdfjs-password-ok-button = சரி +pdfjs-password-cancel-button = ரத்து +pdfjs-web-fonts-disabled = வலை எழுத்துருக்கள் முடக்கப்பட்டுள்ளன: உட்பொதிக்கப்பட்ட PDF எழுத்துருக்களைப் பயன்படுத்த முடியவில்லை. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/te/viewer.ftl b/public/assets/pdfjs/locale/te/viewer.ftl new file mode 100644 index 0000000..94dc2b8 --- /dev/null +++ b/public/assets/pdfjs/locale/te/viewer.ftl @@ -0,0 +1,239 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = మునుపటి పేజీ +pdfjs-previous-button-label = క్రితం +pdfjs-next-button = + .title = తరువాత పేజీ +pdfjs-next-button-label = తరువాత +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = పేజీ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = మొత్తం { $pagesCount } లో +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = (మొత్తం { $pagesCount } లో { $pageNumber }వది) +pdfjs-zoom-out-button = + .title = జూమ్ తగ్గించు +pdfjs-zoom-out-button-label = జూమ్ తగ్గించు +pdfjs-zoom-in-button = + .title = జూమ్ చేయి +pdfjs-zoom-in-button-label = జూమ్ చేయి +pdfjs-zoom-select = + .title = జూమ్ +pdfjs-presentation-mode-button = + .title = ప్రదర్శనా రీతికి మారు +pdfjs-presentation-mode-button-label = ప్రదర్శనా రీతి +pdfjs-open-file-button = + .title = ఫైల్ తెరువు +pdfjs-open-file-button-label = తెరువు +pdfjs-print-button = + .title = ముద్రించు +pdfjs-print-button-label = ముద్రించు + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = పనిముట్లు +pdfjs-tools-button-label = పనిముట్లు +pdfjs-first-page-button = + .title = మొదటి పేజీకి వెళ్ళు +pdfjs-first-page-button-label = మొదటి పేజీకి వెళ్ళు +pdfjs-last-page-button = + .title = చివరి పేజీకి వెళ్ళు +pdfjs-last-page-button-label = చివరి పేజీకి వెళ్ళు +pdfjs-page-rotate-cw-button = + .title = సవ్యదిశలో తిప్పు +pdfjs-page-rotate-cw-button-label = సవ్యదిశలో తిప్పు +pdfjs-page-rotate-ccw-button = + .title = అపసవ్యదిశలో తిప్పు +pdfjs-page-rotate-ccw-button-label = అపసవ్యదిశలో తిప్పు +pdfjs-cursor-text-select-tool-button = + .title = టెక్స్ట్ ఎంపిక సాధనాన్ని ప్రారంభించండి +pdfjs-cursor-text-select-tool-button-label = టెక్స్ట్ ఎంపిక సాధనం +pdfjs-cursor-hand-tool-button = + .title = చేతి సాధనం చేతనించు +pdfjs-cursor-hand-tool-button-label = చేతి సాధనం +pdfjs-scroll-vertical-button-label = నిలువు స్క్రోలింగు + +## Document properties dialog + +pdfjs-document-properties-button = + .title = పత్రము లక్షణాలు... +pdfjs-document-properties-button-label = పత్రము లక్షణాలు... +pdfjs-document-properties-file-name = దస్త్రం పేరు: +pdfjs-document-properties-file-size = దస్త్రం పరిమాణం: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = శీర్షిక: +pdfjs-document-properties-author = మూలకర్త: +pdfjs-document-properties-subject = విషయం: +pdfjs-document-properties-keywords = కీ పదాలు: +pdfjs-document-properties-creation-date = సృష్టించిన తేదీ: +pdfjs-document-properties-modification-date = సవరించిన తేదీ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = సృష్టికర్త: +pdfjs-document-properties-producer = PDF ఉత్పాదకి: +pdfjs-document-properties-version = PDF వర్షన్: +pdfjs-document-properties-page-count = పేజీల సంఖ్య: +pdfjs-document-properties-page-size = కాగితం పరిమాణం: +pdfjs-document-properties-page-size-unit-inches = లో +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = నిలువుచిత్రం +pdfjs-document-properties-page-size-orientation-landscape = అడ్డచిత్రం +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = లేఖ +pdfjs-document-properties-page-size-name-legal = చట్టపరమైన + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +pdfjs-document-properties-linearized-yes = అవును +pdfjs-document-properties-linearized-no = కాదు +pdfjs-document-properties-close-button = మూసివేయి + +## Print + +pdfjs-print-progress-message = ముద్రించడానికి పత్రము సిద్ధమవుతున్నది… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = రద్దుచేయి +pdfjs-printing-not-supported = హెచ్చరిక: ఈ విహారిణి చేత ముద్రణ పూర్తిగా తోడ్పాటు లేదు. +pdfjs-printing-not-ready = హెచ్చరిక: ముద్రణ కొరకు ఈ PDF పూర్తిగా లోడవలేదు. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = పక్కపట్టీ మార్చు +pdfjs-toggle-sidebar-button-label = పక్కపట్టీ మార్చు +pdfjs-document-outline-button = + .title = పత్రము రూపము చూపించు (డబుల్ క్లిక్ చేసి అన్ని అంశాలను విస్తరించు/కూల్చు) +pdfjs-document-outline-button-label = పత్రము అవుట్‌లైన్ +pdfjs-attachments-button = + .title = అనుబంధాలు చూపు +pdfjs-attachments-button-label = అనుబంధాలు +pdfjs-layers-button-label = పొరలు +pdfjs-thumbs-button = + .title = థంబ్‌నైల్స్ చూపు +pdfjs-thumbs-button-label = థంబ్‌నైల్స్ +pdfjs-findbar-button = + .title = పత్రములో కనుగొనుము +pdfjs-findbar-button-label = కనుగొను +pdfjs-additional-layers = అదనపు పొరలు + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = పేజీ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } పేజీ నఖచిత్రం + +## Find panel button title and messages + +pdfjs-find-input = + .title = కనుగొను + .placeholder = పత్రములో కనుగొను… +pdfjs-find-previous-button = + .title = పదం యొక్క ముందు సంభవాన్ని కనుగొను +pdfjs-find-previous-button-label = మునుపటి +pdfjs-find-next-button = + .title = పదం యొక్క తర్వాతి సంభవాన్ని కనుగొను +pdfjs-find-next-button-label = తరువాత +pdfjs-find-highlight-checkbox = అన్నిటిని ఉద్దీపనం చేయుము +pdfjs-find-match-case-checkbox-label = అక్షరముల తేడాతో పోల్చు +pdfjs-find-entire-word-checkbox-label = పూర్తి పదాలు +pdfjs-find-reached-top = పేజీ పైకి చేరుకున్నది, క్రింది నుండి కొనసాగించండి +pdfjs-find-reached-bottom = పేజీ చివరకు చేరుకున్నది, పైనుండి కొనసాగించండి +pdfjs-find-not-found = పదబంధం కనబడలేదు + +## Predefined zoom values + +pdfjs-page-scale-width = పేజీ వెడల్పు +pdfjs-page-scale-fit = పేజీ అమర్పు +pdfjs-page-scale-auto = స్వయంచాలక జూమ్ +pdfjs-page-scale-actual = యథార్ధ పరిమాణం +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF లోడవుచున్నప్పుడు ఒక దోషం ఎదురైంది. +pdfjs-invalid-file-error = చెల్లని లేదా పాడైన PDF ఫైలు. +pdfjs-missing-file-error = దొరకని PDF ఫైలు. +pdfjs-unexpected-response-error = అనుకోని సర్వర్ స్పందన. +pdfjs-rendering-error = పేజీను రెండర్ చేయుటలో ఒక దోషం ఎదురైంది. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } టీకా] + +## Password + +pdfjs-password-label = ఈ PDF ఫైల్ తెరుచుటకు సంకేతపదం ప్రవేశపెట్టుము. +pdfjs-password-invalid = సంకేతపదం చెల్లదు. దయచేసి మళ్ళీ ప్రయత్నించండి. +pdfjs-password-ok-button = సరే +pdfjs-password-cancel-button = రద్దుచేయి +pdfjs-web-fonts-disabled = వెబ్ ఫాంట్లు అచేతనించబడెను: ఎంబెడెడ్ PDF ఫాంట్లు ఉపయోగించలేక పోయింది. + +## Editing + +# Editor Parameters +pdfjs-editor-free-text-color-input = రంగు +pdfjs-editor-free-text-size-input = పరిమాణం +pdfjs-editor-ink-color-input = రంగు +pdfjs-editor-ink-thickness-input = మందం +pdfjs-editor-ink-opacity-input = అకిరణ్యత + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/tg/viewer.ftl b/public/assets/pdfjs/locale/tg/viewer.ftl new file mode 100644 index 0000000..b39fee5 --- /dev/null +++ b/public/assets/pdfjs/locale/tg/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Саҳифаи қаблӣ +pdfjs-previous-button-label = Қаблӣ +pdfjs-next-button = + .title = Саҳифаи навбатӣ +pdfjs-next-button-label = Навбатӣ +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Саҳифа +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = аз { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } аз { $pagesCount }) +pdfjs-zoom-out-button = + .title = Хурд кардан +pdfjs-zoom-out-button-label = Хурд кардан +pdfjs-zoom-in-button = + .title = Калон кардан +pdfjs-zoom-in-button-label = Калон кардан +pdfjs-zoom-select = + .title = Танзими андоза +pdfjs-presentation-mode-button = + .title = Гузариш ба реҷаи тақдим +pdfjs-presentation-mode-button-label = Реҷаи тақдим +pdfjs-open-file-button = + .title = Кушодани файл +pdfjs-open-file-button-label = Кушодан +pdfjs-print-button = + .title = Чоп кардан +pdfjs-print-button-label = Чоп кардан +pdfjs-save-button = + .title = Нигоҳ доштан +pdfjs-save-button-label = Нигоҳ доштан +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Боргирӣ кардан +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Боргирӣ кардан +pdfjs-bookmark-button = + .title = Саҳифаи ҷорӣ (Дидани нишонии URL аз саҳифаи ҷорӣ) +pdfjs-bookmark-button-label = Саҳифаи ҷорӣ + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Абзорҳо +pdfjs-tools-button-label = Абзорҳо +pdfjs-first-page-button = + .title = Ба саҳифаи аввал гузаред +pdfjs-first-page-button-label = Ба саҳифаи аввал гузаред +pdfjs-last-page-button = + .title = Ба саҳифаи охирин гузаред +pdfjs-last-page-button-label = Ба саҳифаи охирин гузаред +pdfjs-page-rotate-cw-button = + .title = Ба самти ҳаракати ақрабаки соат давр задан +pdfjs-page-rotate-cw-button-label = Ба самти ҳаракати ақрабаки соат давр задан +pdfjs-page-rotate-ccw-button = + .title = Ба муқобили самти ҳаракати ақрабаки соат давр задан +pdfjs-page-rotate-ccw-button-label = Ба муқобили самти ҳаракати ақрабаки соат давр задан +pdfjs-cursor-text-select-tool-button = + .title = Фаъол кардани «Абзори интихоби матн» +pdfjs-cursor-text-select-tool-button-label = Абзори интихоби матн +pdfjs-cursor-hand-tool-button = + .title = Фаъол кардани «Абзори даст» +pdfjs-cursor-hand-tool-button-label = Абзори даст +pdfjs-scroll-page-button = + .title = Истифодаи варақзанӣ +pdfjs-scroll-page-button-label = Варақзанӣ +pdfjs-scroll-vertical-button = + .title = Истифодаи варақзании амудӣ +pdfjs-scroll-vertical-button-label = Варақзании амудӣ +pdfjs-scroll-horizontal-button = + .title = Истифодаи варақзании уфуқӣ +pdfjs-scroll-horizontal-button-label = Варақзании уфуқӣ +pdfjs-scroll-wrapped-button = + .title = Истифодаи варақзании миқёсбандӣ +pdfjs-scroll-wrapped-button-label = Варақзании миқёсбандӣ +pdfjs-spread-none-button = + .title = Густариши саҳифаҳо истифода бурда нашавад +pdfjs-spread-none-button-label = Бе густурдани саҳифаҳо +pdfjs-spread-odd-button = + .title = Густариши саҳифаҳо аз саҳифаҳо бо рақамҳои тоқ оғоз карда мешавад +pdfjs-spread-odd-button-label = Саҳифаҳои тоқ аз тарафи чап +pdfjs-spread-even-button = + .title = Густариши саҳифаҳо аз саҳифаҳо бо рақамҳои ҷуфт оғоз карда мешавад +pdfjs-spread-even-button-label = Саҳифаҳои ҷуфт аз тарафи чап + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Хусусиятҳои ҳуҷҷат… +pdfjs-document-properties-button-label = Хусусиятҳои ҳуҷҷат… +pdfjs-document-properties-file-name = Номи файл: +pdfjs-document-properties-file-size = Андозаи файл: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } КБ ({ $b } байт) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байт) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } КБ ({ $size_b } байт) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байт) +pdfjs-document-properties-title = Сарлавҳа: +pdfjs-document-properties-author = Муаллиф: +pdfjs-document-properties-subject = Мавзуъ: +pdfjs-document-properties-keywords = Калимаҳои калидӣ: +pdfjs-document-properties-creation-date = Санаи эҷод: +pdfjs-document-properties-modification-date = Санаи тағйирот: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Эҷодкунанда: +pdfjs-document-properties-producer = Таҳиякунандаи «PDF»: +pdfjs-document-properties-version = Версияи «PDF»: +pdfjs-document-properties-page-count = Шумораи саҳифаҳо: +pdfjs-document-properties-page-size = Андозаи саҳифа: +pdfjs-document-properties-page-size-unit-inches = дюйм +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = амудӣ +pdfjs-document-properties-page-size-orientation-landscape = уфуқӣ +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Мактуб +pdfjs-document-properties-page-size-name-legal = Ҳуқуқӣ + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Намоиши тез дар Интернет: +pdfjs-document-properties-linearized-yes = Ҳа +pdfjs-document-properties-linearized-no = Не +pdfjs-document-properties-close-button = Пӯшидан + +## Print + +pdfjs-print-progress-message = Омодасозии ҳуҷҷат барои чоп… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Бекор кардан +pdfjs-printing-not-supported = Диққат: Чопкунӣ аз тарафи ин браузер ба таври пурра дастгирӣ намешавад. +pdfjs-printing-not-ready = Диққат: Файли «PDF» барои чопкунӣ пурра бор карда нашуд. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Фаъол кардани навори ҷонибӣ +pdfjs-toggle-sidebar-notification-button = + .title = Фаъол кардани навори ҷонибӣ (ҳуҷҷат дорои сохтор/замимаҳо/қабатҳо мебошад) +pdfjs-toggle-sidebar-button-label = Фаъол кардани навори ҷонибӣ +pdfjs-document-outline-button = + .title = Намоиш додани сохтори ҳуҷҷат (барои баркушодан/пеҷондани ҳамаи унсурҳо дубора зер кунед) +pdfjs-document-outline-button-label = Сохтори ҳуҷҷат +pdfjs-attachments-button = + .title = Намоиш додани замимаҳо +pdfjs-attachments-button-label = Замимаҳо +pdfjs-layers-button = + .title = Намоиш додани қабатҳо (барои барқарор кардани ҳамаи қабатҳо ба вазъияти пешфарз дубора зер кунед) +pdfjs-layers-button-label = Қабатҳо +pdfjs-thumbs-button = + .title = Намоиш додани тасвирчаҳо +pdfjs-thumbs-button-label = Тасвирчаҳо +pdfjs-current-outline-item-button = + .title = Ёфтани унсури сохтори ҷорӣ +pdfjs-current-outline-item-button-label = Унсури сохтори ҷорӣ +pdfjs-findbar-button = + .title = Ёфтан дар ҳуҷҷат +pdfjs-findbar-button-label = Ёфтан +pdfjs-additional-layers = Қабатҳои иловагӣ + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Саҳифаи { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Тасвирчаи саҳифаи { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Ёфтан + .placeholder = Ёфтан дар ҳуҷҷат… +pdfjs-find-previous-button = + .title = Ҷустуҷӯи мавриди қаблии ибораи пешниҳодшуда +pdfjs-find-previous-button-label = Қаблӣ +pdfjs-find-next-button = + .title = Ҷустуҷӯи мавриди навбатии ибораи пешниҳодшуда +pdfjs-find-next-button-label = Навбатӣ +pdfjs-find-highlight-checkbox = Ҳамаашро бо ранг ҷудо кардан +pdfjs-find-match-case-checkbox-label = Бо дарназардошти ҳарфҳои хурду калон +pdfjs-find-match-diacritics-checkbox-label = Бо дарназардошти аломатҳои диакритикӣ +pdfjs-find-entire-word-checkbox-label = Калимаҳои пурра +pdfjs-find-reached-top = Ба болои ҳуҷҷат расид, аз поён идома ёфт +pdfjs-find-reached-bottom = Ба поёни ҳуҷҷат расид, аз боло идома ёфт +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } аз { $total } мувофиқат + *[other] { $current } аз { $total } мувофиқат + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Зиёда аз { $limit } мувофиқат + *[other] Зиёда аз { $limit } мувофиқат + } +pdfjs-find-not-found = Ибора ёфт нашуд + +## Predefined zoom values + +pdfjs-page-scale-width = Аз рӯи паҳнои саҳифа +pdfjs-page-scale-fit = Аз рӯи андозаи саҳифа +pdfjs-page-scale-auto = Андозаи худкор +pdfjs-page-scale-actual = Андозаи воқеӣ +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Саҳифаи { $page } + +## Loading indicator messages + +pdfjs-loading-error = Ҳангоми боркунии «PDF» хато ба миён омад. +pdfjs-invalid-file-error = Файли «PDF» нодуруст ё вайроншуда мебошад. +pdfjs-missing-file-error = Файли «PDF» ғоиб аст. +pdfjs-unexpected-response-error = Ҷавоби ногаҳон аз сервер. +pdfjs-rendering-error = Ҳангоми шаклсозии саҳифа хато ба миён омад. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Ҳошиянависӣ - { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Барои кушодани ин файли «PDF» ниҳонвожаро ворид кунед. +pdfjs-password-invalid = Ниҳонвожаи нодуруст. Лутфан, аз нав кӯшиш кунед. +pdfjs-password-ok-button = ХУБ +pdfjs-password-cancel-button = Бекор кардан +pdfjs-web-fonts-disabled = Шрифтҳои интернетӣ ғайрифаъоланд: истифодаи шрифтҳои дарунсохти «PDF» ғайриимкон аст. + +## Editing + +pdfjs-editor-free-text-button = + .title = Матн +pdfjs-editor-free-text-button-label = Матн +pdfjs-editor-ink-button = + .title = Расмкашӣ +pdfjs-editor-ink-button-label = Расмкашӣ +pdfjs-editor-stamp-button = + .title = Илова ё таҳрир кардани тасвирҳо +pdfjs-editor-stamp-button-label = Илова ё таҳрир кардани тасвирҳо +pdfjs-editor-highlight-button = + .title = Ҷудокунӣ +pdfjs-editor-highlight-button-label = Ҷудокунӣ +pdfjs-highlight-floating-button1 = + .title = Ҷудокунӣ + .aria-label = Ҷудокунӣ +pdfjs-highlight-floating-button-label = Ҷудокунӣ + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Тоза кардани нақша +pdfjs-editor-remove-freetext-button = + .title = Тоза кардани матн +pdfjs-editor-remove-stamp-button = + .title = Тоза кардани тасвир +pdfjs-editor-remove-highlight-button = + .title = Тоза кардани ҷудокунӣ + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Ранг +pdfjs-editor-free-text-size-input = Андоза +pdfjs-editor-ink-color-input = Ранг +pdfjs-editor-ink-thickness-input = Ғафсӣ +pdfjs-editor-ink-opacity-input = Шаффофӣ +pdfjs-editor-stamp-add-image-button = + .title = Илова кардани тасвир +pdfjs-editor-stamp-add-image-button-label = Илова кардани тасвир +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Ғафсӣ +pdfjs-editor-free-highlight-thickness-title = + .title = Иваз кардани ғафсӣ ҳангоми ҷудокунии унсурҳо ба ғайр аз матн +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Муҳаррири матн + .default-content = Матнро ворид кунед… +pdfjs-free-text = + .aria-label = Муҳаррири матн +pdfjs-free-text-default-content = Нависед… +pdfjs-ink = + .aria-label = Муҳаррири расмкашӣ +pdfjs-ink-canvas = + .aria-label = Тасвири эҷодкардаи корбар + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Матни иловагӣ +pdfjs-editor-alt-text-edit-button = + .aria-label = Таҳрир кардани матни ивазкунанда +pdfjs-editor-alt-text-edit-button-label = Таҳрир кардани матни иловагӣ +pdfjs-editor-alt-text-dialog-label = Имконеро интихоб намоед +pdfjs-editor-alt-text-dialog-description = Вақте ки одамон тасвирро дида наметавонанд ё вақте ки тасвир бор карда намешавад, матни иловагӣ (Alt text) кумак мерасонад. +pdfjs-editor-alt-text-add-description-label = Илова кардани тавсиф +pdfjs-editor-alt-text-add-description-description = Кӯшиш кунед, ки 1-2 ҷумлаеро нависед, ки ба мавзӯъ, танзим ё амалҳо тавзеҳ медиҳад. +pdfjs-editor-alt-text-mark-decorative-label = Гузоштан ҳамчун матни ороишӣ +pdfjs-editor-alt-text-mark-decorative-description = Ин барои тасвирҳои ороишӣ, ба монанди марзҳо ё аломатҳои обӣ, истифода мешавад. +pdfjs-editor-alt-text-cancel-button = Бекор кардан +pdfjs-editor-alt-text-save-button = Нигоҳ доштан +pdfjs-editor-alt-text-decorative-tooltip = Ҳамчун матни ороишӣ гузошта шуд +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Барои мисол, «Ман забони тоҷикиро дӯст медорам» +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Матни ивазкунанда + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Кунҷи чапи боло — тағйир додани андоза +pdfjs-editor-resizer-label-top-middle = Канори миёнаи боло — тағйир додани андоза +pdfjs-editor-resizer-label-top-right = Кунҷи рости боло — тағйир додани андоза +pdfjs-editor-resizer-label-middle-right = Канори миёнаи рост — тағйир додани андоза +pdfjs-editor-resizer-label-bottom-right = Кунҷи рости поён — тағйир додани андоза +pdfjs-editor-resizer-label-bottom-middle = Канори миёнаи поён — тағйир додани андоза +pdfjs-editor-resizer-label-bottom-left = Кунҷи чапи поён — тағйир додани андоза +pdfjs-editor-resizer-label-middle-left = Канори миёнаи чап — тағйир додани андоза +pdfjs-editor-resizer-top-left = + .aria-label = Кунҷи чапи боло — тағйир додани андоза +pdfjs-editor-resizer-top-middle = + .aria-label = Канори миёнаи боло — тағйир додани андоза +pdfjs-editor-resizer-top-right = + .aria-label = Кунҷи рости боло — тағйир додани андоза +pdfjs-editor-resizer-middle-right = + .aria-label = Канори миёнаи рост — тағйир додани андоза +pdfjs-editor-resizer-bottom-right = + .aria-label = Кунҷи рости поён — тағйир додани андоза +pdfjs-editor-resizer-bottom-middle = + .aria-label = Канори миёнаи поён — тағйир додани андоза +pdfjs-editor-resizer-bottom-left = + .aria-label = Кунҷи чапи поён — тағйир додани андоза +pdfjs-editor-resizer-middle-left = + .aria-label = Канори миёнаи чап — тағйир додани андоза + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Ранги ҷудокунӣ +pdfjs-editor-colorpicker-button = + .title = Иваз кардани ранг +pdfjs-editor-colorpicker-dropdown = + .aria-label = Интихоби ранг +pdfjs-editor-colorpicker-yellow = + .title = Зард +pdfjs-editor-colorpicker-green = + .title = Сабз +pdfjs-editor-colorpicker-blue = + .title = Кабуд +pdfjs-editor-colorpicker-pink = + .title = Гулобӣ +pdfjs-editor-colorpicker-red = + .title = Сурх + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Ҳамаро намоиш додан +pdfjs-editor-highlight-show-all-button = + .title = Ҳамаро намоиш додан + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Таҳрир кардани матни иловагӣ (тафсири тасвир) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Илова кардани матни иловагӣ (тафсири тасвир) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Тафсири худро дар ин ҷо нависед… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Тавсифи мухтасар барои одамоне, ки аксҳоро дида наметавонанд ё вақте ки аксҳо кушода намешаванд. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Ин матни ивазкунанда ба таври худкор сохта шудааст ва шояд нодуруст бошад. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Маълумоти бештар +pdfjs-editor-new-alt-text-create-automatically-button-label = Ба таври худкор эҷод кардани матни иловагӣ +pdfjs-editor-new-alt-text-not-now-button = Ҳоло не +pdfjs-editor-new-alt-text-error-title = Матни иловагӣ ба таври худкор эҷод карда нашуд +pdfjs-editor-new-alt-text-error-description = Лутфан, матни иловагии худро ворид кунед ё баъдтар аз нав кӯшиш кунед. +pdfjs-editor-new-alt-text-error-close-button = Пӯшидан +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Боргирии модели зеҳни сунъӣ (AI) барои матни ивазкунанда ({ $downloadedSize } аз { $totalSize } МБ) + .aria-valuetext = Боргирии модели зеҳни сунъӣ (AI) барои матни ивазкунанда ({ $downloadedSize } аз { $totalSize } МБ) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Матни иловагӣ илова карда шуд +pdfjs-editor-new-alt-text-added-button-label = Матни иловагӣ илова карда шуд +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Матни иловагӣ вуҷуд надорад +pdfjs-editor-new-alt-text-missing-button-label = Матни иловагӣ вуҷуд надорад +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Бознигарӣ кардани матни иловагӣ +pdfjs-editor-new-alt-text-to-review-button-label = Бознигарӣ кардани матни иловагӣ +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Ба таври худкор сохта шудааст: «{ $generatedAltText }» + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Танзимоти матни иловагии тасвир +pdfjs-image-alt-text-settings-button-label = Танзимоти матни иловагии тасвир +pdfjs-editor-alt-text-settings-dialog-label = Танзимоти матни иловагии тасвир +pdfjs-editor-alt-text-settings-automatic-title = Матни иловагии худкор +pdfjs-editor-alt-text-settings-create-model-button-label = Ба таври худкор эҷод кардани матни иловагӣ +pdfjs-editor-alt-text-settings-create-model-description = Ин имкон барои расонидани кумак ба одамоне, ки аксҳоро дида наметавонанд ё вақте ки аксҳо кушода намешаванд, тавсифи аксҳоро пешниҳод мекунад. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Модели зеҳни сунъӣ «AI» барои матни ивазкунанда ({ $totalSize } МБ) +pdfjs-editor-alt-text-settings-ai-model-description = Дар дастгоҳи шумо ба таври маҳаллӣ кор мекунад, бинобар ин махфияти маълумоти шахсии шумо нигоҳ дошта мешавад. Барои матни ивазкунандаи худкор лозим аст. +pdfjs-editor-alt-text-settings-delete-model-button = Нест кардан +pdfjs-editor-alt-text-settings-download-model-button = Боргирӣ кардан +pdfjs-editor-alt-text-settings-downloading-model-button = Дар ҳоли боргирӣ… +pdfjs-editor-alt-text-settings-editor-title = Муҳаррири матни иловагӣ +pdfjs-editor-alt-text-settings-show-dialog-button-label = Дарҳол нишон додани муҳаррири матни ивазкунанда ҳангоми иловакунии тасвир +pdfjs-editor-alt-text-settings-show-dialog-description = Ба шумо кумак мекунад, ки боварӣ ҳосил кунед, ки ҳамаи тасвирҳои шумо дорои матни ивазкунанда мебошанд. +pdfjs-editor-alt-text-settings-close-button = Пӯшидан + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Ҷудосозӣ тоза карда шуд +pdfjs-editor-undo-bar-message-freetext = Матн тоза карда шуд +pdfjs-editor-undo-bar-message-ink = Расм тоза карда шуд +pdfjs-editor-undo-bar-message-stamp = Тасвир тоза карда шуд +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } ҳошиянависӣ тоза карда шуд + *[other] { $count } ҳошиянависӣ тоза карда шуданд + } +pdfjs-editor-undo-bar-undo-button = + .title = Бекор кардан +pdfjs-editor-undo-bar-undo-button-label = Бекор кардан +pdfjs-editor-undo-bar-close-button = + .title = Пӯшидан +pdfjs-editor-undo-bar-close-button-label = Пӯшидан diff --git a/public/assets/pdfjs/locale/th/viewer.ftl b/public/assets/pdfjs/locale/th/viewer.ftl new file mode 100644 index 0000000..cba15f9 --- /dev/null +++ b/public/assets/pdfjs/locale/th/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = หน้าก่อนหน้า +pdfjs-previous-button-label = ก่อนหน้า +pdfjs-next-button = + .title = หน้าถัดไป +pdfjs-next-button-label = ถัดไป +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = หน้า +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = จาก { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } จาก { $pagesCount }) +pdfjs-zoom-out-button = + .title = ซูมออก +pdfjs-zoom-out-button-label = ซูมออก +pdfjs-zoom-in-button = + .title = ซูมเข้า +pdfjs-zoom-in-button-label = ซูมเข้า +pdfjs-zoom-select = + .title = ซูม +pdfjs-presentation-mode-button = + .title = สลับเป็นโหมดการนำเสนอ +pdfjs-presentation-mode-button-label = โหมดการนำเสนอ +pdfjs-open-file-button = + .title = เปิดไฟล์ +pdfjs-open-file-button-label = เปิด +pdfjs-print-button = + .title = พิมพ์ +pdfjs-print-button-label = พิมพ์ +pdfjs-save-button = + .title = บันทึก +pdfjs-save-button-label = บันทึก +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = ดาวน์โหลด +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = ดาวน์โหลด +pdfjs-bookmark-button = + .title = หน้าปัจจุบัน (ดู URL จากหน้าปัจจุบัน) +pdfjs-bookmark-button-label = หน้าปัจจุบัน + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = เครื่องมือ +pdfjs-tools-button-label = เครื่องมือ +pdfjs-first-page-button = + .title = ไปยังหน้าแรก +pdfjs-first-page-button-label = ไปยังหน้าแรก +pdfjs-last-page-button = + .title = ไปยังหน้าสุดท้าย +pdfjs-last-page-button-label = ไปยังหน้าสุดท้าย +pdfjs-page-rotate-cw-button = + .title = หมุนตามเข็มนาฬิกา +pdfjs-page-rotate-cw-button-label = หมุนตามเข็มนาฬิกา +pdfjs-page-rotate-ccw-button = + .title = หมุนทวนเข็มนาฬิกา +pdfjs-page-rotate-ccw-button-label = หมุนทวนเข็มนาฬิกา +pdfjs-cursor-text-select-tool-button = + .title = เปิดใช้งานเครื่องมือการเลือกข้อความ +pdfjs-cursor-text-select-tool-button-label = เครื่องมือการเลือกข้อความ +pdfjs-cursor-hand-tool-button = + .title = เปิดใช้งานเครื่องมือมือ +pdfjs-cursor-hand-tool-button-label = เครื่องมือมือ +pdfjs-scroll-page-button = + .title = ใช้การเลื่อนหน้า +pdfjs-scroll-page-button-label = การเลื่อนหน้า +pdfjs-scroll-vertical-button = + .title = ใช้การเลื่อนแนวตั้ง +pdfjs-scroll-vertical-button-label = การเลื่อนแนวตั้ง +pdfjs-scroll-horizontal-button = + .title = ใช้การเลื่อนแนวนอน +pdfjs-scroll-horizontal-button-label = การเลื่อนแนวนอน +pdfjs-scroll-wrapped-button = + .title = ใช้การเลื่อนแบบคลุม +pdfjs-scroll-wrapped-button-label = เลื่อนแบบคลุม +pdfjs-spread-none-button = + .title = ไม่ต้องรวมการกระจายหน้า +pdfjs-spread-none-button-label = ไม่กระจาย +pdfjs-spread-odd-button = + .title = รวมการกระจายหน้าเริ่มจากหน้าคี่ +pdfjs-spread-odd-button-label = กระจายอย่างเหลือเศษ +pdfjs-spread-even-button = + .title = รวมการกระจายหน้าเริ่มจากหน้าคู่ +pdfjs-spread-even-button-label = กระจายอย่างเท่าเทียม + +## Document properties dialog + +pdfjs-document-properties-button = + .title = คุณสมบัติเอกสาร… +pdfjs-document-properties-button-label = คุณสมบัติเอกสาร… +pdfjs-document-properties-file-name = ชื่อไฟล์: +pdfjs-document-properties-file-size = ขนาดไฟล์: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } ไบต์) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } ไบต์) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } ไบต์) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } ไบต์) +pdfjs-document-properties-title = ชื่อเรื่อง: +pdfjs-document-properties-author = ผู้สร้าง: +pdfjs-document-properties-subject = ชื่อเรื่อง: +pdfjs-document-properties-keywords = คำสำคัญ: +pdfjs-document-properties-creation-date = วันที่สร้าง: +pdfjs-document-properties-modification-date = วันที่แก้ไข: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = ผู้สร้าง: +pdfjs-document-properties-producer = ผู้ผลิต PDF: +pdfjs-document-properties-version = รุ่น PDF: +pdfjs-document-properties-page-count = จำนวนหน้า: +pdfjs-document-properties-page-size = ขนาดหน้า: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = แนวตั้ง +pdfjs-document-properties-page-size-orientation-landscape = แนวนอน +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = จดหมาย +pdfjs-document-properties-page-size-name-legal = ข้อกฎหมาย + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = มุมมองเว็บแบบรวดเร็ว: +pdfjs-document-properties-linearized-yes = ใช่ +pdfjs-document-properties-linearized-no = ไม่ +pdfjs-document-properties-close-button = ปิด + +## Print + +pdfjs-print-progress-message = กำลังเตรียมเอกสารสำหรับการพิมพ์… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = ยกเลิก +pdfjs-printing-not-supported = คำเตือน: เบราว์เซอร์นี้ไม่ได้สนับสนุนการพิมพ์อย่างเต็มที่ +pdfjs-printing-not-ready = คำเตือน: PDF ไม่ได้รับการโหลดอย่างเต็มที่สำหรับการพิมพ์ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = เปิด/ปิดแถบข้าง +pdfjs-toggle-sidebar-notification-button = + .title = เปิด/ปิดแถบข้าง (เอกสารมีเค้าร่าง/ไฟล์แนบ/เลเยอร์) +pdfjs-toggle-sidebar-button-label = เปิด/ปิดแถบข้าง +pdfjs-document-outline-button = + .title = แสดงเค้าร่างเอกสาร (คลิกสองครั้งเพื่อขยาย/ยุบรายการทั้งหมด) +pdfjs-document-outline-button-label = เค้าร่างเอกสาร +pdfjs-attachments-button = + .title = แสดงไฟล์แนบ +pdfjs-attachments-button-label = ไฟล์แนบ +pdfjs-layers-button = + .title = แสดงเลเยอร์ (คลิกสองครั้งเพื่อรีเซ็ตเลเยอร์ทั้งหมดเป็นสถานะเริ่มต้น) +pdfjs-layers-button-label = เลเยอร์ +pdfjs-thumbs-button = + .title = แสดงภาพขนาดย่อ +pdfjs-thumbs-button-label = ภาพขนาดย่อ +pdfjs-current-outline-item-button = + .title = ค้นหารายการเค้าร่างปัจจุบัน +pdfjs-current-outline-item-button-label = รายการเค้าร่างปัจจุบัน +pdfjs-findbar-button = + .title = ค้นหาในเอกสาร +pdfjs-findbar-button-label = ค้นหา +pdfjs-additional-layers = เลเยอร์เพิ่มเติม + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = หน้า { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = ภาพขนาดย่อของหน้า { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ค้นหา + .placeholder = ค้นหาในเอกสาร… +pdfjs-find-previous-button = + .title = หาตำแหน่งก่อนหน้าของวลี +pdfjs-find-previous-button-label = ก่อนหน้า +pdfjs-find-next-button = + .title = หาตำแหน่งถัดไปของวลี +pdfjs-find-next-button-label = ถัดไป +pdfjs-find-highlight-checkbox = เน้นสีทั้งหมด +pdfjs-find-match-case-checkbox-label = ตัวพิมพ์ใหญ่เล็กตรงกัน +pdfjs-find-match-diacritics-checkbox-label = เครื่องหมายกำกับการออกเสียงตรงกัน +pdfjs-find-entire-word-checkbox-label = ทั้งคำ +pdfjs-find-reached-top = ค้นหาถึงจุดเริ่มต้นของหน้า เริ่มค้นต่อจากด้านล่าง +pdfjs-find-reached-bottom = ค้นหาถึงจุดสิ้นสุดหน้า เริ่มค้นต่อจากด้านบน +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = { $current } จาก { $total } รายการที่ตรงกัน +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = มากกว่า { $limit } รายการที่ตรงกัน +pdfjs-find-not-found = ไม่พบวลี + +## Predefined zoom values + +pdfjs-page-scale-width = ความกว้างหน้า +pdfjs-page-scale-fit = พอดีหน้า +pdfjs-page-scale-auto = ซูมอัตโนมัติ +pdfjs-page-scale-actual = ขนาดจริง +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = หน้า { $page } + +## Loading indicator messages + +pdfjs-loading-error = เกิดข้อผิดพลาดขณะโหลด PDF +pdfjs-invalid-file-error = ไฟล์ PDF ไม่ถูกต้องหรือเสียหาย +pdfjs-missing-file-error = ไฟล์ PDF หายไป +pdfjs-unexpected-response-error = การตอบสนองของเซิร์ฟเวอร์ที่ไม่คาดคิด +pdfjs-rendering-error = เกิดข้อผิดพลาดขณะเรนเดอร์หน้า + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [คำอธิบายประกอบ { $type }] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = ป้อนรหัสผ่านเพื่อเปิดไฟล์ PDF นี้ +pdfjs-password-invalid = รหัสผ่านไม่ถูกต้อง โปรดลองอีกครั้ง +pdfjs-password-ok-button = ตกลง +pdfjs-password-cancel-button = ยกเลิก +pdfjs-web-fonts-disabled = แบบอักษรเว็บถูกปิดใช้งาน: ไม่สามารถใช้แบบอักษร PDF ฝังตัว + +## Editing + +pdfjs-editor-free-text-button = + .title = ข้อความ +pdfjs-editor-free-text-button-label = ข้อความ +pdfjs-editor-ink-button = + .title = รูปวาด +pdfjs-editor-ink-button-label = รูปวาด +pdfjs-editor-stamp-button = + .title = เพิ่มหรือแก้ไขภาพ +pdfjs-editor-stamp-button-label = เพิ่มหรือแก้ไขภาพ +pdfjs-editor-highlight-button = + .title = เน้น +pdfjs-editor-highlight-button-label = เน้น +pdfjs-highlight-floating-button1 = + .title = เน้นสี + .aria-label = เน้นสี +pdfjs-highlight-floating-button-label = เน้นสี + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = เอาภาพวาดออก +pdfjs-editor-remove-freetext-button = + .title = เอาข้อความออก +pdfjs-editor-remove-stamp-button = + .title = เอาภาพออก +pdfjs-editor-remove-highlight-button = + .title = เอาการเน้นสีออก + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = สี +pdfjs-editor-free-text-size-input = ขนาด +pdfjs-editor-ink-color-input = สี +pdfjs-editor-ink-thickness-input = ความหนา +pdfjs-editor-ink-opacity-input = ความทึบ +pdfjs-editor-stamp-add-image-button = + .title = เพิ่มภาพ +pdfjs-editor-stamp-add-image-button-label = เพิ่มภาพ +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = ความหนา +pdfjs-editor-free-highlight-thickness-title = + .title = เปลี่ยนความหนาเมื่อเน้นรายการอื่นๆ ที่ไม่ใช่ข้อความ +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = ตัวแก้ไขข้อความ + .default-content = เริ่มพิมพ์ได้เลย… +pdfjs-free-text = + .aria-label = ตัวแก้ไขข้อความ +pdfjs-free-text-default-content = เริ่มพิมพ์… +pdfjs-ink = + .aria-label = ตัวแก้ไขรูปวาด +pdfjs-ink-canvas = + .aria-label = ภาพที่ผู้ใช้สร้างขึ้น + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = ข้อความทดแทน +pdfjs-editor-alt-text-edit-button = + .aria-label = แก้ไขข้อความทดแทน +pdfjs-editor-alt-text-edit-button-label = แก้ไขข้อความทดแทน +pdfjs-editor-alt-text-dialog-label = เลือกตัวเลือก +pdfjs-editor-alt-text-dialog-description = ข้อความทดแทนสามารถช่วยเหลือได้เมื่อผู้ใช้มองไม่เห็นภาพ หรือภาพไม่โหลด +pdfjs-editor-alt-text-add-description-label = เพิ่มคำอธิบาย +pdfjs-editor-alt-text-add-description-description = แนะนำให้ใช้ 1-2 ประโยคซึ่งอธิบายหัวเรื่อง ฉาก หรือการกระทำ +pdfjs-editor-alt-text-mark-decorative-label = ทำเครื่องหมายเป็นสิ่งตกแต่ง +pdfjs-editor-alt-text-mark-decorative-description = สิ่งนี้ใช้สำหรับภาพที่เป็นสิ่งประดับ เช่น ขอบ หรือลายน้ำ +pdfjs-editor-alt-text-cancel-button = ยกเลิก +pdfjs-editor-alt-text-save-button = บันทึก +pdfjs-editor-alt-text-decorative-tooltip = ทำเครื่องหมายเป็นสิ่งตกแต่งแล้ว +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = ตัวอย่างเช่น “ชายหนุ่มคนหนึ่งนั่งลงที่โต๊ะเพื่อรับประทานอาหารมื้อหนึ่ง” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = ข้อความทดแทน + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = มุมซ้ายบน — ปรับขนาด +pdfjs-editor-resizer-label-top-middle = ตรงกลางด้านบน — ปรับขนาด +pdfjs-editor-resizer-label-top-right = มุมขวาบน — ปรับขนาด +pdfjs-editor-resizer-label-middle-right = ตรงกลางด้านขวา — ปรับขนาด +pdfjs-editor-resizer-label-bottom-right = มุมขวาล่าง — ปรับขนาด +pdfjs-editor-resizer-label-bottom-middle = ตรงกลางด้านล่าง — ปรับขนาด +pdfjs-editor-resizer-label-bottom-left = มุมซ้ายล่าง — ปรับขนาด +pdfjs-editor-resizer-label-middle-left = ตรงกลางด้านซ้าย — ปรับขนาด +pdfjs-editor-resizer-top-left = + .aria-label = มุมซ้ายบน — ปรับขนาด +pdfjs-editor-resizer-top-middle = + .aria-label = ตรงกลางด้านบน — ปรับขนาด +pdfjs-editor-resizer-top-right = + .aria-label = มุมขวาบน — ปรับขนาด +pdfjs-editor-resizer-middle-right = + .aria-label = ตรงกลางด้านขวา — ปรับขนาด +pdfjs-editor-resizer-bottom-right = + .aria-label = มุมขวาล่าง — ปรับขนาด +pdfjs-editor-resizer-bottom-middle = + .aria-label = ตรงกลางด้านล่าง — ปรับขนาด +pdfjs-editor-resizer-bottom-left = + .aria-label = มุมซ้ายล่าง — ปรับขนาด +pdfjs-editor-resizer-middle-left = + .aria-label = ตรงกลางด้านซ้าย — ปรับขนาด + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = สีเน้น +pdfjs-editor-colorpicker-button = + .title = เปลี่ยนสี +pdfjs-editor-colorpicker-dropdown = + .aria-label = ทางเลือกสี +pdfjs-editor-colorpicker-yellow = + .title = เหลือง +pdfjs-editor-colorpicker-green = + .title = เขียว +pdfjs-editor-colorpicker-blue = + .title = น้ำเงิน +pdfjs-editor-colorpicker-pink = + .title = ชมพู +pdfjs-editor-colorpicker-red = + .title = แดง + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = แสดงทั้งหมด +pdfjs-editor-highlight-show-all-button = + .title = แสดงทั้งหมด + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = แก้ไขข้อความทดแทน (คำอธิบายภาพ) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = เพิ่มข้อความทดแทน (คำอธิบายภาพ) +pdfjs-editor-new-alt-text-textarea = + .placeholder = เขียนคำอธิบายของคุณที่นี่… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = คำอธิบายสั้นๆ สำหรับผู้ที่ไม่สามารถมองเห็นภาพหรือเมื่อภาพไม่โหลด +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = ข้อความทดแทนนี้ถูกสร้างขึ้นโดยอัตโนมัติและอาจไม่ถูกต้อง +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = เรียนรู้เพิ่มเติม +pdfjs-editor-new-alt-text-create-automatically-button-label = สร้างข้อความทดแทนโดยอัตโนมัติ +pdfjs-editor-new-alt-text-not-now-button = ไม่ใช่ตอนนี้ +pdfjs-editor-new-alt-text-error-title = ไม่สามารถสร้างข้อความทดแทนโดยอัตโนมัติได้ +pdfjs-editor-new-alt-text-error-description = กรุณาเขียนข้อความทดแทนด้วยตัวเองหรือลองใหม่อีกครั้งในภายหลัง +pdfjs-editor-new-alt-text-error-close-button = ปิด +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = กำลังดาวน์โหลดโมเดล AI สำหรับข้อความทดแทน ({ $downloadedSize } จาก { $totalSize } MB) + .aria-valuetext = กำลังดาวน์โหลดโมเดล AI สำหรับข้อความทดแทน ({ $downloadedSize } จาก { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = เพิ่มข้อความทดแทนแล้ว +pdfjs-editor-new-alt-text-added-button-label = เพิ่มข้อความทดแทนแล้ว +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = ขาดข้อความทดแทน +pdfjs-editor-new-alt-text-missing-button-label = ขาดข้อความทดแทน +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = ตรวจสอบข้อความทดแทน +pdfjs-editor-new-alt-text-to-review-button-label = ตรวจสอบข้อความทดแทน +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = สร้างขึ้นโดยอัตโนมัติ: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = ตั้งค่าข้อความทดแทนภาพ +pdfjs-image-alt-text-settings-button-label = ตั้งค่าข้อความทดแทนภาพ +pdfjs-editor-alt-text-settings-dialog-label = ตั้งค่าข้อความทดแทนภาพ +pdfjs-editor-alt-text-settings-automatic-title = การทดแทนด้วยข้อความอัตโนมัติ +pdfjs-editor-alt-text-settings-create-model-button-label = สร้างข้อความทดแทนอัตโนมัติ +pdfjs-editor-alt-text-settings-create-model-description = แนะนำคำอธิบายเพื่อช่วยเหลือผู้ที่ไม่สามารถมองเห็นภาพหรือเมื่อภาพไม่โหลด +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = โมเดล AI สำหรับข้อความทดแทน ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = ทำงานในเครื่องของคุณเพื่อให้ข้อมูลของคุณเป็นส่วนตัว จำเป็นสำหรับข้อความทดแทนอัตโนมัติ +pdfjs-editor-alt-text-settings-delete-model-button = ลบ +pdfjs-editor-alt-text-settings-download-model-button = ดาวน์โหลด +pdfjs-editor-alt-text-settings-downloading-model-button = กำลังดาวน์โหลด… +pdfjs-editor-alt-text-settings-editor-title = ตัวแก้ไขข้อความทดแทน +pdfjs-editor-alt-text-settings-show-dialog-button-label = แสดงตัวแก้ไขข้อความทดแทนทันทีเมื่อเพิ่มภาพ +pdfjs-editor-alt-text-settings-show-dialog-description = ช่วยให้คุณแน่ใจว่าภาพทั้งหมดของคุณมีข้อความทดแทน +pdfjs-editor-alt-text-settings-close-button = ปิด + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = เอาการเน้นสีออกแล้ว +pdfjs-editor-undo-bar-message-freetext = เอาข้อความออกแล้ว +pdfjs-editor-undo-bar-message-ink = เอาภาพวาดออกแล้ว +pdfjs-editor-undo-bar-message-stamp = เอาภาพออกแล้ว +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = เอาคำอธิบายประกอบ { $count } รายการออกแล้ว +pdfjs-editor-undo-bar-undo-button = + .title = เลิกทำ +pdfjs-editor-undo-bar-undo-button-label = เลิกทำ +pdfjs-editor-undo-bar-close-button = + .title = ปิด +pdfjs-editor-undo-bar-close-button-label = ปิด diff --git a/public/assets/pdfjs/locale/tl/viewer.ftl b/public/assets/pdfjs/locale/tl/viewer.ftl new file mode 100644 index 0000000..faa0009 --- /dev/null +++ b/public/assets/pdfjs/locale/tl/viewer.ftl @@ -0,0 +1,257 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Naunang Pahina +pdfjs-previous-button-label = Nakaraan +pdfjs-next-button = + .title = Sunod na Pahina +pdfjs-next-button-label = Sunod +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Pahina +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = ng { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } ng { $pagesCount }) +pdfjs-zoom-out-button = + .title = Paliitin +pdfjs-zoom-out-button-label = Paliitin +pdfjs-zoom-in-button = + .title = Palakihin +pdfjs-zoom-in-button-label = Palakihin +pdfjs-zoom-select = + .title = Mag-zoom +pdfjs-presentation-mode-button = + .title = Lumipat sa Presentation Mode +pdfjs-presentation-mode-button-label = Presentation Mode +pdfjs-open-file-button = + .title = Magbukas ng file +pdfjs-open-file-button-label = Buksan +pdfjs-print-button = + .title = i-Print +pdfjs-print-button-label = i-Print + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Mga Kagamitan +pdfjs-tools-button-label = Mga Kagamitan +pdfjs-first-page-button = + .title = Pumunta sa Unang Pahina +pdfjs-first-page-button-label = Pumunta sa Unang Pahina +pdfjs-last-page-button = + .title = Pumunta sa Huling Pahina +pdfjs-last-page-button-label = Pumunta sa Huling Pahina +pdfjs-page-rotate-cw-button = + .title = Paikutin Pakanan +pdfjs-page-rotate-cw-button-label = Paikutin Pakanan +pdfjs-page-rotate-ccw-button = + .title = Paikutin Pakaliwa +pdfjs-page-rotate-ccw-button-label = Paikutin Pakaliwa +pdfjs-cursor-text-select-tool-button = + .title = I-enable ang Text Selection Tool +pdfjs-cursor-text-select-tool-button-label = Text Selection Tool +pdfjs-cursor-hand-tool-button = + .title = I-enable ang Hand Tool +pdfjs-cursor-hand-tool-button-label = Hand Tool +pdfjs-scroll-vertical-button = + .title = Gumamit ng Vertical Scrolling +pdfjs-scroll-vertical-button-label = Vertical Scrolling +pdfjs-scroll-horizontal-button = + .title = Gumamit ng Horizontal Scrolling +pdfjs-scroll-horizontal-button-label = Horizontal Scrolling +pdfjs-scroll-wrapped-button = + .title = Gumamit ng Wrapped Scrolling +pdfjs-scroll-wrapped-button-label = Wrapped Scrolling +pdfjs-spread-none-button = + .title = Huwag pagsamahin ang mga page spread +pdfjs-spread-none-button-label = No Spreads +pdfjs-spread-odd-button = + .title = Join page spreads starting with odd-numbered pages +pdfjs-spread-odd-button-label = Mga Odd Spread +pdfjs-spread-even-button = + .title = Pagsamahin ang mga page spread na nagsisimula sa mga even-numbered na pahina +pdfjs-spread-even-button-label = Mga Even Spread + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Mga Katangian ng Dokumento… +pdfjs-document-properties-button-label = Mga Katangian ng Dokumento… +pdfjs-document-properties-file-name = File name: +pdfjs-document-properties-file-size = File size: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Pamagat: +pdfjs-document-properties-author = May-akda: +pdfjs-document-properties-subject = Paksa: +pdfjs-document-properties-keywords = Mga keyword: +pdfjs-document-properties-creation-date = Petsa ng Pagkakagawa: +pdfjs-document-properties-modification-date = Petsa ng Pagkakabago: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Tagalikha: +pdfjs-document-properties-producer = PDF Producer: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Bilang ng Pahina: +pdfjs-document-properties-page-size = Laki ng Pahina: +pdfjs-document-properties-page-size-unit-inches = pulgada +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = patayo +pdfjs-document-properties-page-size-orientation-landscape = pahiga +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Fast Web View: +pdfjs-document-properties-linearized-yes = Oo +pdfjs-document-properties-linearized-no = Hindi +pdfjs-document-properties-close-button = Isara + +## Print + +pdfjs-print-progress-message = Inihahanda ang dokumento para sa pag-print… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Kanselahin +pdfjs-printing-not-supported = Babala: Hindi pa ganap na suportado ang pag-print sa browser na ito. +pdfjs-printing-not-ready = Babala: Hindi ganap na nabuksan ang PDF para sa pag-print. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Ipakita/Itago ang Sidebar +pdfjs-toggle-sidebar-notification-button = + .title = Ipakita/Itago ang Sidebar (nagtataglay ang dokumento ng balangkas/mga attachment/mga layer) +pdfjs-toggle-sidebar-button-label = Ipakita/Itago ang Sidebar +pdfjs-document-outline-button = + .title = Ipakita ang Document Outline (mag-double-click para i-expand/collapse ang laman) +pdfjs-document-outline-button-label = Balangkas ng Dokumento +pdfjs-attachments-button = + .title = Ipakita ang mga Attachment +pdfjs-attachments-button-label = Mga attachment +pdfjs-layers-button = + .title = Ipakita ang mga Layer (mag-double click para mareset ang lahat ng layer sa orihinal na estado) +pdfjs-layers-button-label = Mga layer +pdfjs-thumbs-button = + .title = Ipakita ang mga Thumbnail +pdfjs-thumbs-button-label = Mga thumbnail +pdfjs-findbar-button = + .title = Hanapin sa Dokumento +pdfjs-findbar-button-label = Hanapin +pdfjs-additional-layers = Mga Karagdagang Layer + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Pahina { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Thumbnail ng Pahina { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Hanapin + .placeholder = Hanapin sa dokumento… +pdfjs-find-previous-button = + .title = Hanapin ang nakaraang pangyayari ng parirala +pdfjs-find-previous-button-label = Nakaraan +pdfjs-find-next-button = + .title = Hanapin ang susunod na pangyayari ng parirala +pdfjs-find-next-button-label = Susunod +pdfjs-find-highlight-checkbox = I-highlight lahat +pdfjs-find-match-case-checkbox-label = Itugma ang case +pdfjs-find-entire-word-checkbox-label = Buong salita +pdfjs-find-reached-top = Naabot na ang tuktok ng dokumento, ipinagpatuloy mula sa ilalim +pdfjs-find-reached-bottom = Naabot na ang dulo ng dokumento, ipinagpatuloy mula sa tuktok +pdfjs-find-not-found = Hindi natagpuan ang parirala + +## Predefined zoom values + +pdfjs-page-scale-width = Lapad ng Pahina +pdfjs-page-scale-fit = Pagkasyahin ang Pahina +pdfjs-page-scale-auto = Automatic Zoom +pdfjs-page-scale-actual = Totoong sukat +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Nagkaproblema habang niloload ang PDF. +pdfjs-invalid-file-error = Di-wasto o sira ang PDF file. +pdfjs-missing-file-error = Nawawalang PDF file. +pdfjs-unexpected-response-error = Hindi inaasahang tugon ng server. +pdfjs-rendering-error = Nagkaproblema habang nirerender ang pahina. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = Ipasok ang password upang buksan ang PDF file na ito. +pdfjs-password-invalid = Maling password. Subukan uli. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Kanselahin +pdfjs-web-fonts-disabled = Naka-disable ang mga Web font: hindi kayang gamitin ang mga naka-embed na PDF font. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/tr/viewer.ftl b/public/assets/pdfjs/locale/tr/viewer.ftl new file mode 100644 index 0000000..b1b7cbf --- /dev/null +++ b/public/assets/pdfjs/locale/tr/viewer.ftl @@ -0,0 +1,515 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Önceki sayfa +pdfjs-previous-button-label = Önceki +pdfjs-next-button = + .title = Sonraki sayfa +pdfjs-next-button-label = Sonraki +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Sayfa +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = Uzaklaştır +pdfjs-zoom-out-button-label = Uzaklaştır +pdfjs-zoom-in-button = + .title = Yakınlaştır +pdfjs-zoom-in-button-label = Yakınlaştır +pdfjs-zoom-select = + .title = Yakınlaştırma +pdfjs-presentation-mode-button = + .title = Sunum moduna geç +pdfjs-presentation-mode-button-label = Sunum modu +pdfjs-open-file-button = + .title = Dosya aç +pdfjs-open-file-button-label = Aç +pdfjs-print-button = + .title = Yazdır +pdfjs-print-button-label = Yazdır +pdfjs-save-button = + .title = Kaydet +pdfjs-save-button-label = Kaydet +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = İndir +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = İndir +pdfjs-bookmark-button = + .title = Geçerli sayfa (geçerli sayfanın adresini görüntüle) +pdfjs-bookmark-button-label = Geçerli sayfa + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Araçlar +pdfjs-tools-button-label = Araçlar +pdfjs-first-page-button = + .title = İlk sayfaya git +pdfjs-first-page-button-label = İlk sayfaya git +pdfjs-last-page-button = + .title = Son sayfaya git +pdfjs-last-page-button-label = Son sayfaya git +pdfjs-page-rotate-cw-button = + .title = Saat yönünde döndür +pdfjs-page-rotate-cw-button-label = Saat yönünde döndür +pdfjs-page-rotate-ccw-button = + .title = Saat yönünün tersine döndür +pdfjs-page-rotate-ccw-button-label = Saat yönünün tersine döndür +pdfjs-cursor-text-select-tool-button = + .title = Metin seçme aracını etkinleştir +pdfjs-cursor-text-select-tool-button-label = Metin seçme aracı +pdfjs-cursor-hand-tool-button = + .title = El aracını etkinleştir +pdfjs-cursor-hand-tool-button-label = El aracı +pdfjs-scroll-page-button = + .title = Sayfa kaydırmayı kullan +pdfjs-scroll-page-button-label = Sayfa kaydırma +pdfjs-scroll-vertical-button = + .title = Dikey kaydırmayı kullan +pdfjs-scroll-vertical-button-label = Dikey kaydırma +pdfjs-scroll-horizontal-button = + .title = Yatay kaydırmayı kullan +pdfjs-scroll-horizontal-button-label = Yatay kaydırma +pdfjs-scroll-wrapped-button = + .title = Yan yana kaydırmayı kullan +pdfjs-scroll-wrapped-button-label = Yan yana kaydırma +pdfjs-spread-none-button = + .title = Yan yana sayfaları birleştirme +pdfjs-spread-none-button-label = Birleştirme +pdfjs-spread-odd-button = + .title = Yan yana sayfaları tek numaralı sayfalardan başlayarak birleştir +pdfjs-spread-odd-button-label = Tek numaralı +pdfjs-spread-even-button = + .title = Yan yana sayfaları çift numaralı sayfalardan başlayarak birleştir +pdfjs-spread-even-button-label = Çift numaralı + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Belge özellikleri… +pdfjs-document-properties-button-label = Belge özellikleri… +pdfjs-document-properties-file-name = Dosya adı: +pdfjs-document-properties-file-size = Dosya boyutu: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bayt) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bayt) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bayt) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bayt) +pdfjs-document-properties-title = Başlık: +pdfjs-document-properties-author = Yazar: +pdfjs-document-properties-subject = Konu: +pdfjs-document-properties-keywords = Anahtar kelimeler: +pdfjs-document-properties-creation-date = Oluşturma tarihi: +pdfjs-document-properties-modification-date = Değiştirme tarihi: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } { $time } +pdfjs-document-properties-creator = Oluşturan: +pdfjs-document-properties-producer = PDF üreticisi: +pdfjs-document-properties-version = PDF sürümü: +pdfjs-document-properties-page-count = Sayfa sayısı: +pdfjs-document-properties-page-size = Sayfa boyutu: +pdfjs-document-properties-page-size-unit-inches = inç +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = dikey +pdfjs-document-properties-page-size-orientation-landscape = yatay +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Hızlı web görünümü: +pdfjs-document-properties-linearized-yes = Evet +pdfjs-document-properties-linearized-no = Hayır +pdfjs-document-properties-close-button = Kapat + +## Print + +pdfjs-print-progress-message = Belge yazdırılmaya hazırlanıyor… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = %{ $progress } +pdfjs-print-progress-close-button = İptal +pdfjs-printing-not-supported = Uyarı: Yazdırma bu tarayıcı tarafından tam olarak desteklenmemektedir. +pdfjs-printing-not-ready = Uyarı: PDF tamamen yüklenmedi ve yazdırmaya hazır değil. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Kenar çubuğunu aç/kapat +pdfjs-toggle-sidebar-notification-button = + .title = Kenar çubuğunu aç/kapat (Belge ana hat/ekler/katmanlar içeriyor) +pdfjs-toggle-sidebar-button-label = Kenar çubuğunu aç/kapat +pdfjs-document-outline-button = + .title = Belge ana hatlarını göster (Tüm öğeleri genişletmek/daraltmak için çift tıklayın) +pdfjs-document-outline-button-label = Belge ana hatları +pdfjs-attachments-button = + .title = Ekleri göster +pdfjs-attachments-button-label = Ekler +pdfjs-layers-button = + .title = Katmanları göster (tüm katmanları varsayılan duruma sıfırlamak için çift tıklayın) +pdfjs-layers-button-label = Katmanlar +pdfjs-thumbs-button = + .title = Küçük resimleri göster +pdfjs-thumbs-button-label = Küçük resimler +pdfjs-current-outline-item-button = + .title = Mevcut ana hat öğesini bul +pdfjs-current-outline-item-button-label = Mevcut ana hat öğesi +pdfjs-findbar-button = + .title = Belgede bul +pdfjs-findbar-button-label = Bul +pdfjs-additional-layers = Ek katmanlar + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Sayfa { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page }. sayfanın küçük hâli + +## Find panel button title and messages + +pdfjs-find-input = + .title = Bul + .placeholder = Belgede bul… +pdfjs-find-previous-button = + .title = Önceki eşleşmeyi bul +pdfjs-find-previous-button-label = Önceki +pdfjs-find-next-button = + .title = Sonraki eşleşmeyi bul +pdfjs-find-next-button-label = Sonraki +pdfjs-find-highlight-checkbox = Tümünü vurgula +pdfjs-find-match-case-checkbox-label = Büyük-küçük harfe duyarlı +pdfjs-find-match-diacritics-checkbox-label = Fonetik işaretleri bul +pdfjs-find-entire-word-checkbox-label = Tam sözcükler +pdfjs-find-reached-top = Belgenin başına ulaşıldı, sonundan devam edildi +pdfjs-find-reached-bottom = Belgenin sonuna ulaşıldı, başından devam edildi +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $total } eşleşmeden { $current }. eşleşme + *[other] { $total } eşleşmeden { $current }. eşleşme + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] { $limit } eşleşmeden fazla + *[other] { $limit } eşleşmeden fazla + } +pdfjs-find-not-found = Eşleşme bulunamadı + +## Predefined zoom values + +pdfjs-page-scale-width = Sayfa genişliği +pdfjs-page-scale-fit = Sayfayı sığdır +pdfjs-page-scale-auto = Otomatik yakınlaştır +pdfjs-page-scale-actual = Gerçek boyut +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = %{ $scale } + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Sayfa { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF yüklenirken bir hata oluştu. +pdfjs-invalid-file-error = Geçersiz veya bozulmuş PDF dosyası. +pdfjs-missing-file-error = PDF dosyası eksik. +pdfjs-unexpected-response-error = Beklenmeyen sunucu yanıtı. +pdfjs-rendering-error = Sayfa yorumlanırken bir hata oluştu. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } işareti] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Bu PDF dosyasını açmak için parolasını yazın. +pdfjs-password-invalid = Geçersiz parola. Lütfen yeniden deneyin. +pdfjs-password-ok-button = Tamam +pdfjs-password-cancel-button = İptal +pdfjs-web-fonts-disabled = Web fontları devre dışı: Gömülü PDF fontları kullanılamıyor. + +## Editing + +pdfjs-editor-free-text-button = + .title = Metin +pdfjs-editor-free-text-button-label = Metin +pdfjs-editor-ink-button = + .title = Çiz +pdfjs-editor-ink-button-label = Çiz +pdfjs-editor-stamp-button = + .title = Resim ekle veya düzenle +pdfjs-editor-stamp-button-label = Resim ekle veya düzenle +pdfjs-editor-highlight-button = + .title = Vurgula +pdfjs-editor-highlight-button-label = Vurgula +pdfjs-highlight-floating-button1 = + .title = Vurgula + .aria-label = Vurgula +pdfjs-highlight-floating-button-label = Vurgula + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Çizimi kaldır +pdfjs-editor-remove-freetext-button = + .title = Metni kaldır +pdfjs-editor-remove-stamp-button = + .title = Resmi kaldır +pdfjs-editor-remove-highlight-button = + .title = Vurgulamayı kaldır + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Renk +pdfjs-editor-free-text-size-input = Boyut +pdfjs-editor-ink-color-input = Renk +pdfjs-editor-ink-thickness-input = Kalınlık +pdfjs-editor-ink-opacity-input = Saydamlık +pdfjs-editor-stamp-add-image-button = + .title = Resim ekle +pdfjs-editor-stamp-add-image-button-label = Resim ekle +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Kalınlık +pdfjs-editor-free-highlight-thickness-title = + .title = Metin dışındaki öğeleri vurgularken kalınlığı değiştir +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Metin düzenleyicisi + .default-content = Yazmaya başlayın… +pdfjs-free-text = + .aria-label = Metin düzenleyicisi +pdfjs-free-text-default-content = Yazmaya başlayın… +pdfjs-ink = + .aria-label = Çizim düzenleyicisi +pdfjs-ink-canvas = + .aria-label = Kullanıcı tarafından oluşturulan resim + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Alternatif metin +pdfjs-editor-alt-text-edit-button = + .aria-label = Alternatif metni düzenle +pdfjs-editor-alt-text-edit-button-label = Alternatif metni düzenle +pdfjs-editor-alt-text-dialog-label = Bir seçenek seçin +pdfjs-editor-alt-text-dialog-description = Alternatif metin, insanlar resmi göremediğinde veya resim yüklenmediğinde işe yarar. +pdfjs-editor-alt-text-add-description-label = Açıklama ekle +pdfjs-editor-alt-text-add-description-description = Konuyu, ortamı veya eylemleri tanımlayan bir iki cümle yazmaya çalışın. +pdfjs-editor-alt-text-mark-decorative-label = Dekoratif olarak işaretle +pdfjs-editor-alt-text-mark-decorative-description = Kenarlıklar veya filigranlar gibi dekoratif resimler için kullanılır. +pdfjs-editor-alt-text-cancel-button = Vazgeç +pdfjs-editor-alt-text-save-button = Kaydet +pdfjs-editor-alt-text-decorative-tooltip = Dekoratif olarak işaretlendi +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Örneğin, “Genç bir adam yemek yemek için masaya oturuyor” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Alternatif metin + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Sol üst köşe — yeniden boyutlandır +pdfjs-editor-resizer-label-top-middle = Üst orta — yeniden boyutlandır +pdfjs-editor-resizer-label-top-right = Sağ üst köşe — yeniden boyutlandır +pdfjs-editor-resizer-label-middle-right = Orta sağ — yeniden boyutlandır +pdfjs-editor-resizer-label-bottom-right = Sağ alt köşe — yeniden boyutlandır +pdfjs-editor-resizer-label-bottom-middle = Alt orta — yeniden boyutlandır +pdfjs-editor-resizer-label-bottom-left = Sol alt köşe — yeniden boyutlandır +pdfjs-editor-resizer-label-middle-left = Orta sol — yeniden boyutlandır +pdfjs-editor-resizer-top-left = + .aria-label = Sol üst köşe — yeniden boyutlandır +pdfjs-editor-resizer-top-middle = + .aria-label = Üst orta — yeniden boyutlandır +pdfjs-editor-resizer-top-right = + .aria-label = Sağ üst köşe — yeniden boyutlandır +pdfjs-editor-resizer-middle-right = + .aria-label = Orta sağ — yeniden boyutlandır +pdfjs-editor-resizer-bottom-right = + .aria-label = Sağ alt köşe — yeniden boyutlandır +pdfjs-editor-resizer-bottom-middle = + .aria-label = Alt orta — yeniden boyutlandır +pdfjs-editor-resizer-bottom-left = + .aria-label = Sol alt köşe — yeniden boyutlandır +pdfjs-editor-resizer-middle-left = + .aria-label = Orta sol — yeniden boyutlandır + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Vurgu rengi +pdfjs-editor-colorpicker-button = + .title = Rengi değiştir +pdfjs-editor-colorpicker-dropdown = + .aria-label = Renk seçenekleri +pdfjs-editor-colorpicker-yellow = + .title = Sarı +pdfjs-editor-colorpicker-green = + .title = Yeşil +pdfjs-editor-colorpicker-blue = + .title = Mavi +pdfjs-editor-colorpicker-pink = + .title = Pembe +pdfjs-editor-colorpicker-red = + .title = Kırmızı + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Tümünü göster +pdfjs-editor-highlight-show-all-button = + .title = Tümünü göster + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Alt metni düzenle (resim açıklaması) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Alt metin ekle (resim açıklaması) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Açıklamanızı buraya yazın… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Görme engelli kişilere gösterilecek veya resmin yüklenemediği durumlarda gösterilecek kısa açıklama. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Bu alt metin otomatik olarak oluşturulmuştur ve hatalı olabilir. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Daha fazla bilgi alın +pdfjs-editor-new-alt-text-create-automatically-button-label = Otomatik olarak alt metin oluştur +pdfjs-editor-new-alt-text-not-now-button = Şimdi değil +pdfjs-editor-new-alt-text-error-title = Alt metin otomatik olarak oluşturulamadı +pdfjs-editor-new-alt-text-error-description = Lütfen kendi alt metninizi yazın veya daha sonra yeniden deneyin. +pdfjs-editor-new-alt-text-error-close-button = Kapat +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Alt metin yapay zekâ modeli indiriliyor ({ $downloadedSize } / { $totalSize } MB) + .aria-valuetext = Alt metin yapay zekâ modeli indiriliyor ({ $downloadedSize } / { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Alternatif metin eklendi +pdfjs-editor-new-alt-text-added-button-label = Alt metin eklendi +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Alternatif metin eksik +pdfjs-editor-new-alt-text-missing-button-label = Alt metin eksik +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Alternatif metni incele +pdfjs-editor-new-alt-text-to-review-button-label = Alt metni incele +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Otomatik olarak oluşturuldu: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Resim alt metni ayarları +pdfjs-image-alt-text-settings-button-label = Resim alt metni ayarları +pdfjs-editor-alt-text-settings-dialog-label = Resim alt metni ayarları +pdfjs-editor-alt-text-settings-automatic-title = Otomatik alt metin +pdfjs-editor-alt-text-settings-create-model-button-label = Otomatik olarak alt metin oluştur +pdfjs-editor-alt-text-settings-create-model-description = Görme engelli kişilere gösterilecek veya resmin yüklenemediği durumlarda gösterilecek açıklamalar önerir. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Alt metin yapay zekâ modeli ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Verilerinizin gizli kalması için cihazınızda yerel olarak çalışır. Otomatik alt metin için gereklidir. +pdfjs-editor-alt-text-settings-delete-model-button = Sil +pdfjs-editor-alt-text-settings-download-model-button = İndir +pdfjs-editor-alt-text-settings-downloading-model-button = İndiriliyor… +pdfjs-editor-alt-text-settings-editor-title = Alt metin düzenleyicisi +pdfjs-editor-alt-text-settings-show-dialog-button-label = Resim eklerken alt metin düzenleyicisini hemen göster +pdfjs-editor-alt-text-settings-show-dialog-description = Tüm resimlerinizin alt metne sahip olduğundan emin olmanızı sağlar. +pdfjs-editor-alt-text-settings-close-button = Kapat + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Vurgulama silindi +pdfjs-editor-undo-bar-message-freetext = Metin silindi +pdfjs-editor-undo-bar-message-ink = Çizim silindi +pdfjs-editor-undo-bar-message-stamp = Görsel silindi +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } ek açıklama silindi + *[other] { $count } ek açıklama silindi + } +pdfjs-editor-undo-bar-undo-button = + .title = Geri al +pdfjs-editor-undo-bar-undo-button-label = Geri al +pdfjs-editor-undo-bar-close-button = + .title = Kapat +pdfjs-editor-undo-bar-close-button-label = Kapat diff --git a/public/assets/pdfjs/locale/trs/viewer.ftl b/public/assets/pdfjs/locale/trs/viewer.ftl new file mode 100644 index 0000000..aba3c72 --- /dev/null +++ b/public/assets/pdfjs/locale/trs/viewer.ftl @@ -0,0 +1,197 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Pajinâ gunâj rukùu +pdfjs-previous-button-label = Sa gachin +pdfjs-next-button = + .title = Pajinâ 'na' ñaan +pdfjs-next-button-label = Ne' ñaan +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Ñanj +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = si'iaj { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount }) +pdfjs-zoom-out-button = + .title = Nagi'iaj li' +pdfjs-zoom-out-button-label = Nagi'iaj li' +pdfjs-zoom-in-button = + .title = Nagi'iaj niko' +pdfjs-zoom-in-button-label = Nagi'iaj niko' +pdfjs-zoom-select = + .title = dàj nìko ma'an +pdfjs-presentation-mode-button = + .title = Naduno' daj ga ma +pdfjs-presentation-mode-button-label = Daj gà ma +pdfjs-open-file-button = + .title = Na'nïn' chrû ñanj +pdfjs-open-file-button-label = Na'nïn +pdfjs-print-button = + .title = Nari' ña du'ua +pdfjs-print-button-label = Nari' ñadu'ua + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Rasun +pdfjs-tools-button-label = Nej rasùun +pdfjs-first-page-button = + .title = gun' riña pajina asiniin +pdfjs-first-page-button-label = Gun' riña pajina asiniin +pdfjs-last-page-button = + .title = Gun' riña pajina rukù ni'in +pdfjs-last-page-button-label = Gun' riña pajina rukù ni'inj +pdfjs-page-rotate-cw-button = + .title = Tanikaj ne' huat +pdfjs-page-rotate-cw-button-label = Tanikaj ne' huat +pdfjs-page-rotate-ccw-button = + .title = Tanikaj ne' chînt' +pdfjs-page-rotate-ccw-button-label = Tanikaj ne' chint +pdfjs-cursor-text-select-tool-button = + .title = Dugi'iaj sun' sa ganahui texto +pdfjs-cursor-text-select-tool-button-label = Nej rasun arajsun' da' nahui' texto +pdfjs-cursor-hand-tool-button = + .title = Nachrun' nej rasun +pdfjs-cursor-hand-tool-button-label = Sa rajsun ro'o' +pdfjs-scroll-vertical-button = + .title = Garasun' dukuán runūu +pdfjs-scroll-vertical-button-label = Dukuán runūu +pdfjs-scroll-horizontal-button = + .title = Garasun' dukuán nikin' nahui +pdfjs-scroll-horizontal-button-label = Dukuán nikin' nahui +pdfjs-scroll-wrapped-button = + .title = Garasun' sa nachree +pdfjs-scroll-wrapped-button-label = Sa nachree +pdfjs-spread-none-button = + .title = Si nagi'iaj nugun'un' nej pagina hua ninin +pdfjs-spread-none-button-label = Ni'io daj hua pagina +pdfjs-spread-odd-button = + .title = Nagi'iaj nugua'ant nej pajina +pdfjs-spread-odd-button-label = Ni'io' daj hua libro gurin +pdfjs-spread-even-button = + .title = Nakāj dugui' ngà nej pajinâ ayi'ì ngà da' hùi hùi +pdfjs-spread-even-button-label = Nahuin nìko nej + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Nej sa nikāj ñanj… +pdfjs-document-properties-button-label = Nej sa nikāj ñanj… +pdfjs-document-properties-file-name = Si yugui archîbo: +pdfjs-document-properties-file-size = Dàj yachìj archîbo: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Si yugui: +pdfjs-document-properties-author = Sí girirà: +pdfjs-document-properties-subject = Dugui': +pdfjs-document-properties-keywords = Nej nuguan' huìi: +pdfjs-document-properties-creation-date = Gui gurugui' man: +pdfjs-document-properties-modification-date = Nuguan' nahuin nakà: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Guiri ro' +pdfjs-document-properties-producer = Sa ri PDF: +pdfjs-document-properties-version = PDF Version: +pdfjs-document-properties-page-count = Si Guendâ Pâjina: +pdfjs-document-properties-page-size = Dàj yachìj pâjina: +pdfjs-document-properties-page-size-unit-inches = riña +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = nadu'ua +pdfjs-document-properties-page-size-orientation-landscape = dàj huaj +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Da'ngà'a +pdfjs-document-properties-page-size-name-legal = Nuguan' a'nï'ïn + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Nanèt chre ni'iajt riña Web: +pdfjs-document-properties-linearized-yes = Ga'ue +pdfjs-document-properties-linearized-no = Si ga'ue +pdfjs-document-properties-close-button = Narán + +## Print + +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Duyichin' + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Nadunā barrâ nù yi'nïn +pdfjs-toggle-sidebar-button-label = Nadunā barrâ nù yi'nïn +pdfjs-findbar-button-label = Narì' + +## Thumbnails panel item (tooltip and alt text for images) + + +## Find panel button title and messages + +pdfjs-find-previous-button-label = Sa gachîn +pdfjs-find-next-button-label = Ne' ñaan +pdfjs-find-highlight-checkbox = Daran' sa ña'an +pdfjs-find-match-case-checkbox-label = Match case +pdfjs-find-not-found = Nu narì'ij nugua'anj + +## Predefined zoom values + +pdfjs-page-scale-actual = Dàj yàchi akuan' nín +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + + +## Annotations + + +## Password + +pdfjs-password-ok-button = Ga'ue +pdfjs-password-cancel-button = Duyichin' + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/uk/viewer.ftl b/public/assets/pdfjs/locale/uk/viewer.ftl new file mode 100644 index 0000000..dd54727 --- /dev/null +++ b/public/assets/pdfjs/locale/uk/viewer.ftl @@ -0,0 +1,518 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Попередня сторінка +pdfjs-previous-button-label = Попередня +pdfjs-next-button = + .title = Наступна сторінка +pdfjs-next-button-label = Наступна +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Сторінка +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = із { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } із { $pagesCount }) +pdfjs-zoom-out-button = + .title = Зменшити +pdfjs-zoom-out-button-label = Зменшити +pdfjs-zoom-in-button = + .title = Збільшити +pdfjs-zoom-in-button-label = Збільшити +pdfjs-zoom-select = + .title = Масштаб +pdfjs-presentation-mode-button = + .title = Перейти в режим презентації +pdfjs-presentation-mode-button-label = Режим презентації +pdfjs-open-file-button = + .title = Відкрити файл +pdfjs-open-file-button-label = Відкрити +pdfjs-print-button = + .title = Друк +pdfjs-print-button-label = Друк +pdfjs-save-button = + .title = Зберегти +pdfjs-save-button-label = Зберегти +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Завантажити +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Завантажити +pdfjs-bookmark-button = + .title = Поточна сторінка (перегляд URL-адреси з поточної сторінки) +pdfjs-bookmark-button-label = Поточна сторінка + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Інструменти +pdfjs-tools-button-label = Інструменти +pdfjs-first-page-button = + .title = На першу сторінку +pdfjs-first-page-button-label = На першу сторінку +pdfjs-last-page-button = + .title = На останню сторінку +pdfjs-last-page-button-label = На останню сторінку +pdfjs-page-rotate-cw-button = + .title = Повернути за годинниковою стрілкою +pdfjs-page-rotate-cw-button-label = Повернути за годинниковою стрілкою +pdfjs-page-rotate-ccw-button = + .title = Повернути проти годинникової стрілки +pdfjs-page-rotate-ccw-button-label = Повернути проти годинникової стрілки +pdfjs-cursor-text-select-tool-button = + .title = Увімкнути інструмент вибору тексту +pdfjs-cursor-text-select-tool-button-label = Інструмент вибору тексту +pdfjs-cursor-hand-tool-button = + .title = Увімкнути інструмент "Рука" +pdfjs-cursor-hand-tool-button-label = Інструмент "Рука" +pdfjs-scroll-page-button = + .title = Використовувати прокручування сторінки +pdfjs-scroll-page-button-label = Прокручування сторінки +pdfjs-scroll-vertical-button = + .title = Використовувати вертикальне прокручування +pdfjs-scroll-vertical-button-label = Вертикальне прокручування +pdfjs-scroll-horizontal-button = + .title = Використовувати горизонтальне прокручування +pdfjs-scroll-horizontal-button-label = Горизонтальне прокручування +pdfjs-scroll-wrapped-button = + .title = Використовувати масштабоване прокручування +pdfjs-scroll-wrapped-button-label = Масштабоване прокручування +pdfjs-spread-none-button = + .title = Не використовувати розгорнуті сторінки +pdfjs-spread-none-button-label = Без розгорнутих сторінок +pdfjs-spread-odd-button = + .title = Розгорнуті сторінки починаються з непарних номерів +pdfjs-spread-odd-button-label = Непарні сторінки зліва +pdfjs-spread-even-button = + .title = Розгорнуті сторінки починаються з парних номерів +pdfjs-spread-even-button-label = Парні сторінки зліва + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Властивості документа… +pdfjs-document-properties-button-label = Властивості документа… +pdfjs-document-properties-file-name = Назва файлу: +pdfjs-document-properties-file-size = Розмір файлу: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } кБ ({ $b } байтів) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } МБ ({ $b } байтів) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } кБ ({ $size_b } байтів) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } МБ ({ $size_b } байтів) +pdfjs-document-properties-title = Заголовок: +pdfjs-document-properties-author = Автор: +pdfjs-document-properties-subject = Тема: +pdfjs-document-properties-keywords = Ключові слова: +pdfjs-document-properties-creation-date = Дата створення: +pdfjs-document-properties-modification-date = Дата зміни: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Створено: +pdfjs-document-properties-producer = Виробник PDF: +pdfjs-document-properties-version = Версія PDF: +pdfjs-document-properties-page-count = Кількість сторінок: +pdfjs-document-properties-page-size = Розмір сторінки: +pdfjs-document-properties-page-size-unit-inches = дюймів +pdfjs-document-properties-page-size-unit-millimeters = мм +pdfjs-document-properties-page-size-orientation-portrait = книжкова +pdfjs-document-properties-page-size-orientation-landscape = альбомна +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Швидкий перегляд в Інтернеті: +pdfjs-document-properties-linearized-yes = Так +pdfjs-document-properties-linearized-no = Ні +pdfjs-document-properties-close-button = Закрити + +## Print + +pdfjs-print-progress-message = Підготовка документу до друку… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Скасувати +pdfjs-printing-not-supported = Попередження: Цей браузер не повністю підтримує друк. +pdfjs-printing-not-ready = Попередження: PDF не повністю завантажений для друку. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Бічна панель +pdfjs-toggle-sidebar-notification-button = + .title = Перемкнути бічну панель (документ містить ескіз/вкладення/шари) +pdfjs-toggle-sidebar-button-label = Перемкнути бічну панель +pdfjs-document-outline-button = + .title = Показати схему документу (подвійний клік для розгортання/згортання елементів) +pdfjs-document-outline-button-label = Схема документа +pdfjs-attachments-button = + .title = Показати вкладення +pdfjs-attachments-button-label = Вкладення +pdfjs-layers-button = + .title = Показати шари (двічі клацніть, щоб скинути всі шари до типового стану) +pdfjs-layers-button-label = Шари +pdfjs-thumbs-button = + .title = Показати мініатюри +pdfjs-thumbs-button-label = Мініатюри +pdfjs-current-outline-item-button = + .title = Знайти поточний елемент змісту +pdfjs-current-outline-item-button-label = Поточний елемент змісту +pdfjs-findbar-button = + .title = Знайти в документі +pdfjs-findbar-button-label = Знайти +pdfjs-additional-layers = Додаткові шари + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Сторінка { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Ескіз сторінки { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Знайти + .placeholder = Знайти в документі… +pdfjs-find-previous-button = + .title = Знайти попереднє входження фрази +pdfjs-find-previous-button-label = Попереднє +pdfjs-find-next-button = + .title = Знайти наступне входження фрази +pdfjs-find-next-button-label = Наступне +pdfjs-find-highlight-checkbox = Підсвітити все +pdfjs-find-match-case-checkbox-label = З урахуванням регістру +pdfjs-find-match-diacritics-checkbox-label = Відповідність діакритичних знаків +pdfjs-find-entire-word-checkbox-label = Цілі слова +pdfjs-find-reached-top = Досягнуто початку документу, продовжено з кінця +pdfjs-find-reached-bottom = Досягнуто кінця документу, продовжено з початку +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = + { $total -> + [one] { $current } збіг з { $total } + [few] { $current } збіги з { $total } + *[many] { $current } збігів з { $total } + } +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = + { $limit -> + [one] Понад { $limit } збіг + [few] Понад { $limit } збіги + *[many] Понад { $limit } збігів + } +pdfjs-find-not-found = Фразу не знайдено + +## Predefined zoom values + +pdfjs-page-scale-width = За шириною +pdfjs-page-scale-fit = Вмістити +pdfjs-page-scale-auto = Автомасштаб +pdfjs-page-scale-actual = Дійсний розмір +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Сторінка { $page } + +## Loading indicator messages + +pdfjs-loading-error = Під час завантаження PDF сталася помилка. +pdfjs-invalid-file-error = Недійсний або пошкоджений PDF-файл. +pdfjs-missing-file-error = Відсутній PDF-файл. +pdfjs-unexpected-response-error = Неочікувана відповідь сервера. +pdfjs-rendering-error = Під час виведення сторінки сталася помилка. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type }-анотація] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Введіть пароль для відкриття цього PDF-файлу. +pdfjs-password-invalid = Неправильний пароль. Спробуйте ще раз. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Скасувати +pdfjs-web-fonts-disabled = Вебшрифти вимкнено: неможливо використати вбудовані у PDF шрифти. + +## Editing + +pdfjs-editor-free-text-button = + .title = Текст +pdfjs-editor-free-text-button-label = Текст +pdfjs-editor-ink-button = + .title = Малювати +pdfjs-editor-ink-button-label = Малювати +pdfjs-editor-stamp-button = + .title = Додати чи редагувати зображення +pdfjs-editor-stamp-button-label = Додати чи редагувати зображення +pdfjs-editor-highlight-button = + .title = Підсвітити +pdfjs-editor-highlight-button-label = Підсвітити +pdfjs-highlight-floating-button1 = + .title = Підсвітити + .aria-label = Підсвітити +pdfjs-highlight-floating-button-label = Підсвітити + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Вилучити малюнок +pdfjs-editor-remove-freetext-button = + .title = Вилучити текст +pdfjs-editor-remove-stamp-button = + .title = Вилучити зображення +pdfjs-editor-remove-highlight-button = + .title = Вилучити підсвічування + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Колір +pdfjs-editor-free-text-size-input = Розмір +pdfjs-editor-ink-color-input = Колір +pdfjs-editor-ink-thickness-input = Товщина +pdfjs-editor-ink-opacity-input = Прозорість +pdfjs-editor-stamp-add-image-button = + .title = Додати зображення +pdfjs-editor-stamp-add-image-button-label = Додати зображення +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Товщина +pdfjs-editor-free-highlight-thickness-title = + .title = Змінюйте товщину під час підсвічування елементів, крім тексту +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Текстовий редактор + .default-content = Напишіть щось… +pdfjs-free-text = + .aria-label = Текстовий редактор +pdfjs-free-text-default-content = Почніть вводити… +pdfjs-ink = + .aria-label = Графічний редактор +pdfjs-ink-canvas = + .aria-label = Зображення, створене користувачем + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Альтернативний текст +pdfjs-editor-alt-text-edit-button = + .aria-label = Редагувати альтернативний текст +pdfjs-editor-alt-text-edit-button-label = Змінити альтернативний текст +pdfjs-editor-alt-text-dialog-label = Вибрати варіант +pdfjs-editor-alt-text-dialog-description = Альтернативний текст допомагає, коли зображення не видно або коли воно не завантажується. +pdfjs-editor-alt-text-add-description-label = Додати опис +pdfjs-editor-alt-text-add-description-description = Намагайтеся створити 1-2 речення, які описують тему, обставини або дії. +pdfjs-editor-alt-text-mark-decorative-label = Позначити декоративним +pdfjs-editor-alt-text-mark-decorative-description = Використовується для декоративних зображень, наприклад рамок або водяних знаків. +pdfjs-editor-alt-text-cancel-button = Скасувати +pdfjs-editor-alt-text-save-button = Зберегти +pdfjs-editor-alt-text-decorative-tooltip = Позначено декоративним +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Наприклад, “Молодий чоловік сідає за стіл їсти” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Альтернативний текст + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Верхній лівий кут – зміна розміру +pdfjs-editor-resizer-label-top-middle = Вгорі посередині – зміна розміру +pdfjs-editor-resizer-label-top-right = Верхній правий кут – зміна розміру +pdfjs-editor-resizer-label-middle-right = Праворуч посередині – зміна розміру +pdfjs-editor-resizer-label-bottom-right = Нижній правий кут – зміна розміру +pdfjs-editor-resizer-label-bottom-middle = Внизу посередині – зміна розміру +pdfjs-editor-resizer-label-bottom-left = Нижній лівий кут – зміна розміру +pdfjs-editor-resizer-label-middle-left = Ліворуч посередині – зміна розміру +pdfjs-editor-resizer-top-left = + .aria-label = Верхній лівий кут – зміна розміру +pdfjs-editor-resizer-top-middle = + .aria-label = Вгорі посередині – зміна розміру +pdfjs-editor-resizer-top-right = + .aria-label = Верхній правий кут – зміна розміру +pdfjs-editor-resizer-middle-right = + .aria-label = Праворуч посередині – зміна розміру +pdfjs-editor-resizer-bottom-right = + .aria-label = Нижній правий кут – зміна розміру +pdfjs-editor-resizer-bottom-middle = + .aria-label = Внизу посередині – зміна розміру +pdfjs-editor-resizer-bottom-left = + .aria-label = Нижній лівий кут – зміна розміру +pdfjs-editor-resizer-middle-left = + .aria-label = Ліворуч посередині – зміна розміру + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Колір підсвічування +pdfjs-editor-colorpicker-button = + .title = Змінити колір +pdfjs-editor-colorpicker-dropdown = + .aria-label = Вибір кольору +pdfjs-editor-colorpicker-yellow = + .title = Жовтий +pdfjs-editor-colorpicker-green = + .title = Зелений +pdfjs-editor-colorpicker-blue = + .title = Блакитний +pdfjs-editor-colorpicker-pink = + .title = Рожевий +pdfjs-editor-colorpicker-red = + .title = Червоний + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Показати все +pdfjs-editor-highlight-show-all-button = + .title = Показати все + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Редагувати альтернативний текст (опис зображення) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Додати альтернативний текст (опис зображення) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Напишіть свій опис тут… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Короткий опис для людей, які не бачать зображення, або якщо зображення не завантажується. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Цей альтернативний текст створено автоматично, тому він може бути неточним. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Докладніше +pdfjs-editor-new-alt-text-create-automatically-button-label = Автоматично створювати альтернативний текст +pdfjs-editor-new-alt-text-not-now-button = Не зараз +pdfjs-editor-new-alt-text-error-title = Не вдалося автоматично створити альтернативний текст +pdfjs-editor-new-alt-text-error-description = Напишіть власний альтернативний текст або повторіть спробу пізніше. +pdfjs-editor-new-alt-text-error-close-button = Закрити +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Завантаження моделі ШІ для альтернативного тексту ({ $downloadedSize } з { $totalSize } МБ) + .aria-valuetext = Завантаження моделі ШІ для альтернативного тексту ({ $downloadedSize } з { $totalSize } МБ) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Альтернативний текст додано +pdfjs-editor-new-alt-text-added-button-label = Альтернативний текст додано +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Відсутній альтернативний текст +pdfjs-editor-new-alt-text-missing-button-label = Відсутній альтернативний текст +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Переглянути альтернативний текст +pdfjs-editor-new-alt-text-to-review-button-label = Переглянути альтернативний текст +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Створено автоматично: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Налаштування альтернативного тексту зображення +pdfjs-image-alt-text-settings-button-label = Налаштування альтернативного тексту зображення +pdfjs-editor-alt-text-settings-dialog-label = Налаштування альтернативного тексту зображення +pdfjs-editor-alt-text-settings-automatic-title = Автоматичний альтернативний текст +pdfjs-editor-alt-text-settings-create-model-button-label = Автоматично створювати альтернативний текст +pdfjs-editor-alt-text-settings-create-model-description = Пропонує описи, щоб допомогти людям, які не бачать зображення, або якщо зображення не завантажується. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Модель ШІ для альтернативного тексту ({ $totalSize } МБ) +pdfjs-editor-alt-text-settings-ai-model-description = Працює локально на вашому пристрої, тому приватність ваших даних захищена. Призначена для автоматичного створення альтернативного тексту. +pdfjs-editor-alt-text-settings-delete-model-button = Видалити +pdfjs-editor-alt-text-settings-download-model-button = Завантажити +pdfjs-editor-alt-text-settings-downloading-model-button = Завантаження… +pdfjs-editor-alt-text-settings-editor-title = Редактор альтернативного тексту +pdfjs-editor-alt-text-settings-show-dialog-button-label = Показувати редактор альтернативного тексту під час додавання зображення +pdfjs-editor-alt-text-settings-show-dialog-description = Допомагає переконатися, що всі ваші зображення мають альтернативний текст. +pdfjs-editor-alt-text-settings-close-button = Закрити + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Підсвічення вилучено +pdfjs-editor-undo-bar-message-freetext = Текст вилучено +pdfjs-editor-undo-bar-message-ink = Малюнок вилучено +pdfjs-editor-undo-bar-message-stamp = Зображення вилучено +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = + { $count -> + [one] { $count } анотацію вилучено + [few] { $count } анотації вилучено + *[many] { $count } анотацій вилучено + } +pdfjs-editor-undo-bar-undo-button = + .title = Повернути +pdfjs-editor-undo-bar-undo-button-label = Повернути +pdfjs-editor-undo-bar-close-button = + .title = Закрити +pdfjs-editor-undo-bar-close-button-label = Закрити diff --git a/public/assets/pdfjs/locale/ur/viewer.ftl b/public/assets/pdfjs/locale/ur/viewer.ftl new file mode 100644 index 0000000..c15f157 --- /dev/null +++ b/public/assets/pdfjs/locale/ur/viewer.ftl @@ -0,0 +1,248 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = پچھلا صفحہ +pdfjs-previous-button-label = پچھلا +pdfjs-next-button = + .title = اگلا صفحہ +pdfjs-next-button-label = آگے +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = صفحہ +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = { $pagesCount } کا +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } کا { $pagesCount }) +pdfjs-zoom-out-button = + .title = باہر زوم کریں +pdfjs-zoom-out-button-label = باہر زوم کریں +pdfjs-zoom-in-button = + .title = اندر زوم کریں +pdfjs-zoom-in-button-label = اندر زوم کریں +pdfjs-zoom-select = + .title = زوم +pdfjs-presentation-mode-button = + .title = پیشکش موڈ میں چلے جائیں +pdfjs-presentation-mode-button-label = پیشکش موڈ +pdfjs-open-file-button = + .title = مسل کھولیں +pdfjs-open-file-button-label = کھولیں +pdfjs-print-button = + .title = چھاپیں +pdfjs-print-button-label = چھاپیں + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = آلات +pdfjs-tools-button-label = آلات +pdfjs-first-page-button = + .title = پہلے صفحہ پر جائیں +pdfjs-first-page-button-label = پہلے صفحہ پر جائیں +pdfjs-last-page-button = + .title = آخری صفحہ پر جائیں +pdfjs-last-page-button-label = آخری صفحہ پر جائیں +pdfjs-page-rotate-cw-button = + .title = گھڑی وار گھمائیں +pdfjs-page-rotate-cw-button-label = گھڑی وار گھمائیں +pdfjs-page-rotate-ccw-button = + .title = ضد گھڑی وار گھمائیں +pdfjs-page-rotate-ccw-button-label = ضد گھڑی وار گھمائیں +pdfjs-cursor-text-select-tool-button = + .title = متن کے انتخاب کے ٹول کو فعال بناے +pdfjs-cursor-text-select-tool-button-label = متن کے انتخاب کا آلہ +pdfjs-cursor-hand-tool-button = + .title = ہینڈ ٹول کو فعال بناییں +pdfjs-cursor-hand-tool-button-label = ہاتھ کا آلہ +pdfjs-scroll-vertical-button = + .title = عمودی اسکرولنگ کا استعمال کریں +pdfjs-scroll-vertical-button-label = عمودی اسکرولنگ +pdfjs-scroll-horizontal-button = + .title = افقی سکرولنگ کا استعمال کریں +pdfjs-scroll-horizontal-button-label = افقی سکرولنگ +pdfjs-spread-none-button = + .title = صفحہ پھیلانے میں شامل نہ ہوں +pdfjs-spread-none-button-label = کوئی پھیلاؤ نہیں +pdfjs-spread-odd-button-label = تاک پھیلاؤ +pdfjs-spread-even-button-label = جفت پھیلاؤ + +## Document properties dialog + +pdfjs-document-properties-button = + .title = دستاویز خواص… +pdfjs-document-properties-button-label = دستاویز خواص… +pdfjs-document-properties-file-name = نام مسل: +pdfjs-document-properties-file-size = مسل سائز: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = عنوان: +pdfjs-document-properties-author = تخلیق کار: +pdfjs-document-properties-subject = موضوع: +pdfjs-document-properties-keywords = کلیدی الفاظ: +pdfjs-document-properties-creation-date = تخلیق کی تاریخ: +pdfjs-document-properties-modification-date = ترمیم کی تاریخ: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }، { $time } +pdfjs-document-properties-creator = تخلیق کار: +pdfjs-document-properties-producer = PDF پیدا کار: +pdfjs-document-properties-version = PDF ورژن: +pdfjs-document-properties-page-count = صفحہ شمار: +pdfjs-document-properties-page-size = صفہ کی لمبائ: +pdfjs-document-properties-page-size-unit-inches = میں +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = عمودی انداز +pdfjs-document-properties-page-size-orientation-landscape = افقى انداز +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = خط +pdfjs-document-properties-page-size-name-legal = قانونی + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } { $name } { $orientation } + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = تیز ویب دیکھیں: +pdfjs-document-properties-linearized-yes = ہاں +pdfjs-document-properties-linearized-no = نہیں +pdfjs-document-properties-close-button = بند کریں + +## Print + +pdfjs-print-progress-message = چھاپنے کرنے کے لیے دستاویز تیار کیے جا رھے ھیں +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = *{ $progress }%* +pdfjs-print-progress-close-button = منسوخ کریں +pdfjs-printing-not-supported = تنبیہ:چھاپنا اس براؤزر پر پوری طرح معاونت شدہ نہیں ہے۔ +pdfjs-printing-not-ready = تنبیہ: PDF چھپائی کے لیے پوری طرح لوڈ نہیں ہوئی۔ + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = سلائیڈ ٹوگل کریں +pdfjs-toggle-sidebar-button-label = سلائیڈ ٹوگل کریں +pdfjs-document-outline-button = + .title = دستاویز کی سرخیاں دکھایں (تمام اشیاء وسیع / غائب کرنے کے لیے ڈبل کلک کریں) +pdfjs-document-outline-button-label = دستاویز آؤٹ لائن +pdfjs-attachments-button = + .title = منسلکات دکھائیں +pdfjs-attachments-button-label = منسلکات +pdfjs-thumbs-button = + .title = تھمبنیل دکھائیں +pdfjs-thumbs-button-label = مجمل +pdfjs-findbar-button = + .title = دستاویز میں ڈھونڈیں +pdfjs-findbar-button-label = ڈھونڈیں + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = صفحہ { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = صفحے کا مجمل { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = ڈھونڈیں + .placeholder = دستاویز… میں ڈھونڈیں +pdfjs-find-previous-button = + .title = فقرے کا پچھلا وقوع ڈھونڈیں +pdfjs-find-previous-button-label = پچھلا +pdfjs-find-next-button = + .title = فقرے کا اگلہ وقوع ڈھونڈیں +pdfjs-find-next-button-label = آگے +pdfjs-find-highlight-checkbox = تمام نمایاں کریں +pdfjs-find-match-case-checkbox-label = حروف مشابہ کریں +pdfjs-find-entire-word-checkbox-label = تمام الفاظ +pdfjs-find-reached-top = صفحہ کے شروع پر پہنچ گیا، نیچے سے جاری کیا +pdfjs-find-reached-bottom = صفحہ کے اختتام پر پہنچ گیا، اوپر سے جاری کیا +pdfjs-find-not-found = فقرا نہیں ملا + +## Predefined zoom values + +pdfjs-page-scale-width = صفحہ چوڑائی +pdfjs-page-scale-fit = صفحہ فٹنگ +pdfjs-page-scale-auto = خودکار زوم +pdfjs-page-scale-actual = اصل سائز +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = صفحہ { $page } + +## Loading indicator messages + +pdfjs-loading-error = PDF لوڈ کرتے وقت نقص آ گیا۔ +pdfjs-invalid-file-error = ناجائز یا خراب PDF مسل +pdfjs-missing-file-error = PDF مسل غائب ہے۔ +pdfjs-unexpected-response-error = غیرمتوقع پیش کار جواب +pdfjs-rendering-error = صفحہ بناتے ہوئے نقص آ گیا۔ + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }.{ $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } نوٹ] + +## Password + +pdfjs-password-label = PDF مسل کھولنے کے لیے پاس ورڈ داخل کریں. +pdfjs-password-invalid = ناجائز پاس ورڈ. براےؑ کرم دوبارہ کوشش کریں. +pdfjs-password-ok-button = ٹھیک ہے +pdfjs-password-cancel-button = منسوخ کریں +pdfjs-web-fonts-disabled = ویب فانٹ نا اہل ہیں: شامل PDF فانٹ استعمال کرنے میں ناکام۔ + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/uz/viewer.ftl b/public/assets/pdfjs/locale/uz/viewer.ftl new file mode 100644 index 0000000..fb82f22 --- /dev/null +++ b/public/assets/pdfjs/locale/uz/viewer.ftl @@ -0,0 +1,187 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Oldingi sahifa +pdfjs-previous-button-label = Oldingi +pdfjs-next-button = + .title = Keyingi sahifa +pdfjs-next-button-label = Keyingi +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = /{ $pagesCount } +pdfjs-zoom-out-button = + .title = Kichiklashtirish +pdfjs-zoom-out-button-label = Kichiklashtirish +pdfjs-zoom-in-button = + .title = Kattalashtirish +pdfjs-zoom-in-button-label = Kattalashtirish +pdfjs-zoom-select = + .title = Masshtab +pdfjs-presentation-mode-button = + .title = Namoyish usuliga oʻtish +pdfjs-presentation-mode-button-label = Namoyish usuli +pdfjs-open-file-button = + .title = Faylni ochish +pdfjs-open-file-button-label = Ochish +pdfjs-print-button = + .title = Chop qilish +pdfjs-print-button-label = Chop qilish + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Vositalar +pdfjs-tools-button-label = Vositalar +pdfjs-first-page-button = + .title = Birinchi sahifaga oʻtish +pdfjs-first-page-button-label = Birinchi sahifaga oʻtish +pdfjs-last-page-button = + .title = Soʻnggi sahifaga oʻtish +pdfjs-last-page-button-label = Soʻnggi sahifaga oʻtish +pdfjs-page-rotate-cw-button = + .title = Soat yoʻnalishi boʻyicha burish +pdfjs-page-rotate-cw-button-label = Soat yoʻnalishi boʻyicha burish +pdfjs-page-rotate-ccw-button = + .title = Soat yoʻnalishiga qarshi burish +pdfjs-page-rotate-ccw-button-label = Soat yoʻnalishiga qarshi burish + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Hujjat xossalari +pdfjs-document-properties-button-label = Hujjat xossalari +pdfjs-document-properties-file-name = Fayl nomi: +pdfjs-document-properties-file-size = Fayl hajmi: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } bytes) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } bytes) +pdfjs-document-properties-title = Nomi: +pdfjs-document-properties-author = Muallifi: +pdfjs-document-properties-subject = Mavzusi: +pdfjs-document-properties-keywords = Kalit so‘zlar +pdfjs-document-properties-creation-date = Yaratilgan sanasi: +pdfjs-document-properties-modification-date = O‘zgartirilgan sanasi +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Yaratuvchi: +pdfjs-document-properties-producer = PDF ishlab chiqaruvchi: +pdfjs-document-properties-version = PDF versiyasi: +pdfjs-document-properties-page-count = Sahifa soni: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = Yopish + +## Print + +pdfjs-printing-not-supported = Diqqat: chop qilish bruzer tomonidan toʻliq qoʻllab-quvvatlanmaydi. +pdfjs-printing-not-ready = Diqqat: PDF fayl chop qilish uchun toʻliq yuklanmadi. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Yon panelni yoqib/oʻchirib qoʻyish +pdfjs-toggle-sidebar-button-label = Yon panelni yoqib/oʻchirib qoʻyish +pdfjs-document-outline-button-label = Hujjat tuzilishi +pdfjs-attachments-button = + .title = Ilovalarni ko‘rsatish +pdfjs-attachments-button-label = Ilovalar +pdfjs-thumbs-button = + .title = Nishonchalarni koʻrsatish +pdfjs-thumbs-button-label = Nishoncha +pdfjs-findbar-button = + .title = Hujjat ichidan topish + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = { $page } sahifa +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = { $page } sahifa nishonchasi + +## Find panel button title and messages + +pdfjs-find-previous-button = + .title = Soʻzlardagi oldingi hodisani topish +pdfjs-find-previous-button-label = Oldingi +pdfjs-find-next-button = + .title = Iboradagi keyingi hodisani topish +pdfjs-find-next-button-label = Keyingi +pdfjs-find-highlight-checkbox = Barchasini ajratib koʻrsatish +pdfjs-find-match-case-checkbox-label = Katta-kichik harflarni farqlash +pdfjs-find-reached-top = Hujjatning boshigacha yetib keldik, pastdan davom ettiriladi +pdfjs-find-reached-bottom = Hujjatning oxiriga yetib kelindi, yuqoridan davom ettirladi +pdfjs-find-not-found = Soʻzlar topilmadi + +## Predefined zoom values + +pdfjs-page-scale-width = Sahifa eni +pdfjs-page-scale-fit = Sahifani moslashtirish +pdfjs-page-scale-auto = Avtomatik masshtab +pdfjs-page-scale-actual = Haqiqiy hajmi +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = PDF yuklanayotganda xato yuz berdi. +pdfjs-invalid-file-error = Xato yoki buzuq PDF fayli. +pdfjs-missing-file-error = PDF fayl kerak. +pdfjs-unexpected-response-error = Kutilmagan server javobi. +pdfjs-rendering-error = Sahifa renderlanayotganda xato yuz berdi. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Annotation] + +## Password + +pdfjs-password-label = PDF faylni ochish uchun parolni kiriting. +pdfjs-password-invalid = Parol - notoʻgʻri. Qaytadan urinib koʻring. +pdfjs-password-ok-button = OK +pdfjs-web-fonts-disabled = Veb shriftlar oʻchirilgan: ichki PDF shriftlardan foydalanib boʻlmmaydi. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/vi/viewer.ftl b/public/assets/pdfjs/locale/vi/viewer.ftl new file mode 100644 index 0000000..af1291f --- /dev/null +++ b/public/assets/pdfjs/locale/vi/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Trang trước +pdfjs-previous-button-label = Trước +pdfjs-next-button = + .title = Trang Sau +pdfjs-next-button-label = Tiếp +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Trang +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = trên { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } trên { $pagesCount }) +pdfjs-zoom-out-button = + .title = Thu nhỏ +pdfjs-zoom-out-button-label = Thu nhỏ +pdfjs-zoom-in-button = + .title = Phóng to +pdfjs-zoom-in-button-label = Phóng to +pdfjs-zoom-select = + .title = Thu phóng +pdfjs-presentation-mode-button = + .title = Chuyển sang chế độ trình chiếu +pdfjs-presentation-mode-button-label = Chế độ trình chiếu +pdfjs-open-file-button = + .title = Mở tập tin +pdfjs-open-file-button-label = Mở tập tin +pdfjs-print-button = + .title = In +pdfjs-print-button-label = In +pdfjs-save-button = + .title = Lưu +pdfjs-save-button-label = Lưu +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = Tải xuống +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = Tải xuống +pdfjs-bookmark-button = + .title = Trang hiện tại (xem URL từ trang hiện tại) +pdfjs-bookmark-button-label = Trang hiện tại + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Công cụ +pdfjs-tools-button-label = Công cụ +pdfjs-first-page-button = + .title = Về trang đầu +pdfjs-first-page-button-label = Về trang đầu +pdfjs-last-page-button = + .title = Đến trang cuối +pdfjs-last-page-button-label = Đến trang cuối +pdfjs-page-rotate-cw-button = + .title = Xoay theo chiều kim đồng hồ +pdfjs-page-rotate-cw-button-label = Xoay theo chiều kim đồng hồ +pdfjs-page-rotate-ccw-button = + .title = Xoay ngược chiều kim đồng hồ +pdfjs-page-rotate-ccw-button-label = Xoay ngược chiều kim đồng hồ +pdfjs-cursor-text-select-tool-button = + .title = Kích hoạt công cụ chọn vùng văn bản +pdfjs-cursor-text-select-tool-button-label = Công cụ chọn vùng văn bản +pdfjs-cursor-hand-tool-button = + .title = Kích hoạt công cụ con trỏ +pdfjs-cursor-hand-tool-button-label = Công cụ con trỏ +pdfjs-scroll-page-button = + .title = Sử dụng cuộn trang hiện tại +pdfjs-scroll-page-button-label = Cuộn trang hiện tại +pdfjs-scroll-vertical-button = + .title = Sử dụng cuộn dọc +pdfjs-scroll-vertical-button-label = Cuộn dọc +pdfjs-scroll-horizontal-button = + .title = Sử dụng cuộn ngang +pdfjs-scroll-horizontal-button-label = Cuộn ngang +pdfjs-scroll-wrapped-button = + .title = Sử dụng cuộn ngắt dòng +pdfjs-scroll-wrapped-button-label = Cuộn ngắt dòng +pdfjs-spread-none-button = + .title = Không nối rộng trang +pdfjs-spread-none-button-label = Không có phân cách +pdfjs-spread-odd-button = + .title = Nối trang bài bắt đầu với các trang được đánh số lẻ +pdfjs-spread-odd-button-label = Phân cách theo số lẻ +pdfjs-spread-even-button = + .title = Nối trang bài bắt đầu với các trang được đánh số chẵn +pdfjs-spread-even-button-label = Phân cách theo số chẵn + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Thuộc tính của tài liệu… +pdfjs-document-properties-button-label = Thuộc tính của tài liệu… +pdfjs-document-properties-file-name = Tên tập tin: +pdfjs-document-properties-file-size = Kích thước: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } byte) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } byte) +pdfjs-document-properties-title = Tiêu đề: +pdfjs-document-properties-author = Tác giả: +pdfjs-document-properties-subject = Chủ đề: +pdfjs-document-properties-keywords = Từ khóa: +pdfjs-document-properties-creation-date = Ngày tạo: +pdfjs-document-properties-modification-date = Ngày sửa đổi: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Người tạo: +pdfjs-document-properties-producer = Phần mềm tạo PDF: +pdfjs-document-properties-version = Phiên bản PDF: +pdfjs-document-properties-page-count = Tổng số trang: +pdfjs-document-properties-page-size = Kích thước trang: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = khổ dọc +pdfjs-document-properties-page-size-orientation-landscape = khổ ngang +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Thư +pdfjs-document-properties-page-size-name-legal = Pháp lý + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit } ({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit } ({ $name }, { $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = Xem nhanh trên web: +pdfjs-document-properties-linearized-yes = Có +pdfjs-document-properties-linearized-no = Không +pdfjs-document-properties-close-button = Ðóng + +## Print + +pdfjs-print-progress-message = Chuẩn bị trang để in… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Hủy bỏ +pdfjs-printing-not-supported = Cảnh báo: In ấn không được hỗ trợ đầy đủ ở trình duyệt này. +pdfjs-printing-not-ready = Cảnh báo: PDF chưa được tải hết để in. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Bật/Tắt thanh lề +pdfjs-toggle-sidebar-notification-button = + .title = Bật tắt thanh lề (tài liệu bao gồm bản phác thảo/tập tin đính kèm/lớp) +pdfjs-toggle-sidebar-button-label = Bật/Tắt thanh lề +pdfjs-document-outline-button = + .title = Hiển thị tài liệu phác thảo (nhấp đúp vào để mở rộng/thu gọn tất cả các mục) +pdfjs-document-outline-button-label = Bản phác tài liệu +pdfjs-attachments-button = + .title = Hiện nội dung đính kèm +pdfjs-attachments-button-label = Nội dung đính kèm +pdfjs-layers-button = + .title = Hiển thị các lớp (nhấp đúp để đặt lại tất cả các lớp về trạng thái mặc định) +pdfjs-layers-button-label = Lớp +pdfjs-thumbs-button = + .title = Hiển thị ảnh thu nhỏ +pdfjs-thumbs-button-label = Ảnh thu nhỏ +pdfjs-current-outline-item-button = + .title = Tìm mục phác thảo hiện tại +pdfjs-current-outline-item-button-label = Mục phác thảo hiện tại +pdfjs-findbar-button = + .title = Tìm trong tài liệu +pdfjs-findbar-button-label = Tìm +pdfjs-additional-layers = Các lớp bổ sung + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Trang { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Ảnh thu nhỏ của trang { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Tìm + .placeholder = Tìm trong tài liệu… +pdfjs-find-previous-button = + .title = Tìm cụm từ ở phần trước +pdfjs-find-previous-button-label = Trước +pdfjs-find-next-button = + .title = Tìm cụm từ ở phần sau +pdfjs-find-next-button-label = Tiếp +pdfjs-find-highlight-checkbox = Đánh dấu tất cả +pdfjs-find-match-case-checkbox-label = Phân biệt hoa, thường +pdfjs-find-match-diacritics-checkbox-label = Khớp dấu phụ +pdfjs-find-entire-word-checkbox-label = Toàn bộ từ +pdfjs-find-reached-top = Đã đến phần đầu tài liệu, quay trở lại từ cuối +pdfjs-find-reached-bottom = Đã đến phần cuối của tài liệu, quay trở lại từ đầu +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = { $current } trên { $total } kết quả +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = Tìm thấy hơn { $limit } kết quả +pdfjs-find-not-found = Không tìm thấy cụm từ này + +## Predefined zoom values + +pdfjs-page-scale-width = Vừa chiều rộng +pdfjs-page-scale-fit = Vừa chiều cao +pdfjs-page-scale-auto = Tự động chọn kích thước +pdfjs-page-scale-actual = Kích thước thực +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = Trang { $page } + +## Loading indicator messages + +pdfjs-loading-error = Lỗi khi tải tài liệu PDF. +pdfjs-invalid-file-error = Tập tin PDF hỏng hoặc không hợp lệ. +pdfjs-missing-file-error = Thiếu tập tin PDF. +pdfjs-unexpected-response-error = Máy chủ có phản hồi lạ. +pdfjs-rendering-error = Lỗi khi hiển thị trang. + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date }, { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Chú thích] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = Nhập mật khẩu để mở tập tin PDF này. +pdfjs-password-invalid = Mật khẩu không đúng. Vui lòng thử lại. +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Hủy bỏ +pdfjs-web-fonts-disabled = Phông chữ Web bị vô hiệu hóa: không thể sử dụng các phông chữ PDF được nhúng. + +## Editing + +pdfjs-editor-free-text-button = + .title = Văn bản +pdfjs-editor-free-text-button-label = Văn bản +pdfjs-editor-ink-button = + .title = Vẽ +pdfjs-editor-ink-button-label = Vẽ +pdfjs-editor-stamp-button = + .title = Thêm hoặc chỉnh sửa hình ảnh +pdfjs-editor-stamp-button-label = Thêm hoặc chỉnh sửa hình ảnh +pdfjs-editor-highlight-button = + .title = Đánh dấu +pdfjs-editor-highlight-button-label = Đánh dấu +pdfjs-highlight-floating-button1 = + .title = Đánh dấu + .aria-label = Đánh dấu +pdfjs-highlight-floating-button-label = Đánh dấu + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = Xóa bản vẽ +pdfjs-editor-remove-freetext-button = + .title = Xóa văn bản +pdfjs-editor-remove-stamp-button = + .title = Xóa ảnh +pdfjs-editor-remove-highlight-button = + .title = Xóa phần đánh dấu + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = Màu +pdfjs-editor-free-text-size-input = Kích cỡ +pdfjs-editor-ink-color-input = Màu +pdfjs-editor-ink-thickness-input = Độ dày +pdfjs-editor-ink-opacity-input = Độ mờ +pdfjs-editor-stamp-add-image-button = + .title = Thêm hình ảnh +pdfjs-editor-stamp-add-image-button-label = Thêm hình ảnh +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = Độ dày +pdfjs-editor-free-highlight-thickness-title = + .title = Thay đổi độ dày khi đánh dấu các mục không phải là văn bản +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = Trình chỉnh sửa văn bản + .default-content = Bắt đầu nhập… +pdfjs-free-text = + .aria-label = Trình sửa văn bản +pdfjs-free-text-default-content = Bắt đầu nhập… +pdfjs-ink = + .aria-label = Trình sửa nét vẽ +pdfjs-ink-canvas = + .aria-label = Hình ảnh do người dùng tạo + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = Văn bản thay thế +pdfjs-editor-alt-text-edit-button = + .aria-label = Chỉnh sửa văn bản thay thế +pdfjs-editor-alt-text-edit-button-label = Chỉnh sửa văn bản thay thế +pdfjs-editor-alt-text-dialog-label = Chọn một lựa chọn +pdfjs-editor-alt-text-dialog-description = Văn bản thay thế sẽ hữu ích khi mọi người không thể thấy hình ảnh hoặc khi hình ảnh không tải. +pdfjs-editor-alt-text-add-description-label = Thêm một mô tả +pdfjs-editor-alt-text-add-description-description = Hãy nhắm tới 1-2 câu mô tả chủ đề, bối cảnh hoặc hành động. +pdfjs-editor-alt-text-mark-decorative-label = Đánh dấu là trang trí +pdfjs-editor-alt-text-mark-decorative-description = Điều này được sử dụng cho các hình ảnh trang trí, như đường viền hoặc watermark. +pdfjs-editor-alt-text-cancel-button = Hủy bỏ +pdfjs-editor-alt-text-save-button = Lưu +pdfjs-editor-alt-text-decorative-tooltip = Đã đánh dấu là trang trí +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = Ví dụ: “Một thanh niên ngồi xuống bàn để thưởng thức một bữa ăn” +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = Văn bản thay thế + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = Trên cùng bên trái — thay đổi kích thước +pdfjs-editor-resizer-label-top-middle = Trên cùng ở giữa — thay đổi kích thước +pdfjs-editor-resizer-label-top-right = Trên cùng bên phải — thay đổi kích thước +pdfjs-editor-resizer-label-middle-right = Ở giữa bên phải — thay đổi kích thước +pdfjs-editor-resizer-label-bottom-right = Dưới cùng bên phải — thay đổi kích thước +pdfjs-editor-resizer-label-bottom-middle = Ở giữa dưới cùng — thay đổi kích thước +pdfjs-editor-resizer-label-bottom-left = Góc dưới bên trái — thay đổi kích thước +pdfjs-editor-resizer-label-middle-left = Ở giữa bên trái — thay đổi kích thước +pdfjs-editor-resizer-top-left = + .aria-label = Trên cùng bên trái — thay đổi kích thước +pdfjs-editor-resizer-top-middle = + .aria-label = Trên cùng ở giữa — thay đổi kích thước +pdfjs-editor-resizer-top-right = + .aria-label = Trên cùng bên phải — thay đổi kích thước +pdfjs-editor-resizer-middle-right = + .aria-label = Ở giữa bên phải — thay đổi kích thước +pdfjs-editor-resizer-bottom-right = + .aria-label = Dưới cùng bên phải — thay đổi kích thước +pdfjs-editor-resizer-bottom-middle = + .aria-label = Ở giữa dưới cùng — thay đổi kích thước +pdfjs-editor-resizer-bottom-left = + .aria-label = Góc dưới bên trái — thay đổi kích thước +pdfjs-editor-resizer-middle-left = + .aria-label = Ở giữa bên trái — thay đổi kích thước + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = Màu đánh dấu +pdfjs-editor-colorpicker-button = + .title = Thay đổi màu +pdfjs-editor-colorpicker-dropdown = + .aria-label = Lựa chọn màu sắc +pdfjs-editor-colorpicker-yellow = + .title = Vàng +pdfjs-editor-colorpicker-green = + .title = Xanh lục +pdfjs-editor-colorpicker-blue = + .title = Xanh dương +pdfjs-editor-colorpicker-pink = + .title = Hồng +pdfjs-editor-colorpicker-red = + .title = Đỏ + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = Hiện tất cả +pdfjs-editor-highlight-show-all-button = + .title = Hiện tất cả + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = Chỉnh sửa văn bản thay thế (mô tả hình ảnh) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = Thêm văn bản thay thế (mô tả hình ảnh) +pdfjs-editor-new-alt-text-textarea = + .placeholder = Viết mô tả của bạn ở đây… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = Mô tả ngắn gọn dành cho người không xem được ảnh hoặc khi không thể tải ảnh. +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = Văn bản thay thế này được tạo tự động và có thể không chính xác. +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Tìm hiểu thêm +pdfjs-editor-new-alt-text-create-automatically-button-label = Tạo văn bản thay thế tự động +pdfjs-editor-new-alt-text-not-now-button = Không phải bây giờ +pdfjs-editor-new-alt-text-error-title = Không thể tạo tự động văn bản thay thế +pdfjs-editor-new-alt-text-error-description = Vui lòng viết văn bản thay thế của riêng bạn hoặc thử lại sau. +pdfjs-editor-new-alt-text-error-close-button = Đóng +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = Đang tải xuống mô hình AI văn bản thay thế ({ $downloadedSize } trong số { $totalSize } MB) + .aria-valuetext = Đang tải xuống mô hình AI văn bản thay thế ({ $downloadedSize } trong số { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = Đã thêm văn bản thay thế +pdfjs-editor-new-alt-text-added-button-label = Đã thêm văn bản thay thế +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = Thiếu văn bản thay thế +pdfjs-editor-new-alt-text-missing-button-label = Thiếu văn bản thay thế +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = Xem lại văn bản thay thế +pdfjs-editor-new-alt-text-to-review-button-label = Xem lại văn bản thay thế +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Được tạo tự động: { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = Cài đặt văn bản thay thế của hình ảnh +pdfjs-image-alt-text-settings-button-label = Cài đặt văn bản thay thế của hình ảnh +pdfjs-editor-alt-text-settings-dialog-label = Cài đặt văn bản thay thế của hình ảnh +pdfjs-editor-alt-text-settings-automatic-title = Văn bản thay thế tự động +pdfjs-editor-alt-text-settings-create-model-button-label = Tạo văn bản thay thế tự động +pdfjs-editor-alt-text-settings-create-model-description = Đề xuất mô tả giúp ích cho những người không xem được ảnh hoặc khi không thể tải ảnh. +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = Mô hình AI văn bản khác ({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = Chạy cục bộ trên thiết bị của bạn để dữ liệu của bạn luôn ở chế độ riêng tư. Bắt buộc đối với văn bản thay thế tự động. +pdfjs-editor-alt-text-settings-delete-model-button = Xóa +pdfjs-editor-alt-text-settings-download-model-button = Tải xuống +pdfjs-editor-alt-text-settings-downloading-model-button = Đang tải xuống… +pdfjs-editor-alt-text-settings-editor-title = Trình soạn thảo văn bản thay thế +pdfjs-editor-alt-text-settings-show-dialog-button-label = Hiển thị ngay trình soạn thảo văn bản thay thế khi thêm hình ảnh +pdfjs-editor-alt-text-settings-show-dialog-description = Giúp bạn đảm bảo tất cả hình ảnh của bạn đều có văn bản thay thế. +pdfjs-editor-alt-text-settings-close-button = Đóng + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = Đã xóa đánh dấu +pdfjs-editor-undo-bar-message-freetext = Đã xóa văn bản +pdfjs-editor-undo-bar-message-ink = Đã xóa bản vẽ +pdfjs-editor-undo-bar-message-stamp = Đã xóa hình ảnh +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = { $count } chú thích đã bị xóa +pdfjs-editor-undo-bar-undo-button = + .title = Hoàn tác +pdfjs-editor-undo-bar-undo-button-label = Hoàn tác +pdfjs-editor-undo-bar-close-button = + .title = Đóng +pdfjs-editor-undo-bar-close-button-label = Đóng diff --git a/public/assets/pdfjs/locale/wo/viewer.ftl b/public/assets/pdfjs/locale/wo/viewer.ftl new file mode 100644 index 0000000..d66c459 --- /dev/null +++ b/public/assets/pdfjs/locale/wo/viewer.ftl @@ -0,0 +1,127 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Xët wi jiitu +pdfjs-previous-button-label = Bi jiitu +pdfjs-next-button = + .title = Xët wi ci topp +pdfjs-next-button-label = Bi ci topp +pdfjs-zoom-out-button = + .title = Wàññi +pdfjs-zoom-out-button-label = Wàññi +pdfjs-zoom-in-button = + .title = Yaatal +pdfjs-zoom-in-button-label = Yaatal +pdfjs-zoom-select = + .title = Yambalaŋ +pdfjs-presentation-mode-button = + .title = Wañarñil ci anamu wone +pdfjs-presentation-mode-button-label = Anamu Wone +pdfjs-open-file-button = + .title = Ubbi benn dencukaay +pdfjs-open-file-button-label = Ubbi +pdfjs-print-button = + .title = Móol +pdfjs-print-button-label = Móol + +## Secondary toolbar and context menu + + +## Document properties dialog + +pdfjs-document-properties-title = Bopp: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + + +## Print + +pdfjs-printing-not-supported = Artu: Joowkat bii nanguwul lool mool. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-thumbs-button = + .title = Wone nataal yu ndaw yi +pdfjs-thumbs-button-label = Nataal yu ndaw yi +pdfjs-findbar-button = + .title = Gis ci biir jukki bi +pdfjs-findbar-button-label = Wut + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Xët { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Wiñet bu xët { $page } + +## Find panel button title and messages + +pdfjs-find-previous-button = + .title = Seet beneen kaddu bu ni mel te jiitu +pdfjs-find-previous-button-label = Bi jiitu +pdfjs-find-next-button = + .title = Seet beneen kaddu bu ni mel +pdfjs-find-next-button-label = Bi ci topp +pdfjs-find-highlight-checkbox = Melaxal lépp +pdfjs-find-match-case-checkbox-label = Sàmm jëmmalin wi +pdfjs-find-reached-top = Jot nañu ndorteel xët wi, kontine dale ko ci suuf +pdfjs-find-reached-bottom = Jot nañu jeexitalu xët wi, kontine ci ndorte +pdfjs-find-not-found = Gisiñu kaddu gi + +## Predefined zoom values + +pdfjs-page-scale-width = Yaatuwaay bu mët +pdfjs-page-scale-fit = Xët lëmm +pdfjs-page-scale-auto = Yambalaŋ ci saa si +pdfjs-page-scale-actual = Dayo bi am + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Am na njumte ci yebum dencukaay PDF bi. +pdfjs-invalid-file-error = Dencukaay PDF bi baaxul walla mu sankar. +pdfjs-rendering-error = Am njumte bu am bi xët bi di wonewu. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [Karmat { $type }] + +## Password + +pdfjs-password-ok-button = OK +pdfjs-password-cancel-button = Neenal + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/xh/viewer.ftl b/public/assets/pdfjs/locale/xh/viewer.ftl new file mode 100644 index 0000000..0798887 --- /dev/null +++ b/public/assets/pdfjs/locale/xh/viewer.ftl @@ -0,0 +1,212 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = Iphepha langaphambili +pdfjs-previous-button-label = Okwangaphambili +pdfjs-next-button = + .title = Iphepha elilandelayo +pdfjs-next-button-label = Okulandelayo +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = Iphepha +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = kwali- { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } kwali { $pagesCount }) +pdfjs-zoom-out-button = + .title = Bhekelisela Kudana +pdfjs-zoom-out-button-label = Bhekelisela Kudana +pdfjs-zoom-in-button = + .title = Sondeza Kufuphi +pdfjs-zoom-in-button-label = Sondeza Kufuphi +pdfjs-zoom-select = + .title = Yandisa / Nciphisa +pdfjs-presentation-mode-button = + .title = Tshintshela kwimo yonikezelo +pdfjs-presentation-mode-button-label = Imo yonikezelo +pdfjs-open-file-button = + .title = Vula Ifayile +pdfjs-open-file-button-label = Vula +pdfjs-print-button = + .title = Printa +pdfjs-print-button-label = Printa + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = Izixhobo zemiyalelo +pdfjs-tools-button-label = Izixhobo zemiyalelo +pdfjs-first-page-button = + .title = Yiya kwiphepha lokuqala +pdfjs-first-page-button-label = Yiya kwiphepha lokuqala +pdfjs-last-page-button = + .title = Yiya kwiphepha lokugqibela +pdfjs-last-page-button-label = Yiya kwiphepha lokugqibela +pdfjs-page-rotate-cw-button = + .title = Jikelisa ngasekunene +pdfjs-page-rotate-cw-button-label = Jikelisa ngasekunene +pdfjs-page-rotate-ccw-button = + .title = Jikelisa ngasekhohlo +pdfjs-page-rotate-ccw-button-label = Jikelisa ngasekhohlo +pdfjs-cursor-text-select-tool-button = + .title = Vumela iSixhobo sokuKhetha iTeksti +pdfjs-cursor-text-select-tool-button-label = ISixhobo sokuKhetha iTeksti +pdfjs-cursor-hand-tool-button = + .title = Yenza iSixhobo seSandla siSebenze +pdfjs-cursor-hand-tool-button-label = ISixhobo seSandla + +## Document properties dialog + +pdfjs-document-properties-button = + .title = Iipropati zoxwebhu… +pdfjs-document-properties-button-label = Iipropati zoxwebhu… +pdfjs-document-properties-file-name = Igama lefayile: +pdfjs-document-properties-file-size = Isayizi yefayile: +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB (iibhayiti{ $size_b }) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB (iibhayithi{ $size_b }) +pdfjs-document-properties-title = Umxholo: +pdfjs-document-properties-author = Umbhali: +pdfjs-document-properties-subject = Umbandela: +pdfjs-document-properties-keywords = Amagama aphambili: +pdfjs-document-properties-creation-date = Umhla wokwenziwa kwayo: +pdfjs-document-properties-modification-date = Umhla wokulungiswa kwayo: +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = Umntu oyenzileyo: +pdfjs-document-properties-producer = Umvelisi we-PDF: +pdfjs-document-properties-version = Uhlelo lwe-PDF: +pdfjs-document-properties-page-count = Inani lamaphepha: + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + + +## + +pdfjs-document-properties-close-button = Vala + +## Print + +pdfjs-print-progress-message = Ilungisa uxwebhu ukuze iprinte… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = Rhoxisa +pdfjs-printing-not-supported = Isilumkiso: Ukuprinta akuxhaswa ngokupheleleyo yile bhrawuza. +pdfjs-printing-not-ready = Isilumkiso: IPDF ayihlohlwanga ngokupheleleyo ukwenzela ukuprinta. + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = Togola ngebha eseCaleni +pdfjs-toggle-sidebar-button-label = Togola ngebha eseCaleni +pdfjs-document-outline-button = + .title = Bonisa uLwandlalo loXwebhu (cofa kabini ukuze wandise/diliza zonke izinto) +pdfjs-document-outline-button-label = Isishwankathelo soxwebhu +pdfjs-attachments-button = + .title = Bonisa iziqhotyoshelwa +pdfjs-attachments-button-label = Iziqhoboshelo +pdfjs-thumbs-button = + .title = Bonisa ukrobiso kumfanekiso +pdfjs-thumbs-button-label = Ukrobiso kumfanekiso +pdfjs-findbar-button = + .title = Fumana kuXwebhu +pdfjs-findbar-button-label = Fumana + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = Iphepha { $page } +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = Ukrobiso kumfanekiso wephepha { $page } + +## Find panel button title and messages + +pdfjs-find-input = + .title = Fumana + .placeholder = Fumana kuXwebhu… +pdfjs-find-previous-button = + .title = Fumanisa isenzeko sangaphambili sebinzana lamagama +pdfjs-find-previous-button-label = Okwangaphambili +pdfjs-find-next-button = + .title = Fumanisa isenzeko esilandelayo sebinzana lamagama +pdfjs-find-next-button-label = Okulandelayo +pdfjs-find-highlight-checkbox = Qaqambisa konke +pdfjs-find-match-case-checkbox-label = Tshatisa ngobukhulu bukanobumba +pdfjs-find-reached-top = Ufike ngaphezulu ephepheni, kusukwa ngezantsi +pdfjs-find-reached-bottom = Ufike ekupheleni kwephepha, kusukwa ngaphezulu +pdfjs-find-not-found = Ibinzana alifunyenwanga + +## Predefined zoom values + +pdfjs-page-scale-width = Ububanzi bephepha +pdfjs-page-scale-fit = Ukulinganiswa kwephepha +pdfjs-page-scale-auto = Ukwandisa/Ukunciphisa Ngokwayo +pdfjs-page-scale-actual = Ubungakanani bokwenene +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + + +## Loading indicator messages + +pdfjs-loading-error = Imposiso yenzekile xa kulayishwa i-PDF. +pdfjs-invalid-file-error = Ifayile ye-PDF engeyiyo okanye eyonakalisiweyo. +pdfjs-missing-file-error = Ifayile ye-PDF edukileyo. +pdfjs-unexpected-response-error = Impendulo yeseva engalindelekanga. +pdfjs-rendering-error = Imposiso yenzekile xa bekunikezelwa iphepha. + +## Annotations + +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } Ubhalo-nqaku] + +## Password + +pdfjs-password-label = Faka ipasiwedi ukuze uvule le fayile yePDF. +pdfjs-password-invalid = Ipasiwedi ayisebenzi. Nceda uzame kwakhona. +pdfjs-password-ok-button = KULUNGILE +pdfjs-password-cancel-button = Rhoxisa +pdfjs-web-fonts-disabled = Iifonti zewebhu ziqhwalelisiwe: ayikwazi ukusebenzisa iifonti ze-PDF ezincanyathelisiweyo. + +## Editing + + +## Alt-text dialog + + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + diff --git a/public/assets/pdfjs/locale/zh-CN/viewer.ftl b/public/assets/pdfjs/locale/zh-CN/viewer.ftl new file mode 100644 index 0000000..8fe9a6a --- /dev/null +++ b/public/assets/pdfjs/locale/zh-CN/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = 上一页 +pdfjs-previous-button-label = 上一页 +pdfjs-next-button = + .title = 下一页 +pdfjs-next-button-label = 下一页 +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = 页面 +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = / { $pagesCount } +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = ({ $pageNumber } / { $pagesCount }) +pdfjs-zoom-out-button = + .title = 缩小 +pdfjs-zoom-out-button-label = 缩小 +pdfjs-zoom-in-button = + .title = 放大 +pdfjs-zoom-in-button-label = 放大 +pdfjs-zoom-select = + .title = 缩放 +pdfjs-presentation-mode-button = + .title = 切换到演示模式 +pdfjs-presentation-mode-button-label = 演示模式 +pdfjs-open-file-button = + .title = 打开文件 +pdfjs-open-file-button-label = 打开 +pdfjs-print-button = + .title = 打印 +pdfjs-print-button-label = 打印 +pdfjs-save-button = + .title = 保存 +pdfjs-save-button-label = 保存 +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = 下载 +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = 下载 +pdfjs-bookmark-button = + .title = 当前页面(在当前页面查看 URL) +pdfjs-bookmark-button-label = 当前页面 + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = 工具 +pdfjs-tools-button-label = 工具 +pdfjs-first-page-button = + .title = 转到第一页 +pdfjs-first-page-button-label = 转到第一页 +pdfjs-last-page-button = + .title = 转到最后一页 +pdfjs-last-page-button-label = 转到最后一页 +pdfjs-page-rotate-cw-button = + .title = 顺时针旋转 +pdfjs-page-rotate-cw-button-label = 顺时针旋转 +pdfjs-page-rotate-ccw-button = + .title = 逆时针旋转 +pdfjs-page-rotate-ccw-button-label = 逆时针旋转 +pdfjs-cursor-text-select-tool-button = + .title = 启用文本选择工具 +pdfjs-cursor-text-select-tool-button-label = 文本选择工具 +pdfjs-cursor-hand-tool-button = + .title = 启用手形工具 +pdfjs-cursor-hand-tool-button-label = 手形工具 +pdfjs-scroll-page-button = + .title = 使用页面滚动 +pdfjs-scroll-page-button-label = 页面滚动 +pdfjs-scroll-vertical-button = + .title = 使用垂直滚动 +pdfjs-scroll-vertical-button-label = 垂直滚动 +pdfjs-scroll-horizontal-button = + .title = 使用水平滚动 +pdfjs-scroll-horizontal-button-label = 水平滚动 +pdfjs-scroll-wrapped-button = + .title = 使用平铺滚动 +pdfjs-scroll-wrapped-button-label = 平铺滚动 +pdfjs-spread-none-button = + .title = 不加入衔接页 +pdfjs-spread-none-button-label = 单页视图 +pdfjs-spread-odd-button = + .title = 加入衔接页使奇数页作为起始页 +pdfjs-spread-odd-button-label = 双页视图 +pdfjs-spread-even-button = + .title = 加入衔接页使偶数页作为起始页 +pdfjs-spread-even-button-label = 书籍视图 + +## Document properties dialog + +pdfjs-document-properties-button = + .title = 文档属性… +pdfjs-document-properties-button-label = 文档属性… +pdfjs-document-properties-file-name = 文件名: +pdfjs-document-properties-file-size = 文件大小: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB({ $b } 字节) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB({ $b } 字节) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB ({ $size_b } 字节) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB ({ $size_b } 字节) +pdfjs-document-properties-title = 标题: +pdfjs-document-properties-author = 作者: +pdfjs-document-properties-subject = 主题: +pdfjs-document-properties-keywords = 关键词: +pdfjs-document-properties-creation-date = 创建日期: +pdfjs-document-properties-modification-date = 修改日期: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date }, { $time } +pdfjs-document-properties-creator = 创建者: +pdfjs-document-properties-producer = PDF 生成器: +pdfjs-document-properties-version = PDF 版本: +pdfjs-document-properties-page-count = 页数: +pdfjs-document-properties-page-size = 页面大小: +pdfjs-document-properties-page-size-unit-inches = 英寸 +pdfjs-document-properties-page-size-unit-millimeters = 毫米 +pdfjs-document-properties-page-size-orientation-portrait = 纵向 +pdfjs-document-properties-page-size-orientation-landscape = 横向 +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit }({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit }({ $name },{ $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = 快速 Web 视图: +pdfjs-document-properties-linearized-yes = 是 +pdfjs-document-properties-linearized-no = 否 +pdfjs-document-properties-close-button = 关闭 + +## Print + +pdfjs-print-progress-message = 正在准备打印文档… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = 取消 +pdfjs-printing-not-supported = 警告:此浏览器尚未完整支持打印功能。 +pdfjs-printing-not-ready = 警告:此 PDF 未完成加载,无法打印。 + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = 切换侧栏 +pdfjs-toggle-sidebar-notification-button = + .title = 切换侧栏(文档所含的大纲/附件/图层) +pdfjs-toggle-sidebar-button-label = 切换侧栏 +pdfjs-document-outline-button = + .title = 显示文档大纲(双击展开/折叠所有项) +pdfjs-document-outline-button-label = 文档大纲 +pdfjs-attachments-button = + .title = 显示附件 +pdfjs-attachments-button-label = 附件 +pdfjs-layers-button = + .title = 显示图层(双击即可将所有图层重置为默认状态) +pdfjs-layers-button-label = 图层 +pdfjs-thumbs-button = + .title = 显示缩略图 +pdfjs-thumbs-button-label = 缩略图 +pdfjs-current-outline-item-button = + .title = 查找当前大纲项目 +pdfjs-current-outline-item-button-label = 当前大纲项目 +pdfjs-findbar-button = + .title = 在文档中查找 +pdfjs-findbar-button-label = 查找 +pdfjs-additional-layers = 其他图层 + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = 第 { $page } 页 +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = 页面 { $page } 的缩略图 + +## Find panel button title and messages + +pdfjs-find-input = + .title = 查找 + .placeholder = 在文档中查找… +pdfjs-find-previous-button = + .title = 查找词语上一次出现的位置 +pdfjs-find-previous-button-label = 上一页 +pdfjs-find-next-button = + .title = 查找词语后一次出现的位置 +pdfjs-find-next-button-label = 下一页 +pdfjs-find-highlight-checkbox = 全部高亮显示 +pdfjs-find-match-case-checkbox-label = 区分大小写 +pdfjs-find-match-diacritics-checkbox-label = 匹配变音符号 +pdfjs-find-entire-word-checkbox-label = 全词匹配 +pdfjs-find-reached-top = 到达文档开头,从末尾继续 +pdfjs-find-reached-bottom = 到达文档末尾,从开头继续 +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = 第 { $current } 项,共找到 { $total } 个匹配项 +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = 匹配超过 { $limit } 项 +pdfjs-find-not-found = 找不到指定词语 + +## Predefined zoom values + +pdfjs-page-scale-width = 适合页宽 +pdfjs-page-scale-fit = 适合页面 +pdfjs-page-scale-auto = 自动缩放 +pdfjs-page-scale-actual = 实际大小 +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = 第 { $page } 页 + +## Loading indicator messages + +pdfjs-loading-error = 加载 PDF 时发生错误。 +pdfjs-invalid-file-error = 无效或损坏的 PDF 文件。 +pdfjs-missing-file-error = 缺少 PDF 文件。 +pdfjs-unexpected-response-error = 意外的服务器响应。 +pdfjs-rendering-error = 渲染页面时发生错误。 + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date },{ $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } 注释] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = 输入密码以打开此 PDF 文件。 +pdfjs-password-invalid = 密码无效。请重试。 +pdfjs-password-ok-button = 确定 +pdfjs-password-cancel-button = 取消 +pdfjs-web-fonts-disabled = Web 字体已被禁用:无法使用嵌入的 PDF 字体。 + +## Editing + +pdfjs-editor-free-text-button = + .title = 文本 +pdfjs-editor-free-text-button-label = 文本 +pdfjs-editor-ink-button = + .title = 绘图 +pdfjs-editor-ink-button-label = 绘图 +pdfjs-editor-stamp-button = + .title = 添加或编辑图像 +pdfjs-editor-stamp-button-label = 添加或编辑图像 +pdfjs-editor-highlight-button = + .title = 高亮 +pdfjs-editor-highlight-button-label = 高亮 +pdfjs-highlight-floating-button1 = + .title = 高亮 + .aria-label = 高亮 +pdfjs-highlight-floating-button-label = 高亮 + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = 移除绘图 +pdfjs-editor-remove-freetext-button = + .title = 移除文本 +pdfjs-editor-remove-stamp-button = + .title = 移除图像 +pdfjs-editor-remove-highlight-button = + .title = 移除高亮 + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = 颜色 +pdfjs-editor-free-text-size-input = 字号 +pdfjs-editor-ink-color-input = 颜色 +pdfjs-editor-ink-thickness-input = 粗细 +pdfjs-editor-ink-opacity-input = 不透明度 +pdfjs-editor-stamp-add-image-button = + .title = 添加图像 +pdfjs-editor-stamp-add-image-button-label = 添加图像 +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = 粗细 +pdfjs-editor-free-highlight-thickness-title = + .title = 更改高亮粗细(用于文本以外项目) +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = 文本编辑器 + .default-content = 在此键入… +pdfjs-free-text = + .aria-label = 文本编辑器 +pdfjs-free-text-default-content = 开始输入… +pdfjs-ink = + .aria-label = 绘图编辑器 +pdfjs-ink-canvas = + .aria-label = 用户创建图像 + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = 替换文字 +pdfjs-editor-alt-text-edit-button = + .aria-label = 编辑替换文字 +pdfjs-editor-alt-text-edit-button-label = 编辑替换文字 +pdfjs-editor-alt-text-dialog-label = 选择一项 +pdfjs-editor-alt-text-dialog-description = 替换文字可在用户无法看到或加载图像时,描述其内容。 +pdfjs-editor-alt-text-add-description-label = 添加描述 +pdfjs-editor-alt-text-add-description-description = 用一两个句子,描述主题、背景或动作。 +pdfjs-editor-alt-text-mark-decorative-label = 标记为装饰 +pdfjs-editor-alt-text-mark-decorative-description = 用于装饰的图像,例如边框和水印。 +pdfjs-editor-alt-text-cancel-button = 取消 +pdfjs-editor-alt-text-save-button = 保存 +pdfjs-editor-alt-text-decorative-tooltip = 已标记为装饰 +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = 例如:一个少年坐到桌前,准备吃饭 +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = 替换文字 + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = 调整尺寸 - 左上角 +pdfjs-editor-resizer-label-top-middle = 调整尺寸 - 顶部中间 +pdfjs-editor-resizer-label-top-right = 调整尺寸 - 右上角 +pdfjs-editor-resizer-label-middle-right = 调整尺寸 - 右侧中间 +pdfjs-editor-resizer-label-bottom-right = 调整尺寸 - 右下角 +pdfjs-editor-resizer-label-bottom-middle = 调整大小 - 底部中间 +pdfjs-editor-resizer-label-bottom-left = 调整尺寸 - 左下角 +pdfjs-editor-resizer-label-middle-left = 调整尺寸 - 左侧中间 +pdfjs-editor-resizer-top-left = + .aria-label = 调整尺寸 - 左上角 +pdfjs-editor-resizer-top-middle = + .aria-label = 调整尺寸 - 顶部中间 +pdfjs-editor-resizer-top-right = + .aria-label = 调整尺寸 - 右上角 +pdfjs-editor-resizer-middle-right = + .aria-label = 调整尺寸 - 右侧中间 +pdfjs-editor-resizer-bottom-right = + .aria-label = 调整尺寸 - 右下角 +pdfjs-editor-resizer-bottom-middle = + .aria-label = 调整大小 - 底部中间 +pdfjs-editor-resizer-bottom-left = + .aria-label = 调整尺寸 - 左下角 +pdfjs-editor-resizer-middle-left = + .aria-label = 调整尺寸 - 左侧中间 + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = 高亮色 +pdfjs-editor-colorpicker-button = + .title = 更改颜色 +pdfjs-editor-colorpicker-dropdown = + .aria-label = 颜色选择 +pdfjs-editor-colorpicker-yellow = + .title = 黄色 +pdfjs-editor-colorpicker-green = + .title = 绿色 +pdfjs-editor-colorpicker-blue = + .title = 蓝色 +pdfjs-editor-colorpicker-pink = + .title = 粉色 +pdfjs-editor-colorpicker-red = + .title = 红色 + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = 显示全部 +pdfjs-editor-highlight-show-all-button = + .title = 显示全部 + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = 编辑替换文字(图像描述) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = 添加替换文字(图像描述) +pdfjs-editor-new-alt-text-textarea = + .placeholder = 请在此处撰写描述… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = 向无法看到或加载图像的用户提供的简短描述。 +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = 此段替换文字为自动创建,有可能不准确。 +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = 详细了解 +pdfjs-editor-new-alt-text-create-automatically-button-label = 自动创建替换文字 +pdfjs-editor-new-alt-text-not-now-button = 暂时不要 +pdfjs-editor-new-alt-text-error-title = 无法自动创建替换文字 +pdfjs-editor-new-alt-text-error-description = 请自行撰写替换文字,或稍后再试。 +pdfjs-editor-new-alt-text-error-close-button = 关闭 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = 正在下载提供替换文字的 AI 模型({ $downloadedSize }/{ $totalSize } MB) + .aria-valuetext = 正在下载提供替换文字的 AI 模型({ $downloadedSize }/{ $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = 已添加替换文字 +pdfjs-editor-new-alt-text-added-button-label = 已添加替换文字 +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = 缺少替换文字 +pdfjs-editor-new-alt-text-missing-button-label = 缺少替换文字 +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = 检查替换文字 +pdfjs-editor-new-alt-text-to-review-button-label = 检查替换文字 +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = [自动创建] { $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = 图像替换文字设置 +pdfjs-image-alt-text-settings-button-label = 图像替换文字设置 +pdfjs-editor-alt-text-settings-dialog-label = 图像替换文字设置 +pdfjs-editor-alt-text-settings-automatic-title = 自动创建替换文字 +pdfjs-editor-alt-text-settings-create-model-button-label = 自动创建替换文字 +pdfjs-editor-alt-text-settings-create-model-description = 向无法看到或加载图像的用户提供描述。 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = 提供替换文字的 AI 模型({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = 在您的设备本地运行,可使数据保持私密。自动创建替换文字需要使用此模型。 +pdfjs-editor-alt-text-settings-delete-model-button = 删除 +pdfjs-editor-alt-text-settings-download-model-button = 下载 +pdfjs-editor-alt-text-settings-downloading-model-button = 正在下载… +pdfjs-editor-alt-text-settings-editor-title = 替换文字编辑器 +pdfjs-editor-alt-text-settings-show-dialog-button-label = 添加图像后立即显示替换文字编辑器 +pdfjs-editor-alt-text-settings-show-dialog-description = 帮助确保所有图像均拥有替换文字。 +pdfjs-editor-alt-text-settings-close-button = 关闭 + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = 已移除高亮 +pdfjs-editor-undo-bar-message-freetext = 已移除文本 +pdfjs-editor-undo-bar-message-ink = 已移除绘图 +pdfjs-editor-undo-bar-message-stamp = 已移除图像 +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = 已移除 { $count } 条注释 +pdfjs-editor-undo-bar-undo-button = + .title = 撤销 +pdfjs-editor-undo-bar-undo-button-label = 撤销 +pdfjs-editor-undo-bar-close-button = + .title = 关闭 +pdfjs-editor-undo-bar-close-button-label = 关闭 diff --git a/public/assets/pdfjs/locale/zh-TW/viewer.ftl b/public/assets/pdfjs/locale/zh-TW/viewer.ftl new file mode 100644 index 0000000..bc4b7ef --- /dev/null +++ b/public/assets/pdfjs/locale/zh-TW/viewer.ftl @@ -0,0 +1,503 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +## Main toolbar buttons (tooltips and alt text for images) + +pdfjs-previous-button = + .title = 上一頁 +pdfjs-previous-button-label = 上一頁 +pdfjs-next-button = + .title = 下一頁 +pdfjs-next-button-label = 下一頁 +# .title: Tooltip for the pageNumber input. +pdfjs-page-input = + .title = 第 +# Variables: +# $pagesCount (Number) - the total number of pages in the document +# This string follows an input field with the number of the page currently displayed. +pdfjs-of-pages = 頁,共 { $pagesCount } 頁 +# Variables: +# $pageNumber (Number) - the currently visible page +# $pagesCount (Number) - the total number of pages in the document +pdfjs-page-of-pages = (第 { $pageNumber } 頁,共 { $pagesCount } 頁) +pdfjs-zoom-out-button = + .title = 縮小 +pdfjs-zoom-out-button-label = 縮小 +pdfjs-zoom-in-button = + .title = 放大 +pdfjs-zoom-in-button-label = 放大 +pdfjs-zoom-select = + .title = 縮放 +pdfjs-presentation-mode-button = + .title = 切換至簡報模式 +pdfjs-presentation-mode-button-label = 簡報模式 +pdfjs-open-file-button = + .title = 開啟檔案 +pdfjs-open-file-button-label = 開啟 +pdfjs-print-button = + .title = 列印 +pdfjs-print-button-label = 列印 +pdfjs-save-button = + .title = 儲存 +pdfjs-save-button-label = 儲存 +# Used in Firefox for Android as a tooltip for the download button (“download” is a verb). +pdfjs-download-button = + .title = 下載 +# Used in Firefox for Android as a label for the download button (“download” is a verb). +# Length of the translation matters since we are in a mobile context, with limited screen estate. +pdfjs-download-button-label = 下載 +pdfjs-bookmark-button = + .title = 目前頁面(含目前檢視頁面的網址) +pdfjs-bookmark-button-label = 目前頁面 + +## Secondary toolbar and context menu + +pdfjs-tools-button = + .title = 工具 +pdfjs-tools-button-label = 工具 +pdfjs-first-page-button = + .title = 跳到第一頁 +pdfjs-first-page-button-label = 跳到第一頁 +pdfjs-last-page-button = + .title = 跳到最後一頁 +pdfjs-last-page-button-label = 跳到最後一頁 +pdfjs-page-rotate-cw-button = + .title = 順時針旋轉 +pdfjs-page-rotate-cw-button-label = 順時針旋轉 +pdfjs-page-rotate-ccw-button = + .title = 逆時針旋轉 +pdfjs-page-rotate-ccw-button-label = 逆時針旋轉 +pdfjs-cursor-text-select-tool-button = + .title = 開啟文字選擇工具 +pdfjs-cursor-text-select-tool-button-label = 文字選擇工具 +pdfjs-cursor-hand-tool-button = + .title = 開啟頁面移動工具 +pdfjs-cursor-hand-tool-button-label = 頁面移動工具 +pdfjs-scroll-page-button = + .title = 使用單頁捲動版面 +pdfjs-scroll-page-button-label = 單頁捲動 +pdfjs-scroll-vertical-button = + .title = 使用垂直捲動版面 +pdfjs-scroll-vertical-button-label = 垂直捲動 +pdfjs-scroll-horizontal-button = + .title = 使用水平捲動版面 +pdfjs-scroll-horizontal-button-label = 水平捲動 +pdfjs-scroll-wrapped-button = + .title = 使用多頁捲動版面 +pdfjs-scroll-wrapped-button-label = 多頁捲動 +pdfjs-spread-none-button = + .title = 不要進行跨頁顯示 +pdfjs-spread-none-button-label = 不跨頁 +pdfjs-spread-odd-button = + .title = 從奇數頁開始跨頁 +pdfjs-spread-odd-button-label = 奇數跨頁 +pdfjs-spread-even-button = + .title = 從偶數頁開始跨頁 +pdfjs-spread-even-button-label = 偶數跨頁 + +## Document properties dialog + +pdfjs-document-properties-button = + .title = 文件內容… +pdfjs-document-properties-button-label = 文件內容… +pdfjs-document-properties-file-name = 檔案名稱: +pdfjs-document-properties-file-size = 檔案大小: +# Variables: +# $kb (Number) - the PDF file size in kilobytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB({ $b } 位元組) +# Variables: +# $mb (Number) - the PDF file size in megabytes +# $b (Number) - the PDF file size in bytes +pdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB({ $b } 位元組) +# Variables: +# $size_kb (Number) - the PDF file size in kilobytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-kb = { $size_kb } KB({ $size_b } 位元組) +# Variables: +# $size_mb (Number) - the PDF file size in megabytes +# $size_b (Number) - the PDF file size in bytes +pdfjs-document-properties-mb = { $size_mb } MB({ $size_b } 位元組) +pdfjs-document-properties-title = 標題: +pdfjs-document-properties-author = 作者: +pdfjs-document-properties-subject = 主旨: +pdfjs-document-properties-keywords = 關鍵字: +pdfjs-document-properties-creation-date = 建立日期: +pdfjs-document-properties-modification-date = 修改日期: +# Variables: +# $dateObj (Date) - the creation/modification date and time of the PDF file +pdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } +# Variables: +# $date (Date) - the creation/modification date of the PDF file +# $time (Time) - the creation/modification time of the PDF file +pdfjs-document-properties-date-string = { $date } { $time } +pdfjs-document-properties-creator = 建立者: +pdfjs-document-properties-producer = PDF 產生器: +pdfjs-document-properties-version = PDF 版本: +pdfjs-document-properties-page-count = 頁數: +pdfjs-document-properties-page-size = 頁面大小: +pdfjs-document-properties-page-size-unit-inches = in +pdfjs-document-properties-page-size-unit-millimeters = mm +pdfjs-document-properties-page-size-orientation-portrait = 垂直 +pdfjs-document-properties-page-size-orientation-landscape = 水平 +pdfjs-document-properties-page-size-name-a-three = A3 +pdfjs-document-properties-page-size-name-a-four = A4 +pdfjs-document-properties-page-size-name-letter = Letter +pdfjs-document-properties-page-size-name-legal = Legal + +## Variables: +## $width (Number) - the width of the (current) page +## $height (Number) - the height of the (current) page +## $unit (String) - the unit of measurement of the (current) page +## $name (String) - the name of the (current) page +## $orientation (String) - the orientation of the (current) page + +pdfjs-document-properties-page-size-dimension-string = { $width } × { $height } { $unit }({ $orientation }) +pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $height } { $unit }({ $name },{ $orientation }) + +## + +# The linearization status of the document; usually called "Fast Web View" in +# English locales of Adobe software. +pdfjs-document-properties-linearized = 快速 Web 檢視: +pdfjs-document-properties-linearized-yes = 是 +pdfjs-document-properties-linearized-no = 否 +pdfjs-document-properties-close-button = 關閉 + +## Print + +pdfjs-print-progress-message = 正在準備列印文件… +# Variables: +# $progress (Number) - percent value +pdfjs-print-progress-percent = { $progress }% +pdfjs-print-progress-close-button = 取消 +pdfjs-printing-not-supported = 警告: 此瀏覽器未完整支援列印功能。 +pdfjs-printing-not-ready = 警告: 此 PDF 未完成下載以供列印。 + +## Tooltips and alt text for side panel toolbar buttons + +pdfjs-toggle-sidebar-button = + .title = 切換側邊欄 +pdfjs-toggle-sidebar-notification-button = + .title = 切換側邊欄(包含大綱、附件、圖層的文件) +pdfjs-toggle-sidebar-button-label = 切換側邊欄 +pdfjs-document-outline-button = + .title = 顯示文件大綱(雙擊展開/摺疊所有項目) +pdfjs-document-outline-button-label = 文件大綱 +pdfjs-attachments-button = + .title = 顯示附件 +pdfjs-attachments-button-label = 附件 +pdfjs-layers-button = + .title = 顯示圖層(滑鼠雙擊即可將所有圖層重設為預設狀態) +pdfjs-layers-button-label = 圖層 +pdfjs-thumbs-button = + .title = 顯示縮圖 +pdfjs-thumbs-button-label = 縮圖 +pdfjs-current-outline-item-button = + .title = 尋找目前的大綱項目 +pdfjs-current-outline-item-button-label = 目前的大綱項目 +pdfjs-findbar-button = + .title = 在文件中尋找 +pdfjs-findbar-button-label = 尋找 +pdfjs-additional-layers = 其他圖層 + +## Thumbnails panel item (tooltip and alt text for images) + +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-title = + .title = 第 { $page } 頁 +# Variables: +# $page (Number) - the page number +pdfjs-thumb-page-canvas = + .aria-label = 第 { $page } 頁的縮圖 + +## Find panel button title and messages + +pdfjs-find-input = + .title = 尋找 + .placeholder = 在文件中搜尋… +pdfjs-find-previous-button = + .title = 尋找文字前次出現的位置 +pdfjs-find-previous-button-label = 上一個 +pdfjs-find-next-button = + .title = 尋找文字下次出現的位置 +pdfjs-find-next-button-label = 下一個 +pdfjs-find-highlight-checkbox = 強調全部 +pdfjs-find-match-case-checkbox-label = 區分大小寫 +pdfjs-find-match-diacritics-checkbox-label = 符合變音符號 +pdfjs-find-entire-word-checkbox-label = 符合整個字 +pdfjs-find-reached-top = 已搜尋至文件頂端,自底端繼續搜尋 +pdfjs-find-reached-bottom = 已搜尋至文件底端,自頂端繼續搜尋 +# Variables: +# $current (Number) - the index of the currently active find result +# $total (Number) - the total number of matches in the document +pdfjs-find-match-count = 第 { $current } 筆符合,共符合 { $total } 筆 +# Variables: +# $limit (Number) - the maximum number of matches +pdfjs-find-match-count-limit = 符合超過 { $limit } 項 +pdfjs-find-not-found = 找不到指定文字 + +## Predefined zoom values + +pdfjs-page-scale-width = 頁面寬度 +pdfjs-page-scale-fit = 縮放至頁面大小 +pdfjs-page-scale-auto = 自動縮放 +pdfjs-page-scale-actual = 實際大小 +# Variables: +# $scale (Number) - percent value for page scale +pdfjs-page-scale-percent = { $scale }% + +## PDF page + +# Variables: +# $page (Number) - the page number +pdfjs-page-landmark = + .aria-label = 第 { $page } 頁 + +## Loading indicator messages + +pdfjs-loading-error = 載入 PDF 時發生錯誤。 +pdfjs-invalid-file-error = 無效或毀損的 PDF 檔案。 +pdfjs-missing-file-error = 找不到 PDF 檔案。 +pdfjs-unexpected-response-error = 伺服器回應未預期的內容。 +pdfjs-rendering-error = 描繪頁面時發生錯誤。 + +## Annotations + +# Variables: +# $date (Date) - the modification date of the annotation +# $time (Time) - the modification time of the annotation +pdfjs-annotation-date-string = { $date } { $time } +# .alt: This is used as a tooltip. +# Variables: +# $type (String) - an annotation type from a list defined in the PDF spec +# (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +pdfjs-text-annotation-type = + .alt = [{ $type } 註解] +# Variables: +# $dateObj (Date) - the modification date and time of the annotation +pdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") } + +## Password + +pdfjs-password-label = 請輸入用來開啟此 PDF 檔案的密碼。 +pdfjs-password-invalid = 密碼不正確,請再試一次。 +pdfjs-password-ok-button = 確定 +pdfjs-password-cancel-button = 取消 +pdfjs-web-fonts-disabled = 已停用網路字型 (Web fonts): 無法使用 PDF 內嵌字型。 + +## Editing + +pdfjs-editor-free-text-button = + .title = 文字 +pdfjs-editor-free-text-button-label = 文字 +pdfjs-editor-ink-button = + .title = 繪圖 +pdfjs-editor-ink-button-label = 繪圖 +pdfjs-editor-stamp-button = + .title = 新增或編輯圖片 +pdfjs-editor-stamp-button-label = 新增或編輯圖片 +pdfjs-editor-highlight-button = + .title = 強調 +pdfjs-editor-highlight-button-label = 強調 +pdfjs-highlight-floating-button1 = + .title = 強調 + .aria-label = 強調 +pdfjs-highlight-floating-button-label = 強調 + +## Remove button for the various kind of editor. + +pdfjs-editor-remove-ink-button = + .title = 移除繪圖 +pdfjs-editor-remove-freetext-button = + .title = 移除文字 +pdfjs-editor-remove-stamp-button = + .title = 移除圖片 +pdfjs-editor-remove-highlight-button = + .title = 移除強調範圍 + +## + +# Editor Parameters +pdfjs-editor-free-text-color-input = 色彩 +pdfjs-editor-free-text-size-input = 大小 +pdfjs-editor-ink-color-input = 色彩 +pdfjs-editor-ink-thickness-input = 線條粗細 +pdfjs-editor-ink-opacity-input = 透​明度 +pdfjs-editor-stamp-add-image-button = + .title = 新增圖片 +pdfjs-editor-stamp-add-image-button-label = 新增圖片 +# This refers to the thickness of the line used for free highlighting (not bound to text) +pdfjs-editor-free-highlight-thickness-input = 線條粗細 +pdfjs-editor-free-highlight-thickness-title = + .title = 更改強調文字以外的項目時的線條粗細 +# .default-content is used as a placeholder in an empty text editor. +pdfjs-free-text2 = + .aria-label = 文字編輯器 + .default-content = 請打字… +pdfjs-free-text = + .aria-label = 文本編輯器 +pdfjs-free-text-default-content = 在此打字… +pdfjs-ink = + .aria-label = 圖形編輯器 +pdfjs-ink-canvas = + .aria-label = 使用者建立的圖片 + +## Alt-text dialog + +pdfjs-editor-alt-text-button-label = 替代文字 +pdfjs-editor-alt-text-edit-button = + .aria-label = 編輯替代文字 +pdfjs-editor-alt-text-edit-button-label = 編輯替代文字 +pdfjs-editor-alt-text-dialog-label = 挑選一種 +pdfjs-editor-alt-text-dialog-description = 替代文字可協助盲人,或於圖片無法載入時提供說明。 +pdfjs-editor-alt-text-add-description-label = 新增描述 +pdfjs-editor-alt-text-add-description-description = 用 1-2 句文字描述主題、背景或動作。 +pdfjs-editor-alt-text-mark-decorative-label = 標示為裝飾性內容 +pdfjs-editor-alt-text-mark-decorative-description = 這是裝飾性圖片,例如邊框或浮水印。 +pdfjs-editor-alt-text-cancel-button = 取消 +pdfjs-editor-alt-text-save-button = 儲存 +pdfjs-editor-alt-text-decorative-tooltip = 已標示為裝飾性內容 +# .placeholder: This is a placeholder for the alt text input area +pdfjs-editor-alt-text-textarea = + .placeholder = 例如:「有一位年輕男人坐在桌子前面吃飯」 +# Alternative text (alt text) helps when people can't see the image. +pdfjs-editor-alt-text-button = + .aria-label = 替代文字 + +## Editor resizers +## This is used in an aria label to help to understand the role of the resizer. + +pdfjs-editor-resizer-label-top-left = 左上角 — 調整大小 +pdfjs-editor-resizer-label-top-middle = 頂部中間 — 調整大小 +pdfjs-editor-resizer-label-top-right = 右上角 — 調整大小 +pdfjs-editor-resizer-label-middle-right = 中間右方 — 調整大小 +pdfjs-editor-resizer-label-bottom-right = 右下角 — 調整大小 +pdfjs-editor-resizer-label-bottom-middle = 底部中間 — 調整大小 +pdfjs-editor-resizer-label-bottom-left = 左下角 — 調整大小 +pdfjs-editor-resizer-label-middle-left = 中間左方 — 調整大小 +pdfjs-editor-resizer-top-left = + .aria-label = 左上角 — 調整大小 +pdfjs-editor-resizer-top-middle = + .aria-label = 頂部中間 — 調整大小 +pdfjs-editor-resizer-top-right = + .aria-label = 右上角 — 調整大小 +pdfjs-editor-resizer-middle-right = + .aria-label = 中間右方 — 調整大小 +pdfjs-editor-resizer-bottom-right = + .aria-label = 右下角 — 調整大小 +pdfjs-editor-resizer-bottom-middle = + .aria-label = 底部中間 — 調整大小 +pdfjs-editor-resizer-bottom-left = + .aria-label = 左下角 — 調整大小 +pdfjs-editor-resizer-middle-left = + .aria-label = 中間左方 — 調整大小 + +## Color picker + +# This means "Color used to highlight text" +pdfjs-editor-highlight-colorpicker-label = 強調色彩 +pdfjs-editor-colorpicker-button = + .title = 更改色彩 +pdfjs-editor-colorpicker-dropdown = + .aria-label = 色彩選項 +pdfjs-editor-colorpicker-yellow = + .title = 黃色 +pdfjs-editor-colorpicker-green = + .title = 綠色 +pdfjs-editor-colorpicker-blue = + .title = 藍色 +pdfjs-editor-colorpicker-pink = + .title = 粉紅色 +pdfjs-editor-colorpicker-red = + .title = 紅色 + +## Show all highlights +## This is a toggle button to show/hide all the highlights. + +pdfjs-editor-highlight-show-all-button-label = 顯示全部 +pdfjs-editor-highlight-show-all-button = + .title = 顯示全部 + +## New alt-text dialog +## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy. + +# Modal header positioned above a text box where users can edit the alt text. +pdfjs-editor-new-alt-text-dialog-edit-label = 編輯替代文字(圖片描述) +# Modal header positioned above a text box where users can add the alt text. +pdfjs-editor-new-alt-text-dialog-add-label = 新增替代文字(圖片描述) +pdfjs-editor-new-alt-text-textarea = + .placeholder = 在此寫下您的描述文字… +# This text refers to the alt text box above this description. It offers a definition of alt text. +pdfjs-editor-new-alt-text-description = 為看不到圖片的讀者,或圖片無法載入時顯示的簡短描述。 +# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human. +pdfjs-editor-new-alt-text-disclaimer1 = 此替代文字是自動產生的,可能不夠精確。 +pdfjs-editor-new-alt-text-disclaimer-learn-more-url = 更多資訊 +pdfjs-editor-new-alt-text-create-automatically-button-label = 自動產生替代文字 +pdfjs-editor-new-alt-text-not-now-button = 暫時不要 +pdfjs-editor-new-alt-text-error-title = 無法自動產生替代文字 +pdfjs-editor-new-alt-text-error-description = 請自行填寫替代文字,或稍後再試一次。 +pdfjs-editor-new-alt-text-error-close-button = 關閉 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +# $downloadedSize (Number) - the downloaded size (in MB) of the AI model. +# $percent (Number) - the percentage of the downloaded size. +pdfjs-editor-new-alt-text-ai-model-downloading-progress = 正在下載替代文字 AI 模型({ $downloadedSize } / { $totalSize } MB) + .aria-valuetext = 正在下載替代文字 AI 模型({ $downloadedSize } / { $totalSize } MB) +# This is a button that users can click to edit the alt text they have already added. +pdfjs-editor-new-alt-text-added-button = + .aria-label = 已新增替代文字 +pdfjs-editor-new-alt-text-added-button-label = 已新增替代文字 +# This is a button that users can click to open the alt text editor and add alt text when it is not present. +pdfjs-editor-new-alt-text-missing-button = + .aria-label = 缺少替代文字 +pdfjs-editor-new-alt-text-missing-button-label = 缺少替代文字 +# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated. +pdfjs-editor-new-alt-text-to-review-button = + .aria-label = 確認替代文字 +pdfjs-editor-new-alt-text-to-review-button-label = 確認替代文字 +# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear. +# Variables: +# $generatedAltText (String) - the generated alt-text. +pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = 自動產生:{ $generatedAltText } + +## Image alt-text settings + +pdfjs-image-alt-text-settings-button = + .title = 圖片替代文字設定 +pdfjs-image-alt-text-settings-button-label = 圖片替代文字設定 +pdfjs-editor-alt-text-settings-dialog-label = 圖片替代文字設定 +pdfjs-editor-alt-text-settings-automatic-title = 自動化替代文字 +pdfjs-editor-alt-text-settings-create-model-button-label = 自動產生替代文字 +pdfjs-editor-alt-text-settings-create-model-description = 為您建議圖片描述,幫助看不到圖片的讀者,或於圖片無法載入時顯示。 +# Variables: +# $totalSize (Number) - the total size (in MB) of the AI model. +pdfjs-editor-alt-text-settings-download-model-label = 替代文字 AI 模型({ $totalSize } MB) +pdfjs-editor-alt-text-settings-ai-model-description = 在您的本機裝置上運作,以確保您的資料隱私。必須下載此模型才可以自動產生替代文字。 +pdfjs-editor-alt-text-settings-delete-model-button = 刪除 +pdfjs-editor-alt-text-settings-download-model-button = 下載 +pdfjs-editor-alt-text-settings-downloading-model-button = 下載中… +pdfjs-editor-alt-text-settings-editor-title = 替代文字編輯器 +pdfjs-editor-alt-text-settings-show-dialog-button-label = 新增圖片後立即顯示替代文字編輯器 +pdfjs-editor-alt-text-settings-show-dialog-description = 幫助您確保所有圖片都有替代文字。 +pdfjs-editor-alt-text-settings-close-button = 關閉 + +## "Annotations removed" bar + +pdfjs-editor-undo-bar-message-highlight = 已移除強調 +pdfjs-editor-undo-bar-message-freetext = 已移除文字 +pdfjs-editor-undo-bar-message-ink = 已移除繪圖 +pdfjs-editor-undo-bar-message-stamp = 已移除圖片 +# Variables: +# $count (Number) - the number of removed annotations. +pdfjs-editor-undo-bar-message-multiple = 已移除 { $count } 筆註解 +pdfjs-editor-undo-bar-undo-button = + .title = 還原 +pdfjs-editor-undo-bar-undo-button-label = 還原 +pdfjs-editor-undo-bar-close-button = + .title = 關閉 +pdfjs-editor-undo-bar-close-button-label = 關閉 diff --git a/public/assets/pdfjs/standard_fonts/FoxitDingbats.pfb b/public/assets/pdfjs/standard_fonts/FoxitDingbats.pfb new file mode 100644 index 0000000..30d5296 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitDingbats.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitFixed.pfb b/public/assets/pdfjs/standard_fonts/FoxitFixed.pfb new file mode 100644 index 0000000..f12dcbc Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitFixed.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitFixedBold.pfb b/public/assets/pdfjs/standard_fonts/FoxitFixedBold.pfb new file mode 100644 index 0000000..cf8e24a Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitFixedBold.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb b/public/assets/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb new file mode 100644 index 0000000..d288001 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitFixedItalic.pfb b/public/assets/pdfjs/standard_fonts/FoxitFixedItalic.pfb new file mode 100644 index 0000000..d71697d Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitFixedItalic.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitSerif.pfb b/public/assets/pdfjs/standard_fonts/FoxitSerif.pfb new file mode 100644 index 0000000..3fa682e Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitSerif.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitSerifBold.pfb b/public/assets/pdfjs/standard_fonts/FoxitSerifBold.pfb new file mode 100644 index 0000000..ff7c6dd Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitSerifBold.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb b/public/assets/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb new file mode 100644 index 0000000..460231f Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitSerifItalic.pfb b/public/assets/pdfjs/standard_fonts/FoxitSerifItalic.pfb new file mode 100644 index 0000000..d03a7c7 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitSerifItalic.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/FoxitSymbol.pfb b/public/assets/pdfjs/standard_fonts/FoxitSymbol.pfb new file mode 100644 index 0000000..c8f9bca Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/FoxitSymbol.pfb differ diff --git a/public/assets/pdfjs/standard_fonts/LICENSE_FOXIT b/public/assets/pdfjs/standard_fonts/LICENSE_FOXIT new file mode 100644 index 0000000..8b4ed6d --- /dev/null +++ b/public/assets/pdfjs/standard_fonts/LICENSE_FOXIT @@ -0,0 +1,27 @@ +// Copyright 2014 PDFium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/assets/pdfjs/standard_fonts/LICENSE_LIBERATION b/public/assets/pdfjs/standard_fonts/LICENSE_LIBERATION new file mode 100644 index 0000000..aba73e8 --- /dev/null +++ b/public/assets/pdfjs/standard_fonts/LICENSE_LIBERATION @@ -0,0 +1,102 @@ +Digitized data copyright (c) 2010 Google Corporation + with Reserved Font Arimo, Tinos and Cousine. +Copyright (c) 2012 Red Hat, Inc. + with Reserved Font Name Liberation. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE The goals of the Open Font License (OFL) are to stimulate +worldwide development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to provide +a free and open framework in which fonts may be shared and improved in +partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. +The fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + + + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. +This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components +as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting ? in part or in whole ? +any of the components of the Original Version, by changing formats or +by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer +or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components,in + Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the + corresponding Copyright Holder. This restriction only applies to the + primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + +5) The Font Software, modified or unmodified, in part or in whole, must + be distributed entirely under this license, and must not be distributed + under any other license. The requirement for fonts to remain under + this license does not apply to any document created using the Font + Software. + + + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + + + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + diff --git a/public/assets/pdfjs/standard_fonts/LiberationSans-Bold.ttf b/public/assets/pdfjs/standard_fonts/LiberationSans-Bold.ttf new file mode 100644 index 0000000..ee23715 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/LiberationSans-Bold.ttf differ diff --git a/public/assets/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf b/public/assets/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf new file mode 100644 index 0000000..42b5717 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf differ diff --git a/public/assets/pdfjs/standard_fonts/LiberationSans-Italic.ttf b/public/assets/pdfjs/standard_fonts/LiberationSans-Italic.ttf new file mode 100644 index 0000000..0cf6126 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/LiberationSans-Italic.ttf differ diff --git a/public/assets/pdfjs/standard_fonts/LiberationSans-Regular.ttf b/public/assets/pdfjs/standard_fonts/LiberationSans-Regular.ttf new file mode 100644 index 0000000..366d148 Binary files /dev/null and b/public/assets/pdfjs/standard_fonts/LiberationSans-Regular.ttf differ diff --git a/public/assets/pdfjs/viewer.css b/public/assets/pdfjs/viewer.css new file mode 100644 index 0000000..b6df80b --- /dev/null +++ b/public/assets/pdfjs/viewer.css @@ -0,0 +1,6068 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.messageBar{ + --closing-button-icon:url(images/messageBar_closingButton.svg); + --message-bar-close-button-color:var(--text-primary-color); + --message-bar-close-button-color-hover:var(--text-primary-color); + --message-bar-close-button-border-radius:4px; + --message-bar-close-button-border:none; + --message-bar-close-button-hover-bg-color:rgb(21 20 26 / 0.14); + --message-bar-close-button-active-bg-color:rgb(21 20 26 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(21 20 26 / 0.07); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .messageBar{ + --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); + } +} + +:where(html.is-dark) .messageBar{ + --message-bar-close-button-hover-bg-color:rgb(251 251 254 / 0.14); + --message-bar-close-button-active-bg-color:rgb(251 251 254 / 0.21); + --message-bar-close-button-focus-bg-color:rgb(251 251 254 / 0.07); +} + +@media screen and (forced-colors: active){ + + .messageBar{ + --message-bar-close-button-color:ButtonText; + --message-bar-close-button-border:1px solid ButtonText; + --message-bar-close-button-hover-bg-color:ButtonText; + --message-bar-close-button-active-bg-color:ButtonText; + --message-bar-close-button-focus-bg-color:ButtonText; + --message-bar-close-button-color-hover:HighlightText; + } +} + +.messageBar{ + + display:flex; + position:relative; + padding:8px 8px 8px 16px; + flex-direction:column; + justify-content:center; + align-items:center; + gap:8px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + + border-radius:4px; + + border:1px solid var(--message-bar-border-color); + background:var(--message-bar-bg-color); + color:var(--message-bar-fg-color); +} + +.messageBar > div{ + display:flex; + align-items:flex-start; + gap:8px; + align-self:stretch; +} + +:is(.messageBar > div)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--message-bar-icon); + mask-image:var(--message-bar-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-icon-color); + flex-shrink:0; +} + +.messageBar button{ + cursor:pointer; +} + +:is(.messageBar button):focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; +} + +.messageBar .closeButton{ + width:32px; + height:32px; + background:none; + border-radius:var(--message-bar-close-button-border-radius); + border:var(--message-bar-close-button-border); + + display:flex; + align-items:center; + justify-content:center; +} + +:is(.messageBar .closeButton)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--closing-button-icon); + mask-image:var(--closing-button-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--message-bar-close-button-color); +} + +:is(.messageBar .closeButton):is(:hover,:active,:focus)::before{ + background-color:var(--message-bar-close-button-color-hover); +} + +:is(.messageBar .closeButton):hover{ + background-color:var(--message-bar-close-button-hover-bg-color); +} + +:is(.messageBar .closeButton):active{ + background-color:var(--message-bar-close-button-active-bg-color); +} + +:is(.messageBar .closeButton):focus{ + background-color:var(--message-bar-close-button-focus-bg-color); +} + +:is(.messageBar .closeButton) > span{ + display:inline-block; + width:0; + height:0; + overflow:hidden; +} + +#editorUndoBar{ + --text-primary-color:#15141a; + + --message-bar-icon:url(images/secondaryToolbarButton-documentProperties.svg); + --message-bar-icon-color:#0060df; + --message-bar-bg-color:#deeafc; + --message-bar-fg-color:var(--text-primary-color); + --message-bar-border-color:rgb(0 0 0 / 0.08); + + --undo-button-bg-color:rgb(21 20 26 / 0.07); + --undo-button-bg-color-hover:rgb(21 20 26 / 0.14); + --undo-button-bg-color-active:rgb(21 20 26 / 0.21); + + --undo-button-fg-color:var(--message-bar-fg-color); + --undo-button-fg-color-hover:var(--undo-button-fg-color); + --undo-button-fg-color-active:var(--undo-button-fg-color); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) #editorUndoBar{ + --text-primary-color:#fbfbfe; + + --message-bar-icon-color:#73a7f3; + --message-bar-bg-color:#003070; + --message-bar-border-color:rgb(255 255 255 / 0.08); + + --undo-button-bg-color:rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); + --undo-button-bg-color-active:rgb(255 255 255 / 0.21); + } +} + +:where(html.is-dark) #editorUndoBar{ + --text-primary-color:#fbfbfe; + + --message-bar-icon-color:#73a7f3; + --message-bar-bg-color:#003070; + --message-bar-border-color:rgb(255 255 255 / 0.08); + + --undo-button-bg-color:rgb(255 255 255 / 0.08); + --undo-button-bg-color-hover:rgb(255 255 255 / 0.14); + --undo-button-bg-color-active:rgb(255 255 255 / 0.21); +} + +@media screen and (forced-colors: active){ + + #editorUndoBar{ + --text-primary-color:CanvasText; + + --message-bar-icon-color:CanvasText; + --message-bar-bg-color:Canvas; + --message-bar-border-color:CanvasText; + + --undo-button-bg-color:ButtonText; + --undo-button-bg-color-hover:SelectedItem; + --undo-button-bg-color-active:SelectedItem; + + --undo-button-fg-color:ButtonFace; + --undo-button-fg-color-hover:SelectedItemText; + --undo-button-fg-color-active:SelectedItemText; + } +} + +#editorUndoBar{ + + position:fixed; + top:50px; + left:50%; + transform:translateX(-50%); + z-index:10; + + padding-block:8px; + padding-inline:16px 8px; + + font:menu; + font-size:15px; + + cursor:default; +} + +#editorUndoBar button{ + cursor:pointer; +} + +#editorUndoBar #editorUndoBarUndoButton{ + border-radius:4px; + font-weight:590; + line-height:19.5px; + color:var(--undo-button-fg-color); + border:none; + padding:4px 16px; + margin-inline-start:8px; + height:32px; + + background-color:var(--undo-button-bg-color); +} + +:is(#editorUndoBar #editorUndoBarUndoButton):hover{ + background-color:var(--undo-button-bg-color-hover); + color:var(--undo-button-fg-color-hover); +} + +:is(#editorUndoBar #editorUndoBarUndoButton):active{ + background-color:var(--undo-button-bg-color-active); + color:var(--undo-button-fg-color-active); +} + +#editorUndoBar > div{ + align-items:center; +} + +.dialog{ + --dialog-bg-color:white; + --dialog-border-color:white; + --dialog-shadow:0 2px 14px 0 rgb(58 57 68 / 0.2); + --text-primary-color:#15141a; + --text-secondary-color:#5b5b66; + --hover-filter:brightness(0.9); + --link-fg-color:#0060df; + --link-hover-fg-color:#0250bb; + --separator-color:#f0f0f4; + + --textarea-border-color:#8f8f9d; + --textarea-bg-color:white; + --textarea-fg-color:var(--text-secondary-color); + + --radio-bg-color:#f0f0f4; + --radio-checked-bg-color:#fbfbfe; + --radio-border-color:#8f8f9d; + --radio-checked-border-color:#0060df; + + --button-secondary-bg-color:#f0f0f4; + --button-secondary-fg-color:var(--text-primary-color); + --button-secondary-border-color:var(--button-secondary-bg-color); + --button-secondary-hover-bg-color:var(--button-secondary-bg-color); + --button-secondary-hover-fg-color:var(--button-secondary-fg-color); + --button-secondary-hover-border-color:var(--button-secondary-hover-bg-color); + + --button-primary-bg-color:#0060df; + --button-primary-fg-color:#fbfbfe; + --button-primary-border-color:var(--button-primary-bg-color); + --button-primary-hover-bg-color:var(--button-primary-bg-color); + --button-primary-hover-fg-color:var(--button-primary-fg-color); + --button-primary-hover-border-color:var(--button-primary-hover-bg-color); + + --button-disabled-bg-color:color-mix(in srgb, currentcolor, transparent 60%); + --button-disabled-fg-color:var(--button-disabled-bg-color); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .dialog{ + --dialog-bg-color:#1c1b22; + --dialog-border-color:#1c1b22; + --dialog-shadow:0 2px 14px 0 #15141a; + --text-primary-color:#fbfbfe; + --text-secondary-color:#cfcfd8; + --hover-filter:brightness(1.4); + --link-fg-color:#0df; + --link-hover-fg-color:#80ebff; + --separator-color:#52525e; + + --textarea-bg-color:#42414d; + + --radio-bg-color:#2b2a33; + --radio-checked-bg-color:#15141a; + --radio-checked-border-color:#0df; + + --button-secondary-bg-color:#2b2a33; + --button-primary-bg-color:#0df; + --button-primary-fg-color:#15141a; + } +} + +:where(html.is-dark) .dialog{ + --dialog-bg-color:#1c1b22; + --dialog-border-color:#1c1b22; + --dialog-shadow:0 2px 14px 0 #15141a; + --text-primary-color:#fbfbfe; + --text-secondary-color:#cfcfd8; + --hover-filter:brightness(1.4); + --link-fg-color:#0df; + --link-hover-fg-color:#80ebff; + --separator-color:#52525e; + + --textarea-bg-color:#42414d; + + --radio-bg-color:#2b2a33; + --radio-checked-bg-color:#15141a; + --radio-checked-border-color:#0df; + + --button-secondary-bg-color:#2b2a33; + --button-primary-bg-color:#0df; + --button-primary-fg-color:#15141a; +} + +@media screen and (forced-colors: active){ + + .dialog{ + --dialog-bg-color:Canvas; + --dialog-border-color:CanvasText; + --dialog-shadow:none; + --text-primary-color:CanvasText; + --text-secondary-color:CanvasText; + --hover-filter:none; + --link-fg-color:LinkText; + --link-hover-fg-color:LinkText; + --separator-color:CanvasText; + + --textarea-border-color:ButtonBorder; + --textarea-bg-color:Field; + --textarea-fg-color:ButtonText; + + --radio-bg-color:ButtonFace; + --radio-checked-bg-color:ButtonFace; + --radio-border-color:ButtonText; + --radio-checked-border-color:ButtonText; + + --button-secondary-bg-color:ButtonFace; + --button-secondary-fg-color:ButtonText; + --button-secondary-border-color:ButtonText; + --button-secondary-hover-bg-color:AccentColor; + --button-secondary-hover-fg-color:AccentColorText; + + --button-primary-bg-color:ButtonText; + --button-primary-fg-color:ButtonFace; + --button-primary-hover-bg-color:AccentColor; + --button-primary-hover-fg-color:AccentColorText; + + --button-disabled-bg-color:GrayText; + --button-disabled-fg-color:ButtonFace; + } +} + +.dialog{ + + font:message-box; + font-size:13px; + font-weight:400; + line-height:150%; + border-radius:4px; + padding:12px 16px; + border:1px solid var(--dialog-border-color); + background:var(--dialog-bg-color); + color:var(--text-primary-color); + box-shadow:var(--dialog-shadow); +} + +:is(.dialog .mainContainer) *:focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; +} + +:is(.dialog .mainContainer) .title{ + display:flex; + width:auto; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; +} + +:is(:is(.dialog .mainContainer) .title) > span{ + font-size:13px; + font-style:normal; + font-weight:590; + line-height:150%; +} + +:is(.dialog .mainContainer) .dialogSeparator{ + width:100%; + height:0; + margin-block:4px; + border-top:1px solid var(--separator-color); + border-bottom:none; +} + +:is(.dialog .mainContainer) .dialogButtonsGroup{ + display:flex; + gap:12px; + align-self:flex-end; +} + +:is(.dialog .mainContainer) .radio{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; +} + +:is(:is(.dialog .mainContainer) .radio) > .radioButton{ + display:flex; + gap:8px; + align-self:stretch; + align-items:center; +} + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + box-sizing:border-box; + width:16px; + height:16px; + border-radius:50%; + background-color:var(--radio-bg-color); + border:1px solid var(--radio-border-color); +} + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):hover{ + filter:var(--hover-filter); +} + +:is(:is(:is(:is(.dialog .mainContainer) .radio) > .radioButton) input):checked{ + background-color:var(--radio-checked-bg-color); + border:4px solid var(--radio-checked-border-color); +} + +:is(:is(.dialog .mainContainer) .radio) > .radioLabel{ + display:flex; + padding-inline-start:24px; + align-items:flex-start; + gap:10px; + align-self:stretch; +} + +:is(:is(:is(.dialog .mainContainer) .radio) > .radioLabel) > span{ + flex:1 0 0; + font-size:11px; + color:var(--text-secondary-color); +} + +:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton)){ + border-radius:4px; + border:1px solid; + font:menu; + font-weight:600; + padding:4px 16px; + width:auto; + height:32px; +} + +:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))):hover{ + cursor:pointer; + filter:var(--hover-filter); +} + +:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))) > span{ + color:inherit; +} + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))){ + color:var(--button-secondary-fg-color); + background-color:var(--button-secondary-bg-color); + border-color:var(--button-secondary-border-color); +} + +.secondaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))):hover{ + color:var(--button-secondary-hover-fg-color); + background-color:var(--button-secondary-hover-bg-color); + border-color:var(--button-secondary-hover-border-color); +} + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))){ + color:var(--button-primary-fg-color); + background-color:var(--button-primary-bg-color); + border-color:var(--button-primary-border-color); + opacity:1; +} + +.primaryButton:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))):hover{ + color:var(--button-primary-hover-fg-color); + background-color:var(--button-primary-hover-bg-color); + border-color:var(--button-primary-hover-border-color); +} + +:is(:is(.dialog .mainContainer) button:not(:is(.toggle-button,.closeButton,.clearInputButton))):disabled{ + color:var(--button-disabled-fg-color) !important; + background-color:var(--button-disabled-bg-color); + border-color:var(--button-disabled-bg-color); + pointer-events:none; +} + +:is(.dialog .mainContainer) a{ + color:var(--link-fg-color); +} + +:is(:is(.dialog .mainContainer) a):hover{ + color:var(--link-hover-fg-color); +} + +:is(.dialog .mainContainer) textarea{ + font:inherit; + padding:8px; + resize:none; + margin:0; + box-sizing:border-box; + border-radius:4px; + border:1px solid var(--textarea-border-color); + background:var(--textarea-bg-color); + color:var(--textarea-fg-color); +} + +:is(:is(.dialog .mainContainer) textarea):focus{ + outline-offset:0; + border-color:transparent; +} + +:is(:is(.dialog .mainContainer) textarea):disabled{ + pointer-events:none; + opacity:0.4; +} + +:is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#ffebcd; + --message-bar-fg-color:#15141a; + --message-bar-border-color:rgb(0 0 0 / 0.08); + --message-bar-icon:url(images/messageBar_warning.svg); + --message-bar-icon-color:#cd411e; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) :is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#5a3100; + --message-bar-fg-color:#fbfbfe; + --message-bar-border-color:rgb(255 255 255 / 0.08); + --message-bar-icon-color:#e49c49; + } +} + +:where(html.is-dark) :is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:#5a3100; + --message-bar-fg-color:#fbfbfe; + --message-bar-border-color:rgb(255 255 255 / 0.08); + --message-bar-icon-color:#e49c49; +} + +@media screen and (forced-colors: active){ + + :is(.dialog .mainContainer) .messageBar{ + --message-bar-bg-color:HighlightText; + --message-bar-fg-color:CanvasText; + --message-bar-border-color:CanvasText; + --message-bar-icon-color:CanvasText; + } +} + +:is(.dialog .mainContainer) .messageBar{ + + align-self:stretch; +} + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div)::before,:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + margin-block:4px; +} + +:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + flex:1 0 0; +} + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .title{ + font-size:13px; + font-weight:590; +} + +:is(:is(:is(:is(.dialog .mainContainer) .messageBar) > div) > div) .description{ + font-size:13px; +} + +:is(.dialog .mainContainer) .toggler{ + display:flex; + align-items:center; + gap:8px; + align-self:stretch; +} + +:is(:is(.dialog .mainContainer) .toggler) > .togglerLabel{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.textLayer{ + position:absolute; + text-align:initial; + inset:0; + overflow:clip; + opacity:1; + line-height:1; + -webkit-text-size-adjust:none; + -moz-text-size-adjust:none; + text-size-adjust:none; + forced-color-adjust:none; + transform-origin:0 0; + caret-color:CanvasText; + z-index:0; +} + +.textLayer.highlighting{ + touch-action:none; +} + +.textLayer :is(span,br){ + color:transparent; + position:absolute; + white-space:pre; + cursor:text; + transform-origin:0% 0%; +} + +.textLayer > :not(.markedContent),.textLayer .markedContent span:not(.markedContent){ + z-index:1; +} + +.textLayer span.markedContent{ + top:0; + height:0; +} + +.textLayer span[role="img"]{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; +} + +.textLayer .highlight{ + --highlight-bg-color:rgb(180 0 170 / 0.25); + --highlight-selected-bg-color:rgb(0 100 0 / 0.25); + --highlight-backdrop-filter:none; + --highlight-selected-backdrop-filter:none; +} + +@media screen and (forced-colors: active){ + + .textLayer .highlight{ + --highlight-bg-color:transparent; + --highlight-selected-bg-color:transparent; + --highlight-backdrop-filter:var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter:var( + --hcm-highlight-selected-filter + ); + } +} + +.textLayer .highlight{ + + margin:-1px; + padding:1px; + background-color:var(--highlight-bg-color); + -webkit-backdrop-filter:var(--highlight-backdrop-filter); + backdrop-filter:var(--highlight-backdrop-filter); + border-radius:4px; +} + +.appended:is(.textLayer .highlight){ + position:initial; +} + +.begin:is(.textLayer .highlight){ + border-radius:4px 0 0 4px; +} + +.end:is(.textLayer .highlight){ + border-radius:0 4px 4px 0; +} + +.middle:is(.textLayer .highlight){ + border-radius:0; +} + +.selected:is(.textLayer .highlight){ + background-color:var(--highlight-selected-bg-color); + -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); + backdrop-filter:var(--highlight-selected-backdrop-filter); +} + +.textLayer ::-moz-selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer ::selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} + +.textLayer br::-moz-selection{ + background:transparent; +} + +.textLayer br::selection{ + background:transparent; +} + +.textLayer .endOfContent{ + display:block; + position:absolute; + inset:100% 0 0; + z-index:0; + cursor:default; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.textLayer.selecting .endOfContent{ + top:0; +} + +.annotationLayer{ + --annotation-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color:Highlight; + --input-focus-outline:1px solid Canvas; + --input-unfocused-border-color:transparent; + --input-disabled-border-color:transparent; + --input-hover-border-color:black; + --link-outline:none; +} + +@media screen and (forced-colors: active){ + + .annotationLayer{ + --input-focus-border-color:CanvasText; + --input-unfocused-border-color:ActiveText; + --input-disabled-border-color:GrayText; + --input-hover-border-color:Highlight; + --link-outline:1.5px solid LinkText; + } + + .annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid selectedItem; + } + + .annotationLayer .linkAnnotation{ + outline:var(--link-outline); + } + + :is(.annotationLayer .linkAnnotation):hover{ + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + } + + :is(.annotationLayer .linkAnnotation) > a:hover{ + opacity:0 !important; + background:none !important; + box-shadow:none; + } + + .annotationLayer .popupAnnotation .popup{ + outline:calc(1.5px * var(--total-scale-factor)) solid CanvasText !important; + background-color:ButtonFace !important; + color:ButtonText !important; + } + + .annotationLayer .highlightArea:hover::after{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + content:""; + pointer-events:none; + } + + .annotationLayer .popupAnnotation.focused .popup{ + outline:calc(3px * var(--total-scale-factor)) solid Highlight !important; + } +} + +.annotationLayer{ + + position:absolute; + top:0; + left:0; + pointer-events:none; + transform-origin:0 0; +} + +.annotationLayer[data-main-rotation="90"] .norotate{ + transform:rotate(270deg) translateX(-100%); +} + +.annotationLayer[data-main-rotation="180"] .norotate{ + transform:rotate(180deg) translate(-100%, -100%); +} + +.annotationLayer[data-main-rotation="270"] .norotate{ + transform:rotate(90deg) translateY(-100%); +} + +.annotationLayer.disabled section,.annotationLayer.disabled .popup{ + pointer-events:none; +} + +.annotationLayer .annotationContent{ + position:absolute; + width:100%; + height:100%; + pointer-events:none; +} + +.freetext:is(.annotationLayer .annotationContent){ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:1.35; +} + +.annotationLayer section{ + position:absolute; + text-align:initial; + pointer-events:auto; + box-sizing:border-box; + transform-origin:0 0; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +:is(.annotationLayer section):has(div.annotationContent) canvas.annotationContent{ + display:none; +} + +.textLayer.selecting ~ .annotationLayer section{ + pointer-events:none; +} + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton) > a{ + position:absolute; + font-size:1em; + top:0; + left:0; + width:100%; + height:100%; +} + +.annotationLayer :is(.linkAnnotation,.buttonWidgetAnnotation.pushButton):not(.hasBorder) > a:hover{ + opacity:0.2; + background-color:rgb(255 255 0); + box-shadow:0 2px 10px rgb(255 255 0); +} + +.annotationLayer .linkAnnotation.hasBorder:hover{ + background-color:rgb(255 255 0 / 0.2); +} + +.annotationLayer .hasBorder{ + background-size:100% 100%; +} + +.annotationLayer .textAnnotation img{ + position:absolute; + cursor:pointer; + width:100%; + height:100%; + top:0; + left:0; +} + +.annotationLayer .textWidgetAnnotation :is(input,textarea),.annotationLayer .choiceWidgetAnnotation select,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + background-image:var(--annotation-unfocused-field-background); + border:2px solid var(--input-unfocused-border-color); + box-sizing:border-box; + font:calc(9px * var(--total-scale-factor)) sans-serif; + height:100%; + margin:0; + vertical-align:top; + width:100%; +} + +.annotationLayer .textWidgetAnnotation :is(input,textarea):required,.annotationLayer .choiceWidgetAnnotation select:required,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:required{ + outline:1.5px solid red; +} + +.annotationLayer .choiceWidgetAnnotation select option{ + padding:0; +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input{ + border-radius:50%; +} + +.annotationLayer .textWidgetAnnotation textarea{ + resize:none; +} + +.annotationLayer .textWidgetAnnotation [disabled]:is(input,textarea),.annotationLayer .choiceWidgetAnnotation select[disabled],.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input[disabled]{ + background:none; + border:2px solid var(--input-disabled-border-color); + cursor:not-allowed; +} + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input:hover{ + border:2px solid var(--input-hover-border-color); +} + +.annotationLayer .textWidgetAnnotation :is(input,textarea):hover,.annotationLayer .choiceWidgetAnnotation select:hover,.annotationLayer .buttonWidgetAnnotation.checkBox input:hover{ + border-radius:2px; +} + +.annotationLayer .textWidgetAnnotation :is(input,textarea):focus,.annotationLayer .choiceWidgetAnnotation select:focus{ + background:none; + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) :focus{ + background-image:none; + background-color:transparent; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox :focus{ + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton :focus{ + border:2px solid var(--input-focus-border-color); + outline:var(--input-focus-outline); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after,.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + background-color:CanvasText; + content:""; + display:block; + position:absolute; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + height:80%; + left:45%; + width:1px; +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before{ + transform:rotate(45deg); +} + +.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + transform:rotate(-45deg); +} + +.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + border-radius:50%; + height:50%; + left:25%; + top:25%; + width:50%; +} + +.annotationLayer .textWidgetAnnotation input.comb{ + font-family:monospace; + padding-left:2px; + padding-right:0; +} + +.annotationLayer .textWidgetAnnotation input.comb:focus{ + width:103%; +} + +.annotationLayer .buttonWidgetAnnotation:is(.checkBox,.radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; +} + +.annotationLayer .fileAttachmentAnnotation .popupTriggerArea{ + height:100%; + width:100%; +} + +.annotationLayer .popupAnnotation{ + position:absolute; + font-size:calc(9px * var(--total-scale-factor)); + pointer-events:none; + width:-moz-max-content; + width:max-content; + max-width:45%; + height:auto; +} + +.annotationLayer .popup{ + background-color:rgb(255 255 153); + box-shadow:0 calc(2px * var(--total-scale-factor)) calc(5px * var(--total-scale-factor)) rgb(136 136 136); + border-radius:calc(2px * var(--total-scale-factor)); + outline:1.5px solid rgb(255 255 74); + padding:calc(6px * var(--total-scale-factor)); + cursor:pointer; + font:message-box; + white-space:normal; + word-wrap:break-word; + pointer-events:auto; + -webkit-user-select:text; + -moz-user-select:text; + user-select:text; +} + +.annotationLayer .popupAnnotation.focused .popup{ + outline-width:3px; +} + +.annotationLayer .popup *{ + font-size:calc(9px * var(--total-scale-factor)); +} + +.annotationLayer .popup > .header{ + display:inline-block; +} + +.annotationLayer .popup > .header h1{ + display:inline; +} + +.annotationLayer .popup > .header .popupDate{ + display:inline-block; + margin-left:calc(5px * var(--total-scale-factor)); + width:-moz-fit-content; + width:fit-content; +} + +.annotationLayer .popupContent{ + border-top:1px solid rgb(51 51 51); + margin-top:calc(2px * var(--total-scale-factor)); + padding-top:calc(2px * var(--total-scale-factor)); +} + +.annotationLayer .richText > *{ + white-space:pre-wrap; + font-size:calc(9px * var(--total-scale-factor)); +} + +.annotationLayer .popupTriggerArea{ + cursor:pointer; +} + +.annotationLayer section svg{ + position:absolute; + width:100%; + height:100%; + top:0; + left:0; +} + +.annotationLayer .annotationTextContent{ + position:absolute; + width:100%; + height:100%; + opacity:0; + color:transparent; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + pointer-events:none; +} + +:is(.annotationLayer .annotationTextContent) span{ + width:100%; + display:inline-block; +} + +.annotationLayer svg.quadrilateralsContainer{ + contain:strict; + width:0; + height:0; + position:absolute; + top:0; + left:0; + z-index:-1; +} + +:root{ + --xfa-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --xfa-focus-outline:auto; +} + +@media screen and (forced-colors: active){ + :root{ + --xfa-focus-outline:2px solid CanvasText; + } + .xfaLayer *:required{ + outline:1.5px solid selectedItem; + } +} + +.xfaLayer{ + background-color:transparent; +} + +.xfaLayer .highlight{ + margin:-1px; + padding:1px; + background-color:rgb(239 203 237); + border-radius:4px; +} + +.xfaLayer .highlight.appended{ + position:initial; +} + +.xfaLayer .highlight.begin{ + border-radius:4px 0 0 4px; +} + +.xfaLayer .highlight.end{ + border-radius:0 4px 4px 0; +} + +.xfaLayer .highlight.middle{ + border-radius:0; +} + +.xfaLayer .highlight.selected{ + background-color:rgb(203 223 203); +} + +.xfaPage{ + overflow:hidden; + position:relative; +} + +.xfaContentarea{ + position:absolute; +} + +.xfaPrintOnly{ + display:none; +} + +.xfaLayer{ + position:absolute; + text-align:initial; + top:0; + left:0; + transform-origin:0 0; + line-height:1.2; +} + +.xfaLayer *{ + color:inherit; + font:inherit; + font-style:inherit; + font-weight:inherit; + font-kerning:inherit; + letter-spacing:-0.01px; + text-align:inherit; + text-decoration:inherit; + box-sizing:border-box; + background-color:transparent; + padding:0; + margin:0; + pointer-events:auto; + line-height:inherit; +} + +.xfaLayer *:required{ + outline:1.5px solid red; +} + +.xfaLayer div, +.xfaLayer svg, +.xfaLayer svg *{ + pointer-events:none; +} + +.xfaLayer a{ + color:blue; +} + +.xfaRich li{ + margin-left:3em; +} + +.xfaFont{ + color:black; + font-weight:normal; + font-kerning:none; + font-size:10px; + font-style:normal; + letter-spacing:0; + text-decoration:none; + vertical-align:0; +} + +.xfaCaption{ + overflow:hidden; + flex:0 0 auto; +} + +.xfaCaptionForCheckButton{ + overflow:hidden; + flex:1 1 auto; +} + +.xfaLabel{ + height:100%; + width:100%; +} + +.xfaLeft{ + display:flex; + flex-direction:row; + align-items:center; +} + +.xfaRight{ + display:flex; + flex-direction:row-reverse; + align-items:center; +} + +:is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + max-height:100%; +} + +.xfaTop{ + display:flex; + flex-direction:column; + align-items:flex-start; +} + +.xfaBottom{ + display:flex; + flex-direction:column-reverse; + align-items:flex-start; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + width:100%; +} + +.xfaBorder{ + background-color:transparent; + position:absolute; + pointer-events:none; +} + +.xfaWrapped{ + width:100%; + height:100%; +} + +:is(.xfaTextfield, .xfaSelect):focus{ + background-image:none; + background-color:transparent; + outline:var(--xfa-focus-outline); + outline-offset:-1px; +} + +:is(.xfaCheckbox, .xfaRadio):focus{ + outline:var(--xfa-focus-outline); +} + +.xfaTextfield, +.xfaSelect{ + height:100%; + width:100%; + flex:1 1 auto; + border:none; + resize:none; + background-image:var(--xfa-unfocused-field-background); +} + +.xfaSelect{ + padding-inline:2px; +} + +:is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect){ + flex:0 1 auto; +} + +.xfaButton{ + cursor:pointer; + width:100%; + height:100%; + border:none; + text-align:center; +} + +.xfaLink{ + width:100%; + height:100%; + position:absolute; + top:0; + left:0; +} + +.xfaCheckbox, +.xfaRadio{ + width:100%; + height:100%; + flex:0 0 auto; + border:none; +} + +.xfaRich{ + white-space:pre-wrap; + width:100%; + height:100%; +} + +.xfaImage{ + -o-object-position:left top; + object-position:left top; + -o-object-fit:contain; + object-fit:contain; + width:100%; + height:100%; +} + +.xfaLrTb, +.xfaRlTb, +.xfaTb{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaLr{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaRl{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; +} + +.xfaTb > div{ + justify-content:left; +} + +.xfaPosition{ + position:relative; +} + +.xfaArea{ + position:relative; +} + +.xfaValignMiddle{ + display:flex; + align-items:center; +} + +.xfaTable{ + display:flex; + flex-direction:column; + align-items:stretch; +} + +.xfaTable .xfaRow{ + display:flex; + flex-direction:row; + align-items:stretch; +} + +.xfaTable .xfaRlRow{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; + flex:1; +} + +.xfaTable .xfaRlRow > div{ + flex:1; +} + +:is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea){ + background:initial; +} + +@media print{ + .xfaTextfield, + .xfaSelect{ + background:transparent; + } + + .xfaSelect{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + text-indent:1px; + text-overflow:""; + } +} + +.canvasWrapper svg{ + transform:none; +} + +.moving:is(.canvasWrapper svg){ + z-index:100000; +} + +[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="90"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, 1, -1, 0, 1, 0); +} + +[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="180"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(-1, 0, 0, -1, 1, 1); +} + +[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) mask,[data-main-rotation="270"]:is(.highlight:is(.canvasWrapper svg),.highlightOutline:is(.canvasWrapper svg)) use:not(.clip,.mask){ + transform:matrix(0, -1, 1, 0, 0, 1); +} + +.draw:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; +} + +.draw[data-draw-rotation="90"]:is(.canvasWrapper svg){ + transform:rotate(90deg); +} + +.draw[data-draw-rotation="180"]:is(.canvasWrapper svg){ + transform:rotate(180deg); +} + +.draw[data-draw-rotation="270"]:is(.canvasWrapper svg){ + transform:rotate(270deg); +} + +.highlight:is(.canvasWrapper svg){ + --blend-mode:multiply; +} + +@media screen and (forced-colors: active){ + + .highlight:is(.canvasWrapper svg){ + --blend-mode:difference; + } +} + +.highlight:is(.canvasWrapper svg){ + + position:absolute; + mix-blend-mode:var(--blend-mode); +} + +.highlight:is(.canvasWrapper svg):not(.free){ + fill-rule:evenodd; +} + +.highlightOutline:is(.canvasWrapper svg){ + position:absolute; + mix-blend-mode:normal; + fill-rule:evenodd; + fill:none; +} + +.highlightOutline.hovered:is(.canvasWrapper svg):not(.free):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:var(--outline-width); +} + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + var(--outline-width) + 2 * var(--outline-around-width) + ); +} + +.highlightOutline.selected:is(.canvasWrapper svg):not(.free) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:var(--outline-width); +} + +.highlightOutline.free.hovered:is(.canvasWrapper svg):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:calc(2 * var(--outline-width)); +} + +.highlightOutline.free.selected:is(.canvasWrapper svg) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + 2 * (var(--outline-width) + var(--outline-around-width)) + ); +} + +.highlightOutline.free.selected:is(.canvasWrapper svg) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:calc(2 * var(--outline-width)); +} + +.toggle-button{ + --button-background-color:color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover:color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active:color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary:#0060df; + --color-accent-primary-hover:#0250bb; + --color-accent-primary-active:#054096; + --border-radius-circle:9999px; + --border-width:1px; + --size-item-small:16px; + --size-item-large:32px; + --color-canvas:white; + --background-color-canvas:var(--color-canvas); + --border-color-interactive:#8f8f9d; + --border-color-interactive-hover:var(--border-color-interactive); + --border-color-interactive-active:var(--border-color-interactive); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .toggle-button{ + --color-accent-primary:#0df; + --color-accent-primary-hover:#80ebff; + --color-accent-primary-active:#aaf2ff; + --color-canvas:#1c1b22; + --border-color-interactive:#f9f9fa; + } +} + +:where(html.is-dark) .toggle-button{ + --color-accent-primary:#0df; + --color-accent-primary-hover:#80ebff; + --color-accent-primary-active:#aaf2ff; + --color-canvas:#1c1b22; + --border-color-interactive:#f9f9fa; +} + +@media (forced-colors: active){ + + .toggle-button{ + --color-accent-primary:ButtonText; + --color-accent-primary-hover:SelectedItem; + --color-accent-primary-active:SelectedItem; + --button-background-color:ButtonFace; + --border-color-interactive:ButtonText; + --border-color-interactive-hover:SelectedItem; + --border-color-interactive-active:ButtonText; + --color-canvas:ButtonText; + --background-color-canvas:Canvas; + } +} + +.toggle-button{ + --toggle-background-color:var(--button-background-color); + --toggle-background-color-hover:var(--button-background-color-hover); + --toggle-background-color-active:var(--button-background-color-active); + --toggle-background-color-pressed:var(--color-accent-primary); + --toggle-background-color-pressed-hover:var(--color-accent-primary-hover); + --toggle-background-color-pressed-active:var(--color-accent-primary-active); + --toggle-border-color:var(--border-color-interactive); + --toggle-border-color-hover:var(--toggle-border-color); + --toggle-border-color-active:var(--toggle-border-color); + --toggle-border-radius:var(--border-radius-circle); + --toggle-border-width:var(--border-width); + --toggle-height:var(--size-item-small); + --toggle-width:var(--size-item-large); + --toggle-dot-background-color:var(--toggle-border-color); + --toggle-dot-background-color-hover:var(--toggle-dot-background-color); + --toggle-dot-background-color-active:var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed:var(--background-color-canvas); + --toggle-dot-margin:1px; + --toggle-dot-height:calc( + var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * + var(--toggle-border-width) + ); + --toggle-dot-width:var(--toggle-dot-height); + --toggle-dot-transform-x:calc( + var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) + ); + --input-width:var(--toggle-width); + + -webkit-appearance:none; + + -moz-appearance:none; + + appearance:none; + padding:0; + border:var(--toggle-border-width) solid var(--toggle-border-color); + height:var(--toggle-height); + width:var(--toggle-width); + border-radius:var(--toggle-border-radius); + background-color:var(--toggle-background-color); + box-sizing:border-box; +} + +.toggle-button:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); +} + +.toggle-button:enabled:hover{ + background-color:var(--toggle-background-color-hover); + border-color:var(--toggle-border-color); +} + +.toggle-button:enabled:hover:active{ + background-color:var(--toggle-background-color-active); + border-color:var(--toggle-border-color); +} + +.toggle-button::before{ + display:block; + content:""; + background-color:var(--toggle-dot-background-color); + height:var(--toggle-dot-height); + width:var(--toggle-dot-width); + margin:var(--toggle-dot-margin); + border-radius:var(--toggle-border-radius); + translate:0; +} + +.toggle-button[aria-pressed="true"]{ + background-color:var(--toggle-background-color-pressed); + border-color:transparent; +} + +.toggle-button[aria-pressed="true"]:enabled:hover{ + background-color:var(--toggle-background-color-pressed-hover); + border-color:transparent; +} + +.toggle-button[aria-pressed="true"]:enabled:hover:active{ + background-color:var(--toggle-background-color-pressed-active); + border-color:transparent; +} + +.toggle-button[aria-pressed="true"]::before{ + translate:var(--toggle-dot-transform-x); + background-color:var(--toggle-dot-background-color-on-pressed); +} + +.toggle-button[aria-pressed="true"]:enabled:hover::before,.toggle-button[aria-pressed="true"]:enabled:hover:active::before{ + background-color:var(--toggle-dot-background-color-on-pressed); +} + +.toggle-button[aria-pressed="true"]:-moz-locale-dir(rtl)::before,[dir="rtl"] .toggle-button[aria-pressed="true"]::before{ + translate:calc(-1 * var(--toggle-dot-transform-x)); +} + +@media (prefers-reduced-motion: no-preference){ + .toggle-button::before{ + transition:translate 100ms; + } +} + +@media (prefers-contrast){ + .toggle-button:enabled:hover{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button:enabled:hover:active{ + border-color:var(--toggle-border-color-active); + } + + .toggle-button[aria-pressed="true"]:enabled{ + border-color:var(--toggle-border-color); + position:relative; + } + + .toggle-button[aria-pressed="true"]:enabled:hover{ + border-color:var(--toggle-border-color-hover); + } + + .toggle-button[aria-pressed="true"]:enabled:hover:active{ + background-color:var(--toggle-dot-background-color-active); + border-color:var(--toggle-dot-background-color-hover); + } + + .toggle-button:enabled:hover::before, + .toggle-button:enabled:hover:active::before{ + background-color:var(--toggle-dot-background-color-hover); + } +} + +@media (forced-colors){ + .toggle-button{ + --toggle-dot-background-color:var(--color-accent-primary); + --toggle-dot-background-color-hover:var(--color-accent-primary-hover); + --toggle-dot-background-color-active:var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed:var(--button-background-color); + --toggle-border-color-hover:var(--border-color-interactive-hover); + --toggle-border-color-active:var(--border-color-interactive-active); + } + + .toggle-button[aria-pressed="true"]:enabled::after{ + border:1px solid var(--button-background-color); + content:""; + position:absolute; + height:var(--toggle-height); + width:var(--toggle-width); + display:block; + border-radius:var(--toggle-border-radius); + inset:-2px; + } + + .toggle-button[aria-pressed="true"]:enabled:hover:active::after{ + border-color:var(--toggle-border-color-active); + } +} + +:root{ + --clear-signature-button-icon:url(images/editor-toolbar-delete.svg); + --signature-bg:#f9f9fb; + --signature-hover-bg:#f0f0f4; + --button-signature-bg:transparent; + --button-signature-color:var(--main-color); + --button-signature-active-bg:#cfcfd8; + --button-signature-active-border:none; + --button-signature-active-color:var(--button-signature-color); + --button-signature-border:none; + --button-signature-hover-bg:#e0e0e6; + --button-signature-hover-color:var(--button-signature-color); +} + +@media (prefers-color-scheme: dark){ + + :root:where(:not(.is-light)){ + --signature-bg:#2b2a33; + --signature-hover-bg:var(--signature-bg); + --button-signature-active-bg:#5b5b66; + --button-signature-hover-bg:#52525e; + } +} + +:root:where(.is-dark){ + --signature-bg:#2b2a33; + --signature-hover-bg:var(--signature-bg); + --button-signature-active-bg:#5b5b66; + --button-signature-hover-bg:#52525e; +} + +@media screen and (forced-colors: active){ + + :root{ + --signature-bg:HighlightText; + --signature-hover-bg:var(--signature-bg); + --button-signature-bg:HighlightText; + --button-signature-color:ButtonText; + --button-signature-active-bg:ButtonText; + --button-signature-active-color:HighlightText; + --button-signature-border:1px solid ButtonText; + --button-signature-hover-bg:Highlight; + --button-signature-hover-color:HighlightText; + } +} + +.signatureDialog{ + --primary-color:var(--text-primary-color); + --description-input-color:var(--primary-color); + --border-color:#8f8f9d; +} + +@media screen and (forced-colors: active){ + + .signatureDialog{ + --primary-color:ButtonText; + --border-color:ButtonText; + } +} + +.signatureDialog{ + + width:570px; + max-width:100%; + min-width:300px; + padding:16px 0; +} + +.signatureDialog .mainContainer{ + width:100%; + display:flex; + flex-direction:column; + align-items:flex-start; + gap:12px; +} + +:is(.signatureDialog .mainContainer) span:not([role="sectionhead"]){ + font-size:13px; + font-style:normal; + font-weight:400; + line-height:normal; +} + +:is(.signatureDialog .mainContainer) .title{ + margin-inline-start:16px; +} + +.signatureDialog .inputWithClearButton{ + --button-dimension:24px; + + --closing-button-icon:url(images/messageBar_closingButton.svg); + --closing-button-color:var(--primary-color); + + width:100%; + position:relative; + display:flex; + align-items:center; + justify-content:center; +} + +:is(.signatureDialog .inputWithClearButton) > input{ + width:100%; + height:32px; + padding-inline:8px calc(4px + var(--button-dimension)); + box-sizing:border-box; + background-color:transparent; + border-radius:4px; + border:1px solid var(--border-color); + color:var(--description-input-color); +} + +:is(.signatureDialog .inputWithClearButton) .clearInputButton{ + position:absolute; + inset-block-start:4px; + inset-inline-end:4px; + display:inline-block; + width:var(--button-dimension); + height:var(--button-dimension); + background-color:var(--closing-button-color); + -webkit-mask-size:cover; + mask-size:cover; + -webkit-mask-image:var(--closing-button-icon); + mask-image:var(--closing-button-icon); + padding:0; + border:0; +} + +#addSignatureDialog{ + --secondary-color:var(--text-secondary-color); + --bg-hover:#e0e0e6; + --tab-top-line-active-color:#0060df; + --tab-top-line-active-hover-color:var(--tab-text-hover-color); + --tab-top-line-hover-color:#8f8f9d; + --tab-top-line-inactive-color:#cfcfd8; + --tab-bottom-line-active-color:var(--tab-top-line-inactive-color); + --tab-bottom-line-hover-color:var(--tab-top-line-inactive-color); + --tab-bottom-line-inactive-color:var(--tab-top-line-inactive-color); + --tab-bg:var(--dialog-bg-color); + --tab-bg-active-color:var(--tab-bg); + --tab-bg-active-hover-color:var(--bg-hover); + --tab-bg-hover:var(--bg-hover); + --tab-text-color:var(--primary-color); + --tab-text-active-color:var(--tab-top-line-active-color); + --tab-text-active-hover-color:var(--tab-text-hover-color); + --tab-text-hover-color:var(--tab-text-color); + --signature-placeholder-color:var(--secondary-color); + --signature-draw-placeholder-color:var(--primary-color); + --signature-color:var(--primary-color); + --clear-signature-button-border-width:0; + --clear-signature-button-border-style:solid; + --clear-signature-button-border-color:transparent; + --clear-signature-button-border-disabled-color:transparent; + --clear-signature-button-color:var(--primary-color); + --clear-signature-button-hover-color:var(--clear-signature-button-color); + --clear-signature-button-active-color:var(--clear-signature-button-color); + --clear-signature-button-disabled-color:var(--clear-signature-button-color); + --clear-signature-button-focus-color:var(--clear-signature-button-color); + --clear-signature-button-bg:var(--dialog-bg-color); + --clear-signature-button-bg-hover:var(--bg-hover); + --clear-signature-button-bg-active:#cfcfd8; + --clear-signature-button-bg-focus:#f0f0f4; + --clear-signature-button-bg-disabled:color-mix( + in srgb, + #f0f0f4, + transparent 40% + ); + --save-warning-color:var(--secondary-color); + --thickness-bg:var(--dialog-bg-color); + --thickness-label-color:var(--primary-color); + --thickness-slider-color:var(--primary-color); + --draw-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) #addSignatureDialog{ + --dialog-bg-color:#42414d; + --bg-hover:#52525e; + --primary-color:#fbfbfe; + --secondary-color:#cfcfd8; + --tab-top-line-active-color:#0df; + --tab-top-line-inactive-color:#8f8f9d; + --clear-signature-button-bg-active:#5b5b66; + --clear-signature-button-bg-focus:#2b2a33; + --clear-signature-button-bg-disabled:color-mix( + in srgb, + #2b2a33, + transparent 40% + ); + } +} + +:where(html.is-dark) #addSignatureDialog{ + --dialog-bg-color:#42414d; + --bg-hover:#52525e; + --primary-color:#fbfbfe; + --secondary-color:#cfcfd8; + --tab-top-line-active-color:#0df; + --tab-top-line-inactive-color:#8f8f9d; + --clear-signature-button-bg-active:#5b5b66; + --clear-signature-button-bg-focus:#2b2a33; + --clear-signature-button-bg-disabled:color-mix( + in srgb, + #2b2a33, + transparent 40% + ); +} + +@media screen and (forced-colors: active){ + + #addSignatureDialog{ + --secondary-color:ButtonText; + --bg:HighlightText; + --bg-hover:var(--bg); + --tab-top-line-active-color:ButtonText; + --tab-top-line-active-hover-color:HighlightText; + --tab-top-line-hover-color:SelectedItem; + --tab-top-line-inactive-color:ButtonText; + --tab-bottom-line-active-color:var(--tab-top-line-active-color); + --tab-bottom-line-hover-color:var(--tab-top-line-hover-color); + --tab-bg:var(--bg); + --tab-bg-active-color:SelectedItem; + --tab-bg-active-hover-color:SelectedItem; + --tab-text-color:ButtonText; + --tab-text-active-color:HighlightText; + --tab-text-active-hover-color:HighlightText; + --tab-text-hover-color:SelectedItem; + --signature-color:ButtonText; + --clear-signature-button-border-width:1px; + --clear-signature-button-border-style:solid; + --clear-signature-button-border-color:ButtonText; + --clear-signature-button-border-disabled-color:GrayText; + --clear-signature-button-color:ButtonText; + --clear-signature-button-hover-color:HighlightText; + --clear-signature-button-active-color:SelectedItem; + --clear-signature-button-focus-color:CanvasText; + --clear-signature-button-disabled-color:GrayText; + --clear-signature-button-bg:var(--bg); + --clear-signature-button-bg-hover:SelectedItem; + --clear-signature-button-bg-active:var(--bg); + --clear-signature-button-bg-focus:var(--bg); + --clear-signature-button-bg-disabled:var(--bg); + --thickness-bg:Canvas; + --thickness-label-color:CanvasText; + --thickness-slider-color:ButtonText; + } +} + +#addSignatureDialog #addSignatureDialogLabel{ + overflow:hidden; + position:absolute; + inset:0; + width:0; + height:0; +} + +#addSignatureDialog.waiting::after{ + content:""; + cursor:wait; + position:absolute; + inset:0; + width:100%; + height:100%; +} + +:is(#addSignatureDialog .mainContainer) [role="tablist"]{ + width:100%; + display:flex; + align-items:flex-start; + gap:0; +} + +:is(:is(#addSignatureDialog .mainContainer) [role="tablist"]) > [role="tab"]{ + flex:1 0 0; + align-self:stretch; + background-color:var(--tab-bg); + padding-inline:0; + cursor:default; + + border-inline:0; + border-block-width:1px; + border-block-style:solid; + border-block-start-color:var(--tab-top-line-inactive-color); + border-block-end-color:var(--tab-bottom-line-inactive-color); + border-radius:0; + + font:menu; + font-size:13px; + font-style:normal; + line-height:normal; + font-weight:400; + color:var(--tab-text-color); +} + +:is(:is(:is(#addSignatureDialog .mainContainer) [role="tablist"]) > [role="tab"]):hover{ + border-block-start-width:2px; + border-block-start-color:var(--tab-top-line-hover-color); + border-block-end-color:var(--tab-bottom-line-hover-color); + background-color:var(--tab-bg-hover); + color:var(--tab-text-hover-color); +} + +:is(:is(:is(#addSignatureDialog .mainContainer) [role="tablist"]) > [role="tab"]):focus-visible{ + outline:2px solid var(--tab-top-line-active-color); + outline-offset:-2px; +} + +[aria-selected="true"]:is(:is(:is(#addSignatureDialog .mainContainer) [role="tablist"]) > [role="tab"]){ + border-block-start-width:2px; + border-block-start-color:var(--tab-top-line-active-color); + border-block-end-color:var(--tab-bottom-line-active-color); + background-color:var(--tab-bg-active-color); + font-weight:590; + color:var(--tab-text-active-color); +} + +[aria-selected="true"]:is(:is(:is(#addSignatureDialog .mainContainer) [role="tablist"]) > [role="tab"]):hover{ + border-block-start-color:var(--tab-top-line-active-hover-color); + background-color:var(--tab-bg-active-hover-color); + color:var(--tab-text-active-hover-color); +} + +:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer{ + width:100%; + height:auto; + display:flex; + flex-direction:column; + align-items:flex-end; + align-self:stretch; + gap:12px; + padding-inline:16px; + box-sizing:border-box; +} + +:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]{ + position:relative; + width:100%; + height:220px; + background-color:var(--signature-bg); + border-radius:4px; +} + +:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) > svg{ + position:absolute; + inset:0; + width:100%; + height:100%; + background-color:transparent; +} + +#addSignatureTypeContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]){ + display:none; +} + +#addSignatureTypeContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureTypeInput{ + position:absolute; + inset:0; + width:100%; + height:100%; + border:0; + padding:0; + text-align:center; + color:var(--signature-color); + background-color:transparent; + + font-family:"Brush script", "Apple Chancery", "Segoe script", "Freestyle Script", "Palace Script MT", "Brush Script MT", TK, cursive, serif; + font-size:44px; + font-style:italic; + font-weight:400; +} + +:is(#addSignatureTypeContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureTypeInput)::-moz-placeholder{ + color:var(--signature-placeholder-color); + text-align:center; + + font:menu; + font-style:normal; + font-weight:274; + font-size:44px; + line-height:normal; +} + +:is(#addSignatureTypeContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureTypeInput)::placeholder{ + color:var(--signature-placeholder-color); + text-align:center; + + font:menu; + font-style:normal; + font-weight:274; + font-size:44px; + line-height:normal; +} + +#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]){ + display:none; +} + +#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) > span{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + display:grid; + align-items:center; + justify-content:center; + + background-color:transparent; + color:var(--signature-placeholder-color); + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) > svg{ + stroke:var(--primary-color); + fill:none; + stroke-opacity:1; + stroke-linecap:round; + stroke-linejoin:round; + stroke-miterlimit:10; +} + +:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) > svg):hover{ + cursor:var(--draw-cursor); +} + +#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness{ + position:absolute; + width:100%; + inset-block-end:0; + display:grid; + align-items:center; + justify-content:center; + pointer-events:none; +} + +:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > span{ + color:var(--signature-draw-placeholder-color); +} + +:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div{ + width:auto; + height:auto; + display:flex; + align-items:center; + justify-content:center; + gap:8px; + padding:6px 8px; + margin:0; + background-color:var(--thickness-bg); + border-radius:4px 4px 0 0; + pointer-events:auto; +} + +:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > label{ + color:var(--thickness-label-color); +} + +:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input{ + width:100px; + height:14px; + background-color:transparent; +} + +:is(:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input)::-webkit-slider-runnable-track,:is(:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input)::-moz-range-track,:is(:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input)::-moz-range-progress{ + background-color:var(--thickness-slider-color); +} + +:is(:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input)::-webkit-slider-thumb,:is(:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input)::-moz-range-thumb{ + background-color:var(--thickness-bg); +} + +:is(:is(#addSignatureDrawContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #thickness) > div) > input{ + + border-radius:4.5px; + border:0; + color:var(--signature-color); +} + +#addSignatureImageContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]){ + display:none; +} + +#addSignatureImageContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) > svg{ + stroke:none; + stroke-width:0; + fill:var(--primary-color); + fill-opacity:1; +} + +#addSignatureImageContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureImagePlaceholder{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + background-color:transparent; + display:flex; + flex-direction:column; + align-items:center; + justify-content:center; +} + +:is(#addSignatureImageContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureImagePlaceholder) a{ + text-decoration:underline; + cursor:pointer; +} + +#addSignatureImageContainer:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > [role="tabpanel"]) #addSignatureFilePicker{ + visibility:hidden; + position:relative; + width:0; + height:0; +} + +[data-selected="type"]:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > #addSignatureTypeContainer,[data-selected="draw"]:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > #addSignatureDrawContainer,[data-selected="image"]:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) > #addSignatureImageContainer{ + display:block; +} + +:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls{ + display:flex; + flex-direction:column; + justify-content:center; + align-items:flex-start; + gap:12px; + align-self:stretch; +} + +:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer{ + display:flex; + align-items:flex-end; + gap:16px; + align-self:stretch; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #addSignatureDescriptionContainer{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + flex:1 0 0; +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #addSignatureDescriptionContainer) > label{ + width:auto; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton{ + display:flex; + height:32px; + padding:4px 8px; + align-items:center; + background-color:var(--clear-signature-button-bg); + border-width:var(--clear-signature-button-border-width); + border-style:var(--clear-signature-button-border-style); + border-color:var(--clear-signature-button-border-color); + border-radius:4px; +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton) > span{ + display:flex; + height:24px; + align-items:center; + gap:4px; + flex-shrink:0; + + color:var(--clear-signature-button-color); +} + +:is(:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton) > span)::after{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--clear-signature-button-icon); + mask-image:var(--clear-signature-button-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--clear-signature-button-color); + flex-shrink:0; +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):hover{ + background-color:var(--clear-signature-button-bg-hover); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):hover > span{ + color:var(--clear-signature-button-hover-color); +} + +:is(:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):hover > span)::after{ + background-color:var(--clear-signature-button-hover-color); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):active{ + background-color:var(--clear-signature-button-bg-active); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):active > span{ + color:var(--clear-signature-button-active-color); +} + +:is(:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):active > span)::after{ + background-color:var(--clear-signature-button-active-color); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):focus-visible{ + background-color:var(--clear-signature-button-bg-focus); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):focus-visible > span{ + color:var(--clear-signature-button-focus-color); +} + +:is(:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):focus-visible > span)::after{ + background-color:var(--clear-signature-button-focus-color); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):disabled{ + background-color:var(--clear-signature-button-bg-disabled); + border-color:var(--clear-signature-button-border-disabled-color); +} + +:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):disabled > span{ + color:var(--clear-signature-button-disabled-color); +} + +:is(:is(:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #horizontalContainer) #clearSignatureButton):disabled > span)::after{ + background-color:var( + --clear-signature-button-disabled-color + ); +} + +:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer{ + display:grid; + grid-template-columns:max-content max-content; + gap:4px; + width:100%; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer) > input{ + margin:0; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer) > label{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer):not(.fullStorage) #addSignatureSaveWarning{ + display:none; +} + +.fullStorage:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer) #addSignatureSaveWarning{ + display:block; + opacity:1; + color:var(--save-warning-color); + font-size:11px; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer):is([disabled],.fullStorage){ + pointer-events:none; +} + +:is(:is(:is(:is(#addSignatureDialog .mainContainer) #addSignatureActionContainer) #addSignatureControls) #addSignatureSaveContainer):is([disabled],.fullStorage) > :not(#addSignatureSaveWarning){ + opacity:0.4; +} + +#editSignatureDescriptionDialog .mainContainer{ + padding-inline:16px; + box-sizing:border-box; +} + +:is(#editSignatureDescriptionDialog .mainContainer) .title{ + margin-inline-start:0; +} + +:is(#editSignatureDescriptionDialog .mainContainer) #editSignatureDescriptionAndView{ + width:auto; + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + align-self:stretch; +} + +:is(:is(#editSignatureDescriptionDialog .mainContainer) #editSignatureDescriptionAndView) #editSignatureDescriptionContainer{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + flex:1 1 auto; +} + +:is(:is(#editSignatureDescriptionDialog .mainContainer) #editSignatureDescriptionAndView) > svg{ + width:210px; + height:180px; + padding:8px; + background-color:var(--signature-bg); +} + +:is(:is(:is(#editSignatureDescriptionDialog .mainContainer) #editSignatureDescriptionAndView) > svg) > path{ + stroke:var(--button-signature-color); + stroke-width:1px; + stroke-linecap:round; + stroke-linejoin:round; + stroke-miterlimit:10; + vector-effect:non-scaling-stroke; + fill:none; +} + +.contours:is(:is(:is(:is(#editSignatureDescriptionDialog .mainContainer) #editSignatureDescriptionAndView) > svg) > path){ + fill:var(--button-signature-color); + stroke-width:0.5px; +} + +#editorSignatureParamsToolbar{ + padding:8px; +} + +#editorSignatureParamsToolbar #addSignatureDoorHanger{ + gap:8px; + padding:2px; +} + +:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer{ + height:32px; + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + gap:8px; +} + +:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button{ + border:var(--button-signature-border); + border-radius:4px; + background-color:var(--button-signature-bg); + color:var(--button-signature-color); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button):hover{ + background-color:var(--button-signature-hover-bg); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button):active{ + border:var(--button-signature-active-border); + background-color:var(--button-signature-active-bg); + color:var(--button-signature-active-color); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button):active::before{ + background-color:var(--button-signature-active-color); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button):focus-visible{ + outline:var(--focus-ring-outline); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) button):focus-visible::before{ + background-color:var(--button-signature-color); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .deleteButton)::before{ + -webkit-mask-image:var(--clear-signature-button-icon); + mask-image:var(--clear-signature-button-icon); +} + +:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton{ + width:auto; + height:100%; + min-height:var(--menuitem-height); + aspect-ratio:unset; + display:flex; + align-items:center; + justify-content:flex-start; + outline:none; + border-radius:4px; + box-sizing:border-box; + font:message-box; + position:relative; + flex:1 1 auto; + padding:0; + gap:8px; + text-align:start; + white-space:normal; + cursor:default; + overflow:hidden; +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton) > svg{ + display:inline-block; + height:100%; + aspect-ratio:1; + background-color:var(--signature-bg); + flex:none; + padding:4px; + box-sizing:border-box; + border:none; + border-radius:4px; +} + +:is(:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton) > svg) > path{ + stroke:var(--button-signature-color); + stroke-width:1px; + stroke-linecap:round; + stroke-linejoin:round; + stroke-miterlimit:10; + vector-effect:non-scaling-stroke; + fill:none; +} + +.contours:is(:is(:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton) > svg) > path){ + fill:var(--button-signature-color); + stroke-width:0.5px; +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton):is(:hover,:active) > svg{ + border-radius:4px 0 0 4px; + background-color:var(--signature-hover-bg); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton):hover > span{ + color:var(--button-signature-hover-color); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton):active{ + background-color:var(--button-signature-active-bg); +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton):is([disabled="disabled"],[disabled]){ + opacity:0.5; + pointer-events:none; +} + +:is(:is(:is(#editorSignatureParamsToolbar #addSignatureDoorHanger) .toolbarAddSignatureButtonContainer) .toolbarAddSignatureButton) > span{ + height:auto; + text-overflow:ellipsis; + white-space:nowrap; + flex:1 1 auto; + font:menu; + font-size:13px; + font-style:normal; + font-weight:400; + line-height:normal; + overflow:hidden; +} + +.editDescription.altText{ + --alt-text-add-image:url(images/editor-toolbar-edit.svg) !important; +} + +.editDescription.altText::before{ + width:16px !important; + height:16px !important; +} + +:root{ + --outline-width:2px; + --outline-color:#0060df; + --outline-around-width:1px; + --outline-around-color:#f0f0f4; + --hover-outline-around-color:var(--outline-around-color); + --focus-outline:solid var(--outline-width) var(--outline-color); + --unfocus-outline:solid var(--outline-width) transparent; + --focus-outline-around:solid var(--outline-around-width) var(--outline-around-color); + --hover-outline-color:#8f8f9d; + --hover-outline:solid var(--outline-width) var(--hover-outline-color); + --hover-outline-around:solid var(--outline-around-width) var(--hover-outline-around-color); + --freetext-line-height:1.35; + --freetext-padding:2px; + --resizer-bg-color:var(--outline-color); + --resizer-size:6px; + --resizer-shift:calc( + 0px - (var(--outline-width) + var(--resizer-size)) / 2 - + var(--outline-around-width) + ); + --editorFreeText-editing-cursor:text; + --editorInk-editing-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; + --editorHighlight-editing-cursor:url(images/cursor-editorTextHighlight.svg) 24 24, text; + --editorFreeHighlight-editing-cursor:url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; + + --new-alt-text-warning-image:url(images/altText_warning.svg); +} +.visuallyHidden{ + position:absolute; + top:0; + left:0; + border:0; + margin:0; + padding:0; + width:0; + height:0; + overflow:hidden; + white-space:nowrap; + font-size:0; +} + +.textLayer.highlighting{ + cursor:var(--editorFreeHighlight-editing-cursor); +} + +.textLayer.highlighting:not(.free) span{ + cursor:var(--editorHighlight-editing-cursor); +} + +[role="img"]:is(.textLayer.highlighting:not(.free) span){ + cursor:var(--editorFreeHighlight-editing-cursor); +} + +.textLayer.highlighting.free span{ + cursor:var(--editorFreeHighlight-editing-cursor); +} + +:is(#viewerContainer.pdfPresentationMode:fullscreen,.annotationEditorLayer.disabled) .noAltTextBadge{ + display:none !important; +} + +@media (min-resolution: 1.1dppx){ + :root{ + --editorFreeText-editing-cursor:url(images/cursor-editorFreeText.svg) 0 16, text; + } +} + +@media screen and (forced-colors: active){ + :root{ + --outline-color:CanvasText; + --outline-around-color:ButtonFace; + --resizer-bg-color:ButtonText; + --hover-outline-color:Highlight; + --hover-outline-around-color:SelectedItemText; + } +} + +[data-editor-rotation="90"]{ + transform:rotate(90deg); +} + +[data-editor-rotation="180"]{ + transform:rotate(180deg); +} + +[data-editor-rotation="270"]{ + transform:rotate(270deg); +} + +.annotationEditorLayer{ + background:transparent; + position:absolute; + inset:0; + font-size:calc(100px * var(--total-scale-factor)); + transform-origin:0 0; + cursor:auto; +} + +.annotationEditorLayer .selectedEditor{ + z-index:100000 !important; +} + +.annotationEditorLayer.drawing *{ + pointer-events:none !important; +} + +.annotationEditorLayer.waiting{ + content:""; + cursor:wait; + position:absolute; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer.disabled{ + pointer-events:none; +} + +.annotationEditorLayer.freetextEditing{ + cursor:var(--editorFreeText-editing-cursor); +} + +.annotationEditorLayer.inkEditing{ + cursor:var(--editorInk-editing-cursor); +} + +.annotationEditorLayer .draw{ + box-sizing:border-box; +} + +.annotationEditorLayer +:is(.freeTextEditor, .inkEditor, .stampEditor, .signatureEditor){ + position:absolute; + background:transparent; + z-index:1; + transform-origin:0 0; + cursor:auto; + max-width:100%; + max-height:100%; + border:var(--unfocus-outline); +} + +.draggable.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)){ + cursor:move; +} + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)){ + border:var(--focus-outline); + outline:var(--focus-outline-around); +} + +.selectedEditor:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor))::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + pointer-events:none; +} + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)):hover:not(.selectedEditor){ + border:var(--hover-outline); + outline:var(--hover-outline-around); +} + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)):hover:not(.selectedEditor)::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); +} + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar{ + --editor-toolbar-delete-image:url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color:#f0f0f4; + --editor-toolbar-highlight-image:url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-fg-color:#2e2e56; + --editor-toolbar-border-color:#8f8f9d; + --editor-toolbar-hover-border-color:var(--editor-toolbar-border-color); + --editor-toolbar-hover-bg-color:#e0e0e6; + --editor-toolbar-hover-fg-color:var(--editor-toolbar-fg-color); + --editor-toolbar-hover-outline:none; + --editor-toolbar-focus-outline-color:#0060df; + --editor-toolbar-shadow:0 2px 6px 0 rgb(58 57 68 / 0.2); + --editor-toolbar-vert-offset:6px; + --editor-toolbar-height:28px; + --editor-toolbar-padding:2px; + --alt-text-done-color:#2ac3a2; + --alt-text-warning-color:#0090ed; + --alt-text-hover-done-color:var(--alt-text-done-color); + --alt-text-hover-warning-color:var(--alt-text-warning-color); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) :is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:#2b2a33; + --editor-toolbar-fg-color:#fbfbfe; + --editor-toolbar-hover-bg-color:#52525e; + --editor-toolbar-focus-outline-color:#0df; + --alt-text-done-color:#54ffbd; + --alt-text-warning-color:#80ebff; + } +} + +:where(html.is-dark) :is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:#2b2a33; + --editor-toolbar-fg-color:#fbfbfe; + --editor-toolbar-hover-bg-color:#52525e; + --editor-toolbar-focus-outline-color:#0df; + --alt-text-done-color:#54ffbd; + --alt-text-warning-color:#80ebff; +} + +@media screen and (forced-colors: active){ + + :is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:ButtonFace; + --editor-toolbar-fg-color:ButtonText; + --editor-toolbar-border-color:ButtonText; + --editor-toolbar-hover-border-color:AccentColor; + --editor-toolbar-hover-bg-color:ButtonFace; + --editor-toolbar-hover-fg-color:AccentColor; + --editor-toolbar-hover-outline:2px solid var(--editor-toolbar-hover-border-color); + --editor-toolbar-focus-outline-color:ButtonBorder; + --editor-toolbar-shadow:none; + --alt-text-done-color:var(--editor-toolbar-fg-color); + --alt-text-warning-color:var(--editor-toolbar-fg-color); + --alt-text-hover-done-color:var(--editor-toolbar-hover-fg-color); + --alt-text-hover-warning-color:var(--editor-toolbar-hover-fg-color); + } +} + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar{ + + display:flex; + width:-moz-fit-content; + width:fit-content; + height:var(--editor-toolbar-height); + flex-direction:column; + justify-content:center; + align-items:center; + cursor:default; + pointer-events:auto; + box-sizing:content-box; + padding:var(--editor-toolbar-padding); + + position:absolute; + inset-inline-end:0; + inset-block-start:calc(100% + var(--editor-toolbar-vert-offset)); + + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); +} + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar){ + display:none; +} + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar):has(:focus-visible){ + border-color:transparent; +} + +[dir="ltr"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar){ + transform-origin:100% 0; +} + +[dir="rtl"] :is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar){ + transform-origin:0 0; +} + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons{ + display:flex; + justify-content:center; + align-items:center; + gap:0; + height:100%; +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) button{ + padding:0; +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .divider{ + width:0; + height:calc( + 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) + ); + border-left:1px solid var(--editor-toolbar-border-color); + border-right:none; + display:inline-block; + margin-inline:2px; +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .highlightButton{ + width:var(--editor-toolbar-height); +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .highlightButton)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-highlight-image); + mask-image:var(--editor-toolbar-highlight-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .highlightButton):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .delete{ + width:var(--editor-toolbar-height); +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .delete)::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-delete-image); + mask-image:var(--editor-toolbar-delete-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .delete):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) > *{ + height:var(--editor-toolbar-height); +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) > :not(.divider){ + border:none; + background-color:transparent; + cursor:pointer; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover{ + border-radius:2px; + background-color:var(--editor-toolbar-hover-bg-color); + color:var(--editor-toolbar-hover-fg-color); + outline:var(--editor-toolbar-hover-outline); + outline-offset:1px; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):hover:active{ + outline:none; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) > :not(.divider)):focus-visible{ + border-radius:2px; + outline:2px solid var(--editor-toolbar-focus-outline-color); +} + +:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText{ + --alt-text-add-image:url(images/altText_add.svg); + --alt-text-done-image:url(images/altText_done.svg); + + display:flex; + align-items:center; + justify-content:center; + width:-moz-max-content; + width:max-content; + padding-inline:8px; + pointer-events:all; + font:menu; + font-weight:590; + font-size:12px; + color:var(--editor-toolbar-fg-color); +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText):disabled{ + pointer-events:none; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + content:""; + -webkit-mask-image:var(--alt-text-add-image); + mask-image:var(--alt-text-add-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + width:12px; + height:13px; + background-color:var(--editor-toolbar-fg-color); + margin-inline-end:4px; +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} + +.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); +} + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + background-color:var(--alt-text-warning-color); + -webkit-mask-size:cover; + mask-size:cover; +} + +.new:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-warning-color); +} + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText)::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); + background-color:var(--alt-text-done-color); +} + +.new.done:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText):hover::before{ + background-color:var(--alt-text-hover-done-color); +} + +:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip{ + display:none; + word-wrap:anywhere; +} + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#f0f0f4; + --alt-text-tooltip-fg:#15141a; + --alt-text-tooltip-border:#8f8f9d; + --alt-text-tooltip-shadow:0px 2px 6px 0px rgb(58 57 68 / 0.2); +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#1c1b22; + --alt-text-tooltip-fg:#fbfbfe; + --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; + } +} + +:where(html.is-dark) .show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:#1c1b22; + --alt-text-tooltip-fg:#fbfbfe; + --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; +} + +@media screen and (forced-colors: active){ + + .show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + --alt-text-tooltip-bg:Canvas; + --alt-text-tooltip-fg:CanvasText; + --alt-text-tooltip-border:CanvasText; + --alt-text-tooltip-shadow:none; + } +} + +.show:is(:is(:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.highlightEditor,.signatureEditor),.textLayer) .editToolbar) .buttons) .altText) .tooltip){ + + display:inline-flex; + flex-direction:column; + align-items:center; + justify-content:center; + position:absolute; + top:calc(100% + 2px); + inset-inline-start:0; + padding-block:2px 3px; + padding-inline:3px; + max-width:300px; + width:-moz-max-content; + width:max-content; + height:auto; + font-size:12px; + + border:0.5px solid var(--alt-text-tooltip-border); + background:var(--alt-text-tooltip-bg); + box-shadow:var(--alt-text-tooltip-shadow); + color:var(--alt-text-tooltip-fg); + + pointer-events:none; +} + +.annotationEditorLayer .freeTextEditor{ + padding:calc(var(--freetext-padding) * var(--total-scale-factor)); + width:auto; + height:auto; + touch-action:none; +} + +.annotationEditorLayer .freeTextEditor .internal{ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:var(--freetext-line-height); + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.annotationEditorLayer .freeTextEditor .overlay{ + position:absolute; + display:none; + background:transparent; + inset:0; + width:100%; + height:100%; +} + +.annotationEditorLayer freeTextEditor .overlay.enabled{ + display:block; +} + +.annotationEditorLayer .freeTextEditor .internal:empty::before{ + content:attr(default-content); + color:gray; +} + +.annotationEditorLayer .freeTextEditor .internal:focus{ + outline:none; + -webkit-user-select:auto; + -moz-user-select:auto; + user-select:auto; +} + +.annotationEditorLayer .inkEditor{ + width:100%; + height:100%; +} + +.annotationEditorLayer .inkEditor.editing{ + cursor:inherit; +} + +.annotationEditorLayer .inkEditor .inkEditorCanvas{ + position:absolute; + inset:0; + width:100%; + height:100%; + touch-action:none; +} + +.annotationEditorLayer .stampEditor{ + width:auto; + height:auto; +} + +:is(.annotationEditorLayer .stampEditor) canvas{ + position:absolute; + width:100%; + height:100%; + margin:0; + top:0; + left:0; +} + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#f0f0f4; + --no-alt-text-badge-bg-color:#cfcfd8; + --no-alt-text-badge-fg-color:#5b5b66; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) :is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#52525e; + --no-alt-text-badge-bg-color:#fbfbfe; + --no-alt-text-badge-fg-color:#15141a; + } +} + +:where(html.is-dark) :is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:#52525e; + --no-alt-text-badge-bg-color:#fbfbfe; + --no-alt-text-badge-fg-color:#15141a; +} + +@media screen and (forced-colors: active){ + + :is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + --no-alt-text-badge-border-color:ButtonText; + --no-alt-text-badge-bg-color:ButtonFace; + --no-alt-text-badge-fg-color:ButtonText; + } +} + +:is(.annotationEditorLayer .stampEditor) .noAltTextBadge{ + + position:absolute; + inset-inline-end:5px; + inset-block-end:5px; + display:inline-flex; + width:32px; + height:32px; + padding:3px; + justify-content:center; + align-items:center; + pointer-events:none; + z-index:1; + + border-radius:2px; + border:1px solid var(--no-alt-text-badge-border-color); + background:var(--no-alt-text-badge-bg-color); +} + +:is(:is(.annotationEditorLayer .stampEditor) .noAltTextBadge)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-warning-image); + mask-image:var(--new-alt-text-warning-image); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--no-alt-text-badge-fg-color); +} + +:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers{ + position:absolute; + inset:0; +} + +.hidden:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers){ + display:none; +} + +:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer{ + width:var(--resizer-size); + height:var(--resizer-size); + background:content-box var(--resizer-bg-color); + border:var(--focus-outline-around); + border-radius:2px; + position:absolute; +} + +.topLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:var(--resizer-shift); +} + +.topMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); +} + +.topRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + top:var(--resizer-shift); + right:var(--resizer-shift); +} + +.middleRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + right:var(--resizer-shift); +} + +.bottomRight:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + right:var(--resizer-shift); +} + +.bottomMiddle:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); +} + +.bottomLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + bottom:var(--resizer-shift); + left:var(--resizer-shift); +} + +.middleLeft:is(:is(:is(.annotationEditorLayer :is(.freeTextEditor,.inkEditor,.stampEditor,.signatureEditor)) > .resizers) > .resizer){ + top:calc(50% + var(--resizer-shift)); + left:var(--resizer-shift); +} + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nwse-resize; +} + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ns-resize; +} + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:nesw-resize; +} + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="180"],[data-editor-rotation="0"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="90"],[data-editor-rotation="270"])) > .resizers > .resizer){ + cursor:ew-resize; +} + +.topLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nesw-resize; +} + +.topMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomMiddle:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ew-resize; +} + +.topRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.bottomLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:nwse-resize; +} + +.middleRight:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer),.middleLeft:is(:is(.annotationEditorLayer[data-main-rotation="0"] :is([data-editor-rotation="90"],[data-editor-rotation="270"]),.annotationEditorLayer[data-main-rotation="90"] :is([data-editor-rotation="0"],[data-editor-rotation="180"]),.annotationEditorLayer[data-main-rotation="180"] :is([data-editor-rotation="270"],[data-editor-rotation="90"]),.annotationEditorLayer[data-main-rotation="270"] :is([data-editor-rotation="180"],[data-editor-rotation="0"])) > .resizers > .resizer){ + cursor:ns-resize; +} + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar{ + rotate:270deg; +} + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="90"],[data-main-rotation="90"] [data-editor-rotation="0"],[data-main-rotation="180"] [data-editor-rotation="270"],[data-main-rotation="270"] [data-editor-rotation="180"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="180"],[data-main-rotation="90"] [data-editor-rotation="90"],[data-main-rotation="180"] [data-editor-rotation="0"],[data-main-rotation="270"] [data-editor-rotation="270"])) .editToolbar{ + rotate:180deg; + inset-inline-end:100%; + inset-block-start:calc(0pc - var(--editor-toolbar-vert-offset)); +} + +:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar{ + rotate:90deg; +} + +[dir="ltr"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:100%; +} + +[dir="rtl"] :is(:is(.annotationEditorLayer :is([data-main-rotation="0"] [data-editor-rotation="270"],[data-main-rotation="90"] [data-editor-rotation="180"],[data-main-rotation="180"] [data-editor-rotation="90"],[data-main-rotation="270"] [data-editor-rotation="0"])) .editToolbar){ + inset-inline-start:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} + +.dialog.altText::backdrop{ + -webkit-mask:url(#alttext-manager-mask); + mask:url(#alttext-manager-mask); +} + +.dialog.altText.positioned{ + margin:0; +} + +.dialog.altText #altTextContainer{ + width:300px; + height:-moz-fit-content; + height:fit-content; + display:inline-flex; + flex-direction:column; + align-items:flex-start; + gap:16px; +} + +:is(.dialog.altText #altTextContainer) #overallDescription{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + align-self:stretch; +} + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) span{ + align-self:stretch; +} + +:is(:is(.dialog.altText #altTextContainer) #overallDescription) .title{ + font-size:13px; + font-style:normal; + font-weight:590; +} + +:is(.dialog.altText #altTextContainer) #addDescription{ + display:flex; + flex-direction:column; + align-items:stretch; + gap:8px; +} + +:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea{ + flex:1; + padding-inline:24px 10px; +} + +:is(:is(:is(.dialog.altText #altTextContainer) #addDescription) .descriptionArea) textarea{ + width:100%; + min-height:75px; +} + +:is(.dialog.altText #altTextContainer) #buttons{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:8px; + align-self:stretch; +} + +.dialog.newAltText{ + --new-alt-text-ai-disclaimer-icon:url(images/altText_disclaimer.svg); + --new-alt-text-spinner-icon:url(images/altText_spinner.svg); + --preview-image-bg-color:#f0f0f4; + --preview-image-border:none; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .dialog.newAltText{ + --preview-image-bg-color:#2b2a33; + } +} + +:where(html.is-dark) .dialog.newAltText{ + --preview-image-bg-color:#2b2a33; +} + +@media screen and (forced-colors: active){ + + .dialog.newAltText{ + --preview-image-bg-color:ButtonFace; + --preview-image-border:1px solid ButtonText; + } +} + +.dialog.newAltText{ + + width:80%; + max-width:570px; + min-width:300px; + padding:0; +} + +.dialog.newAltText.noAi #newAltTextDisclaimer,.dialog.newAltText.noAi #newAltTextCreateAutomatically{ + display:none !important; +} + +.dialog.newAltText.aiInstalling #newAltTextCreateAutomatically{ + display:none !important; +} + +.dialog.newAltText.aiInstalling #newAltTextDownloadModel{ + display:flex !important; +} + +.dialog.newAltText.error #newAltTextNotNow{ + display:none !important; +} + +.dialog.newAltText.error #newAltTextCancel{ + display:inline-block !important; +} + +.dialog.newAltText:not(.error) #newAltTextError{ + display:none !important; +} + +.dialog.newAltText #newAltTextContainer{ + display:flex; + width:auto; + padding:16px; + flex-direction:column; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + flex:0 1 auto; + line-height:normal; +} + +:is(.dialog.newAltText #newAltTextContainer) #mainContent{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:12px; + align-self:stretch; + flex:1 1 auto; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionAndSettings{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:16px; + flex:1 0 0; + align-self:stretch; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; + flex:1 1 auto; +} + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer{ + width:100%; + height:70px; + position:relative; +} + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea{ + width:100%; + height:100%; + padding:8px; +} + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::-moz-placeholder{ + color:var(--text-secondary-color); +} + +:is(:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea)::placeholder{ + color:var(--text-secondary-color); +} + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:none; + position:absolute; + width:16px; + height:16px; + inset-inline-start:8px; + inset-block-start:8px; + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + pointer-events:none; +} + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::-moz-placeholder{ + color:transparent; +} + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) textarea::placeholder{ + color:transparent; +} + +.loading:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescriptionContainer) .altTextSpinner{ + display:inline-block; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); +} + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDescription{ + font-size:11px; +} + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer{ + display:flex; + flex-direction:row; + align-items:flex-start; + gap:4px; + font-size:11px; +} + +:is(:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #descriptionInstruction) #newAltTextDisclaimer)::before{ + content:""; + display:inline-block; + width:17px; + height:16px; + -webkit-mask-image:var(--new-alt-text-ai-disclaimer-icon); + mask-image:var(--new-alt-text-ai-disclaimer-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); + flex:1 0 auto; +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel{ + display:flex; + align-items:center; + gap:4px; + align-self:stretch; +} + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextDownloadModel)::before{ + content:""; + display:inline-block; + width:16px; + height:16px; + -webkit-mask-image:var(--new-alt-text-spinner-icon); + mask-image:var(--new-alt-text-spinner-icon); + -webkit-mask-size:cover; + mask-size:cover; + background-color:var(--text-secondary-color); +} + +:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview{ + width:180px; + aspect-ratio:1; + display:flex; + justify-content:center; + align-items:center; + flex:0 0 auto; + background-color:var(--preview-image-bg-color); + border:var(--preview-image-border); +} + +:is(:is(:is(.dialog.newAltText #newAltTextContainer) #mainContent) #newAltTextImagePreview) > canvas{ + max-width:100%; + max-height:100%; +} + +.colorPicker{ + --hover-outline-color:#0250bb; + --selected-outline-color:#0060df; + --swatch-border-color:#cfcfd8; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) .colorPicker{ + --hover-outline-color:#80ebff; + --selected-outline-color:#aaf2ff; + --swatch-border-color:#52525e; + } +} + +:where(html.is-dark) .colorPicker{ + --hover-outline-color:#80ebff; + --selected-outline-color:#aaf2ff; + --swatch-border-color:#52525e; +} + +@media screen and (forced-colors: active){ + + .colorPicker{ + --hover-outline-color:Highlight; + --selected-outline-color:var(--hover-outline-color); + --swatch-border-color:ButtonText; + } +} + +.colorPicker .swatch{ + width:16px; + height:16px; + border:1px solid var(--swatch-border-color); + border-radius:100%; + outline-offset:2px; + box-sizing:border-box; + forced-color-adjust:none; +} + +.colorPicker button:is(:hover,.selected) > .swatch{ + border:none; +} + +.annotationEditorLayer[data-main-rotation="0"] .highlightEditor:not(.free) > .editToolbar{ + rotate:0deg; +} + +.annotationEditorLayer[data-main-rotation="90"] .highlightEditor:not(.free) > .editToolbar{ + rotate:270deg; +} + +.annotationEditorLayer[data-main-rotation="180"] .highlightEditor:not(.free) > .editToolbar{ + rotate:180deg; +} + +.annotationEditorLayer[data-main-rotation="270"] .highlightEditor:not(.free) > .editToolbar{ + rotate:90deg; +} + +.annotationEditorLayer .highlightEditor{ + position:absolute; + background:transparent; + z-index:1; + cursor:auto; + max-width:100%; + max-height:100%; + border:none; + outline:none; + pointer-events:none; + transform-origin:0 0; +} + +:is(.annotationEditorLayer .highlightEditor):not(.free){ + transform:none; +} + +:is(.annotationEditorLayer .highlightEditor) .internal{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + pointer-events:auto; +} + +.disabled:is(.annotationEditorLayer .highlightEditor) .internal{ + pointer-events:none; +} + +.selectedEditor:is(.annotationEditorLayer .highlightEditor) .internal{ + cursor:pointer; +} + +:is(.annotationEditorLayer .highlightEditor) .editToolbar{ + --editor-toolbar-colorpicker-arrow-image:url(images/toolbarButton-menuArrow.svg); + + transform-origin:center !important; +} + +:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker{ + position:relative; + width:auto; + display:flex; + justify-content:center; + align-items:center; + gap:4px; + padding:4px; +} + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker)::after{ + content:""; + -webkit-mask-image:var(--editor-toolbar-colorpicker-arrow-image); + mask-image:var(--editor-toolbar-colorpicker-arrow-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:12px; + height:12px; +} + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):hover::after{ + background-color:var(--editor-toolbar-hover-fg-color); +} + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden)){ + background-color:var(--editor-toolbar-hover-bg-color); +} + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker):has(.dropdown:not(.hidden))::after{ + scale:-1; +} + +:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown{ + position:absolute; + display:flex; + justify-content:center; + align-items:center; + flex-direction:column; + gap:11px; + padding-block:8px; + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + inset-block-start:calc(100% + 4px); + width:calc(100% + 2 * var(--editor-toolbar-padding)); +} + +:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button{ + width:100%; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; +} + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; +} + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline-offset:2px; +} + +[aria-selected="true"]:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); +} + +:is(:is(:is(:is(:is(:is(.annotationEditorLayer .highlightEditor) .editToolbar) .buttons) .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); +} + +.editorParamsToolbar:has(#highlightParamsToolbarContainer){ + padding:unset; +} + +#highlightParamsToolbarContainer{ + gap:16px; + padding-inline:10px; + padding-block-end:12px; +} + +#highlightParamsToolbarContainer .colorPicker{ + display:flex; + flex-direction:column; + gap:8px; +} + +:is(#highlightParamsToolbarContainer .colorPicker) .dropdown{ + display:flex; + justify-content:space-between; + align-items:center; + flex-direction:row; + height:auto; +} + +:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button{ + width:auto; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + flex:0 0 auto; + padding:0; +} + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) .swatch{ + width:24px; + height:24px; +} + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:active,:focus-visible){ + outline:none; +} + +[aria-selected="true"]:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button) > .swatch{ + outline:2px solid var(--selected-outline-color); +} + +:is(:is(:is(#highlightParamsToolbarContainer .colorPicker) .dropdown) button):is(:hover,:active,:focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); +} + +#highlightParamsToolbarContainer #editorHighlightThickness{ + display:flex; + flex-direction:column; + align-items:center; + gap:4px; + align-self:stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .editorParamsLabel{ + height:auto; + align-self:stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + + --example-color:#bfbfc9; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) :is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:#80808e; + } +} + +:where(html.is-dark) :is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:#80808e; +} + +@media screen and (forced-colors: active){ + + :is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker{ + --example-color:CanvasText; + } +} + +:is(:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) > .editorParamsSlider[disabled]){ + opacity:0.4; +} + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::before,:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + content:""; + width:8px; + aspect-ratio:1; + display:block; + border-radius:100%; + background-color:var(--example-color); +} + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker)::after{ + width:24px; +} + +:is(:is(#highlightParamsToolbarContainer #editorHighlightThickness) .thicknessPicker) .editorParamsSlider{ + width:unset; + height:14px; +} + +#highlightParamsToolbarContainer #editorHighlightVisibility{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#d7d7db; +} + +@media (prefers-color-scheme: dark){ + + :where(html:not(.is-light)) :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#8f8f9d; + } +} + +:where(html.is-dark) :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:#8f8f9d; +} + +@media screen and (forced-colors: active){ + + :is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + --divider-color:CanvasText; + } +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .divider{ + + margin-block:4px; + width:100%; + height:1px; + background-color:var(--divider-color); +} + +:is(#highlightParamsToolbarContainer #editorHighlightVisibility) .toggler{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; +} + +#altTextSettingsDialog{ + padding:16px; +} + +#altTextSettingsDialog #altTextSettingsContainer{ + display:flex; + width:573px; + flex-direction:column; + gap:16px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) .mainContainer{ + gap:16px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) .description{ + color:var(--text-secondary-color); +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings{ + display:flex; + flex-direction:column; + gap:12px; +} + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) button{ + width:-moz-fit-content; + width:fit-content; +} + +.download:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings) #deleteModelButton{ + display:none; +} + +:is(:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings):not(.download) #downloadModelButton{ + display:none; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticAltText,:is(#altTextSettingsDialog #altTextSettingsContainer) #altTextEditor{ + display:flex; + flex-direction:column; + gap:8px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #createModelDescription,:is(#altTextSettingsDialog #altTextSettingsContainer) #aiModelSettings,:is(#altTextSettingsDialog #altTextSettingsContainer) #showAltTextDialogDescription{ + padding-inline-start:40px; +} + +:is(#altTextSettingsDialog #altTextSettingsContainer) #automaticSettings{ + display:flex; + flex-direction:column; + gap:16px; +} + +:root{ + --viewer-container-height:0; + --pdfViewer-padding-bottom:0; + --page-margin:1px auto -8px; + --page-border:9px solid transparent; + --spreadHorizontalWrapped-margin-LR:-3.5px; + --loading-icon-delay:400ms; + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); +} + +@media (prefers-color-scheme: dark){ + + :root:where(:not(.is-light)){ + --focus-ring-color:#0df; + } +} + +:root:where(.is-dark){ + --focus-ring-color:#0df; +} + +@media screen and (forced-colors: active){ + + :root{ + --pdfViewer-padding-bottom:9px; + --page-margin:8px auto -1px; + --page-border:1px solid CanvasText; + --spreadHorizontalWrapped-margin-LR:3.5px; + --focus-ring-color:CanvasText; + } +} + +[data-main-rotation="90"]{ + transform:rotate(90deg) translateY(-100%); +} +[data-main-rotation="180"]{ + transform:rotate(180deg) translate(-100%, -100%); +} +[data-main-rotation="270"]{ + transform:rotate(270deg) translateX(-100%); +} + +#hiddenCopyElement, +.hiddenCanvasElement{ + position:absolute; + top:0; + left:0; + width:0; + height:0; + display:none; +} + +.pdfViewer{ + --scale-factor:1; + --page-bg-color:unset; + + padding-bottom:var(--pdfViewer-padding-bottom); + + --hcm-highlight-filter:none; + --hcm-highlight-selected-filter:none; +} + +@media screen and (forced-colors: active){ + + .pdfViewer{ + --hcm-highlight-filter:invert(100%); + } +} + +.pdfViewer.copyAll{ + cursor:wait; +} + +.pdfViewer .canvasWrapper{ + overflow:hidden; + width:100%; + height:100%; +} + +:is(.pdfViewer .canvasWrapper) canvas{ + position:absolute; + top:0; + left:0; + margin:0; + display:block; + width:100%; + height:100%; + contain:content; +} + +:is(:is(.pdfViewer .canvasWrapper) canvas) .structTree{ + contain:strict; +} + +.pdfViewer .page{ + --user-unit:1; + --total-scale-factor:calc(var(--scale-factor) * var(--user-unit)); + --scale-round-x:1px; + --scale-round-y:1px; + + direction:ltr; + width:816px; + height:1056px; + margin:var(--page-margin); + position:relative; + overflow:visible; + border:var(--page-border); + background-clip:content-box; + background-color:var(--page-bg-color, rgb(255 255 255)); +} + +.pdfViewer .dummyPage{ + position:relative; + width:0; + height:var(--viewer-container-height); +} + +.pdfViewer.noUserSelect{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.pdfViewer.removePageBorders .page{ + margin:0 auto 10px; + border:none; +} + +.pdfViewer:is(.scrollHorizontal, .scrollWrapped), +.spread{ + margin-inline:3.5px; + text-align:center; +} + +.pdfViewer.scrollHorizontal, +.spread{ + white-space:nowrap; +} + +.pdfViewer.removePageBorders, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread{ + margin-inline:0; +} + +.spread :is(.page, .dummyPage), +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread){ + display:inline-block; + vertical-align:middle; +} + +.spread .page, +.pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:var(--spreadHorizontalWrapped-margin-LR); +} + +.pdfViewer.removePageBorders .spread .page, +.pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:5px; +} + +.pdfViewer .page.loadingIcon::after{ + position:absolute; + top:0; + left:0; + content:""; + width:100%; + height:100%; + background:url("images/loading-icon.gif") center no-repeat; + display:none; + transition-property:display; + transition-delay:var(--loading-icon-delay); + z-index:5; + contain:strict; +} + +.pdfViewer .page.loading::after{ + display:block; +} + +.pdfViewer .page:not(.loading)::after{ + transition-property:none; + display:none; +} + +.pdfPresentationMode .pdfViewer{ + padding-bottom:0; +} + +.pdfPresentationMode .spread{ + margin:0; +} + +.pdfPresentationMode .pdfViewer .page{ + margin:0 auto; + border:2px solid transparent; +} + +:root{ + --dir-factor:1; + --inline-start:left; + --inline-end:right; + + --sidebar-width:200px; + --sidebar-transition-duration:200ms; + --sidebar-transition-timing-function:ease; + + --toolbar-height:32px; + --toolbar-horizontal-padding:1px; + --toolbar-vertical-padding:2px; + --icon-size:16px; + + --toolbar-icon-opacity:0.7; + --doorhanger-icon-opacity:0.9; + --doorhanger-height:8px; + + --main-color:rgb(12 12 13); + --body-bg-color:rgb(212 212 215); + --progressBar-color:rgb(10 132 255); + --progressBar-bg-color:rgb(221 221 222); + --progressBar-blend-color:rgb(116 177 239); + --scrollbar-color:auto; + --scrollbar-bg-color:auto; + --toolbar-icon-bg-color:rgb(0 0 0); + --toolbar-icon-hover-bg-color:rgb(0 0 0); + + --sidebar-narrow-bg-color:rgb(212 212 215 / 0.9); + --sidebar-toolbar-bg-color:rgb(245 246 247); + --toolbar-bg-color:rgb(249 249 250); + --toolbar-border-color:rgb(184 184 184); + --toolbar-box-shadow:0 1px 0 var(--toolbar-border-color); + --toolbar-border-bottom:none; + --toolbarSidebar-box-shadow:inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25), 0 1px 0 rgb(0 0 0 / 0.15), 0 0 1px rgb(0 0 0 / 0.1); + --toolbarSidebar-border-bottom:none; + --button-hover-color:color-mix(in srgb, currentColor 17%, transparent); + --toggled-btn-color:rgb(0 0 0); + --toggled-btn-bg-color:rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); + --toggled-hover-btn-outline:none; + --dropdown-btn-bg-color:rgb(215 215 219); + --dropdown-btn-border:none; + --separator-color:rgb(0 0 0 / 0.3); + --field-color:rgb(6 6 6); + --field-bg-color:rgb(255 255 255); + --field-border-color:rgb(187 187 188); + --treeitem-color:rgb(0 0 0 / 0.8); + --treeitem-bg-color:rgb(0 0 0 / 0.15); + --treeitem-hover-color:rgb(0 0 0 / 0.9); + --treeitem-selected-color:rgb(0 0 0 / 0.9); + --treeitem-selected-bg-color:rgb(0 0 0 / 0.25); + --thumbnail-hover-color:rgb(0 0 0 / 0.1); + --thumbnail-selected-color:rgb(0 0 0 / 0.2); + --doorhanger-bg-color:rgb(255 255 255); + --doorhanger-border-color:rgb(12 12 13 / 0.2); + --doorhanger-hover-color:rgb(12 12 13); + --doorhanger-separator-color:rgb(222 222 222); + --dialog-button-border:none; + --dialog-button-bg-color:rgb(12 12 13 / 0.1); + --dialog-button-hover-bg-color:rgb(12 12 13 / 0.3); + + --loading-icon:url(images/loading.svg); + --treeitem-expanded-icon:url(images/treeitem-expanded.svg); + --treeitem-collapsed-icon:url(images/treeitem-collapsed.svg); + --toolbarButton-editorFreeText-icon:url(images/toolbarButton-editorFreeText.svg); + --toolbarButton-editorHighlight-icon:url(images/toolbarButton-editorHighlight.svg); + --toolbarButton-editorInk-icon:url(images/toolbarButton-editorInk.svg); + --toolbarButton-editorStamp-icon:url(images/toolbarButton-editorStamp.svg); + --toolbarButton-editorSignature-icon:url(images/toolbarButton-editorSignature.svg); + --toolbarButton-menuArrow-icon:url(images/toolbarButton-menuArrow.svg); + --toolbarButton-sidebarToggle-icon:url(images/toolbarButton-sidebarToggle.svg); + --toolbarButton-secondaryToolbarToggle-icon:url(images/toolbarButton-secondaryToolbarToggle.svg); + --toolbarButton-pageUp-icon:url(images/toolbarButton-pageUp.svg); + --toolbarButton-pageDown-icon:url(images/toolbarButton-pageDown.svg); + --toolbarButton-zoomOut-icon:url(images/toolbarButton-zoomOut.svg); + --toolbarButton-zoomIn-icon:url(images/toolbarButton-zoomIn.svg); + --toolbarButton-presentationMode-icon:url(images/toolbarButton-presentationMode.svg); + --toolbarButton-print-icon:url(images/toolbarButton-print.svg); + --toolbarButton-openFile-icon:url(images/toolbarButton-openFile.svg); + --toolbarButton-download-icon:url(images/toolbarButton-download.svg); + --toolbarButton-bookmark-icon:url(images/toolbarButton-bookmark.svg); + --toolbarButton-viewThumbnail-icon:url(images/toolbarButton-viewThumbnail.svg); + --toolbarButton-viewOutline-icon:url(images/toolbarButton-viewOutline.svg); + --toolbarButton-viewAttachments-icon:url(images/toolbarButton-viewAttachments.svg); + --toolbarButton-viewLayers-icon:url(images/toolbarButton-viewLayers.svg); + --toolbarButton-currentOutlineItem-icon:url(images/toolbarButton-currentOutlineItem.svg); + --toolbarButton-search-icon:url(images/toolbarButton-search.svg); + --findbarButton-previous-icon:url(images/findbarButton-previous.svg); + --findbarButton-next-icon:url(images/findbarButton-next.svg); + --secondaryToolbarButton-firstPage-icon:url(images/secondaryToolbarButton-firstPage.svg); + --secondaryToolbarButton-lastPage-icon:url(images/secondaryToolbarButton-lastPage.svg); + --secondaryToolbarButton-rotateCcw-icon:url(images/secondaryToolbarButton-rotateCcw.svg); + --secondaryToolbarButton-rotateCw-icon:url(images/secondaryToolbarButton-rotateCw.svg); + --secondaryToolbarButton-selectTool-icon:url(images/secondaryToolbarButton-selectTool.svg); + --secondaryToolbarButton-handTool-icon:url(images/secondaryToolbarButton-handTool.svg); + --secondaryToolbarButton-scrollPage-icon:url(images/secondaryToolbarButton-scrollPage.svg); + --secondaryToolbarButton-scrollVertical-icon:url(images/secondaryToolbarButton-scrollVertical.svg); + --secondaryToolbarButton-scrollHorizontal-icon:url(images/secondaryToolbarButton-scrollHorizontal.svg); + --secondaryToolbarButton-scrollWrapped-icon:url(images/secondaryToolbarButton-scrollWrapped.svg); + --secondaryToolbarButton-spreadNone-icon:url(images/secondaryToolbarButton-spreadNone.svg); + --secondaryToolbarButton-spreadOdd-icon:url(images/secondaryToolbarButton-spreadOdd.svg); + --secondaryToolbarButton-spreadEven-icon:url(images/secondaryToolbarButton-spreadEven.svg); + --secondaryToolbarButton-imageAltTextSettings-icon:var( + --toolbarButton-editorStamp-icon + ); + --secondaryToolbarButton-documentProperties-icon:url(images/secondaryToolbarButton-documentProperties.svg); + --editorParams-stampAddImage-icon:url(images/toolbarButton-zoomIn.svg); +} + +[dir="rtl"]:root{ + --dir-factor:-1; + --inline-start:right; + --inline-end:left; +} + +@media (prefers-color-scheme: dark){ + :root:where(:not(.is-light)){ + --main-color:rgb(249 249 250); + --body-bg-color:rgb(42 42 46); + --progressBar-color:rgb(0 96 223); + --progressBar-bg-color:rgb(40 40 43); + --progressBar-blend-color:rgb(20 68 133); + --scrollbar-color:rgb(121 121 123); + --scrollbar-bg-color:rgb(35 35 39); + --toolbar-icon-bg-color:rgb(255 255 255); + --toolbar-icon-hover-bg-color:rgb(255 255 255); + + --sidebar-narrow-bg-color:rgb(42 42 46 / 0.9); + --sidebar-toolbar-bg-color:rgb(50 50 52); + --toolbar-bg-color:rgb(56 56 61); + --toolbar-border-color:rgb(12 12 13); + --toggled-btn-color:rgb(255 255 255); + --toggled-btn-bg-color:rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); + --dropdown-btn-bg-color:rgb(74 74 79); + --separator-color:rgb(0 0 0 / 0.3); + --field-color:rgb(250 250 250); + --field-bg-color:rgb(64 64 68); + --field-border-color:rgb(115 115 115); + --treeitem-color:rgb(255 255 255 / 0.8); + --treeitem-bg-color:rgb(255 255 255 / 0.15); + --treeitem-hover-color:rgb(255 255 255 / 0.9); + --treeitem-selected-color:rgb(255 255 255 / 0.9); + --treeitem-selected-bg-color:rgb(255 255 255 / 0.25); + --thumbnail-hover-color:rgb(255 255 255 / 0.1); + --thumbnail-selected-color:rgb(255 255 255 / 0.2); + --doorhanger-bg-color:#42414d; + --doorhanger-border-color:rgb(39 39 43); + --doorhanger-hover-color:rgb(249 249 250); + --doorhanger-separator-color:rgb(92 92 97); + --dialog-button-bg-color:rgb(92 92 97); + --dialog-button-hover-bg-color:rgb(115 115 115); + } +} + +:root:where(.is-dark){ + --main-color:rgb(249 249 250); + --body-bg-color:rgb(42 42 46); + --progressBar-color:rgb(0 96 223); + --progressBar-bg-color:rgb(40 40 43); + --progressBar-blend-color:rgb(20 68 133); + --scrollbar-color:rgb(121 121 123); + --scrollbar-bg-color:rgb(35 35 39); + --toolbar-icon-bg-color:rgb(255 255 255); + --toolbar-icon-hover-bg-color:rgb(255 255 255); + + --sidebar-narrow-bg-color:rgb(42 42 46 / 0.9); + --sidebar-toolbar-bg-color:rgb(50 50 52); + --toolbar-bg-color:rgb(56 56 61); + --toolbar-border-color:rgb(12 12 13); + --toggled-btn-color:rgb(255 255 255); + --toggled-btn-bg-color:rgb(0 0 0 / 0.3); + --toggled-hover-active-btn-color:rgb(0 0 0 / 0.4); + --dropdown-btn-bg-color:rgb(74 74 79); + --separator-color:rgb(0 0 0 / 0.3); + --field-color:rgb(250 250 250); + --field-bg-color:rgb(64 64 68); + --field-border-color:rgb(115 115 115); + --treeitem-color:rgb(255 255 255 / 0.8); + --treeitem-bg-color:rgb(255 255 255 / 0.15); + --treeitem-hover-color:rgb(255 255 255 / 0.9); + --treeitem-selected-color:rgb(255 255 255 / 0.9); + --treeitem-selected-bg-color:rgb(255 255 255 / 0.25); + --thumbnail-hover-color:rgb(255 255 255 / 0.1); + --thumbnail-selected-color:rgb(255 255 255 / 0.2); + --doorhanger-bg-color:#42414d; + --doorhanger-border-color:rgb(39 39 43); + --doorhanger-hover-color:rgb(249 249 250); + --doorhanger-separator-color:rgb(92 92 97); + --dialog-button-bg-color:rgb(92 92 97); + --dialog-button-hover-bg-color:rgb(115 115 115); +} + +@media screen and (forced-colors: active){ + :root{ + --button-hover-color:Highlight; + --toolbar-icon-opacity:1; + --toolbar-icon-bg-color:ButtonText; + --toolbar-icon-hover-bg-color:ButtonFace; + --toggled-hover-active-btn-color:ButtonText; + --toggled-hover-btn-outline:2px solid ButtonBorder; + --toolbar-border-color:CanvasText; + --toolbar-border-bottom:1px solid var(--toolbar-border-color); + --toolbar-box-shadow:none; + --toggled-btn-color:HighlightText; + --toggled-btn-bg-color:LinkText; + --doorhanger-hover-color:ButtonFace; + --doorhanger-border-color-whcm:1px solid ButtonText; + --doorhanger-triangle-opacity-whcm:0; + --dialog-button-border:1px solid Highlight; + --dialog-button-hover-bg-color:Highlight; + --dialog-button-hover-color:ButtonFace; + --dropdown-btn-border:1px solid ButtonText; + --field-border-color:ButtonText; + --main-color:CanvasText; + --separator-color:GrayText; + --doorhanger-separator-color:GrayText; + --toolbarSidebar-box-shadow:none; + --toolbarSidebar-border-bottom:1px solid var(--toolbar-border-color); + } +} + +@media screen and (prefers-reduced-motion: reduce){ + :root{ + --sidebar-transition-duration:0; + } +} + +@keyframes progressIndeterminate{ + 0%{ + transform:translateX(calc(-142px * var(--dir-factor))); + } + + 100%{ + transform:translateX(0); + } +} + +html[data-toolbar-density="compact"]{ + --toolbar-height:30px; +} + +html[data-toolbar-density="touch"]{ + --toolbar-height:44px; +} + +html, +body{ + height:100%; + width:100%; +} + +body{ + margin:0; + background-color:var(--body-bg-color); + scrollbar-color:var(--scrollbar-color) var(--scrollbar-bg-color); +} + +body.wait::before{ + content:""; + position:fixed; + width:100%; + height:100%; + z-index:100000; + cursor:wait; +} + +.hidden, +[hidden]{ + display:none !important; +} + +#viewerContainer.pdfPresentationMode:fullscreen{ + top:0; + background-color:rgb(0 0 0); + width:100%; + height:100%; + overflow:hidden; + cursor:none; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +.pdfPresentationMode:fullscreen section:not([data-internal-link]){ + pointer-events:none; +} + +.pdfPresentationMode:fullscreen .textLayer span{ + cursor:none; +} + +.pdfPresentationMode.pdfPresentationModeControls > *, +.pdfPresentationMode.pdfPresentationModeControls .textLayer span{ + cursor:default; +} + +#outerContainer{ + width:100%; + height:100%; + position:relative; + margin:0; +} + +#sidebarContainer{ + position:absolute; + inset-block:var(--toolbar-height) 0; + inset-inline-start:calc(-1 * var(--sidebar-width)); + width:var(--sidebar-width); + visibility:hidden; + z-index:1; + font:message-box; + border-top:1px solid transparent; + border-inline-end:var(--doorhanger-border-color-whcm); + transition-property:inset-inline-start; + transition-duration:var(--sidebar-transition-duration); + transition-timing-function:var(--sidebar-transition-timing-function); +} + +#outerContainer:is(.sidebarMoving, .sidebarOpen) #sidebarContainer{ + visibility:visible; +} + +#outerContainer.sidebarOpen #sidebarContainer{ + inset-inline-start:0; +} + +#mainContainer{ + position:absolute; + inset:0; + min-width:350px; + margin:0; + display:flex; + flex-direction:column; +} + +#sidebarContent{ + inset-block:var(--toolbar-height) 0; + inset-inline-start:0; + overflow:auto; + position:absolute; + width:100%; + box-shadow:inset calc(-1px * var(--dir-factor)) 0 0 rgb(0 0 0 / 0.25); +} + +#viewerContainer{ + overflow:auto; + position:absolute; + inset:var(--toolbar-height) 0 0; + outline:none; + z-index:0; +} + +#viewerContainer:not(.pdfPresentationMode){ + transition-duration:var(--sidebar-transition-duration); + transition-timing-function:var(--sidebar-transition-timing-function); +} + +#outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode){ + inset-inline-start:var(--sidebar-width); + transition-property:inset-inline-start; +} + +#sidebarContainer :is(input, button, select){ + font:message-box; +} + +.toolbar{ + z-index:2; +} + +#toolbarSidebar{ + width:100%; + height:var(--toolbar-height); + background-color:var(--sidebar-toolbar-bg-color); + box-shadow:var(--toolbarSidebar-box-shadow); + border-bottom:var(--toolbarSidebar-border-bottom); + padding:var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); + justify-content:space-between; +} + +#toolbarSidebar #toolbarSidebarLeft{ + width:auto; + height:100%; +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewThumbnail::before{ + -webkit-mask-image:var(--toolbarButton-viewThumbnail-icon); + mask-image:var(--toolbarButton-viewThumbnail-icon); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewOutline::before{ + -webkit-mask-image:var(--toolbarButton-viewOutline-icon); + mask-image:var(--toolbarButton-viewOutline-icon); + transform:scaleX(var(--dir-factor)); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewAttachments::before{ + -webkit-mask-image:var(--toolbarButton-viewAttachments-icon); + mask-image:var(--toolbarButton-viewAttachments-icon); +} + +:is(#toolbarSidebar #toolbarSidebarLeft) #viewLayers::before{ + -webkit-mask-image:var(--toolbarButton-viewLayers-icon); + mask-image:var(--toolbarButton-viewLayers-icon); +} + +#toolbarSidebar #toolbarSidebarRight{ + width:auto; + height:100%; + padding-inline-end:2px; +} + +#sidebarResizer{ + position:absolute; + inset-block:0; + inset-inline-end:-6px; + width:6px; + z-index:200; + cursor:ew-resize; +} + +#outerContainer.sidebarOpen #loadingBar{ + inset-inline-start:var(--sidebar-width); +} + +#outerContainer.sidebarResizing +:is(#sidebarContainer, #viewerContainer, #loadingBar){ + transition-duration:0s; +} + +.doorHanger, +.doorHangerRight{ + border-radius:2px; + box-shadow:0 1px 5px var(--doorhanger-border-color), 0 0 0 1px var(--doorhanger-border-color); + border:var(--doorhanger-border-color-whcm); + background-color:var(--doorhanger-bg-color); + inset-block-start:calc(100% + var(--doorhanger-height) - 2px); +} + +:is(.doorHanger,.doorHangerRight)::after,:is(.doorHanger,.doorHangerRight)::before{ + bottom:100%; + border-style:solid; + border-color:transparent; + content:""; + height:0; + width:0; + position:absolute; + pointer-events:none; + opacity:var(--doorhanger-triangle-opacity-whcm); +} + +:is(.doorHanger,.doorHangerRight)::before{ + border-width:calc(var(--doorhanger-height) + 2px); + border-bottom-color:var(--doorhanger-border-color); +} + +:is(.doorHanger,.doorHangerRight)::after{ + border-width:var(--doorhanger-height); +} + +.doorHangerRight{ + inset-inline-end:calc(50% - var(--doorhanger-height) - 1px); +} + +.doorHangerRight::before{ + inset-inline-end:-1px; +} + +.doorHangerRight::after{ + border-bottom-color:var(--doorhanger-bg-color); + inset-inline-end:1px; +} + +.doorHanger{ + inset-inline-start:calc(50% - var(--doorhanger-height) - 1px); +} + +.doorHanger::before{ + inset-inline-start:-1px; +} + +.doorHanger::after{ + border-bottom-color:var(--toolbar-bg-color); + inset-inline-start:1px; +} + +.dialogButton{ + border:none; + background:none; + width:28px; + height:28px; + outline:none; +} + +.dialogButton:is(:hover, :focus-visible){ + background-color:var(--dialog-button-hover-bg-color); +} + +.dialogButton:is(:hover, :focus-visible) > span{ + color:var(--dialog-button-hover-color); +} + +.splitToolbarButtonSeparator{ + float:var(--inline-start); + width:0; + height:62%; + border-left:1px solid var(--separator-color); + border-right:none; +} + +.dialogButton{ + min-width:16px; + margin:2px 1px; + padding:2px 6px 0; + border:none; + border-radius:2px; + color:var(--main-color); + font-size:12px; + line-height:14px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; + box-sizing:border-box; +} + +.treeItemToggler::before{ + position:absolute; + display:inline-block; + width:16px; + height:16px; + + content:""; + background-color:var(--toolbar-icon-bg-color); + -webkit-mask-size:cover; + mask-size:cover; +} + +#sidebarToggleButton::before{ + -webkit-mask-image:var(--toolbarButton-sidebarToggle-icon); + mask-image:var(--toolbarButton-sidebarToggle-icon); + transform:scaleX(var(--dir-factor)); +} + +#secondaryToolbarToggleButton::before{ + -webkit-mask-image:var(--toolbarButton-secondaryToolbarToggle-icon); + mask-image:var(--toolbarButton-secondaryToolbarToggle-icon); + transform:scaleX(var(--dir-factor)); +} + +#previous::before{ + -webkit-mask-image:var(--toolbarButton-pageUp-icon); + mask-image:var(--toolbarButton-pageUp-icon); +} + +#next::before{ + -webkit-mask-image:var(--toolbarButton-pageDown-icon); + mask-image:var(--toolbarButton-pageDown-icon); +} + +#zoomOutButton::before{ + -webkit-mask-image:var(--toolbarButton-zoomOut-icon); + mask-image:var(--toolbarButton-zoomOut-icon); +} + +#zoomInButton::before{ + -webkit-mask-image:var(--toolbarButton-zoomIn-icon); + mask-image:var(--toolbarButton-zoomIn-icon); +} + +#presentationMode::before{ + -webkit-mask-image:var(--toolbarButton-presentationMode-icon); + mask-image:var(--toolbarButton-presentationMode-icon); +} + +#editorFreeTextButton::before{ + -webkit-mask-image:var(--toolbarButton-editorFreeText-icon); + mask-image:var(--toolbarButton-editorFreeText-icon); +} + +#editorHighlightButton::before{ + -webkit-mask-image:var(--toolbarButton-editorHighlight-icon); + mask-image:var(--toolbarButton-editorHighlight-icon); +} + +#editorInkButton::before{ + -webkit-mask-image:var(--toolbarButton-editorInk-icon); + mask-image:var(--toolbarButton-editorInk-icon); +} + +#editorStampButton::before{ + -webkit-mask-image:var(--toolbarButton-editorStamp-icon); + mask-image:var(--toolbarButton-editorStamp-icon); +} + +#editorSignatureButton::before{ + -webkit-mask-image:var(--toolbarButton-editorSignature-icon); + mask-image:var(--toolbarButton-editorSignature-icon); +} + +#printButton::before{ + -webkit-mask-image:var(--toolbarButton-print-icon); + mask-image:var(--toolbarButton-print-icon); +} + +#secondaryOpenFile::before{ + -webkit-mask-image:var(--toolbarButton-openFile-icon); + mask-image:var(--toolbarButton-openFile-icon); +} + +#downloadButton::before{ + -webkit-mask-image:var(--toolbarButton-download-icon); + mask-image:var(--toolbarButton-download-icon); +} + +#viewBookmark::before{ + -webkit-mask-image:var(--toolbarButton-bookmark-icon); + mask-image:var(--toolbarButton-bookmark-icon); +} + +#currentOutlineItem::before{ + -webkit-mask-image:var(--toolbarButton-currentOutlineItem-icon); + mask-image:var(--toolbarButton-currentOutlineItem-icon); + transform:scaleX(var(--dir-factor)); +} + +#viewFindButton::before{ + -webkit-mask-image:var(--toolbarButton-search-icon); + mask-image:var(--toolbarButton-search-icon); +} + +.pdfSidebarNotification::after{ + position:absolute; + display:inline-block; + top:2px; + inset-inline-end:2px; + content:""; + background-color:rgb(112 219 85); + height:9px; + width:9px; + border-radius:50%; +} + +.verticalToolbarSeparator{ + display:block; + margin-inline:2px; + width:0; + height:80%; + border-left:1px solid var(--separator-color); + border-right:none; + box-sizing:border-box; +} + +.horizontalToolbarSeparator{ + display:block; + margin:6px 0; + border-top:1px solid var(--doorhanger-separator-color); + border-bottom:none; + height:0; + width:100%; +} + +.toggleButton{ + display:inline; +} + +.toggleButton:has( > input:checked){ + color:var(--toggled-btn-color); + background-color:var(--toggled-btn-bg-color); +} + +.toggleButton:is(:hover,:has( > input:focus-visible)){ + color:var(--toggled-btn-color); + background-color:var(--button-hover-color); +} + +.toggleButton > input{ + position:absolute; + top:50%; + left:50%; + opacity:0; + width:0; + height:0; +} + +.toolbarField{ + padding:4px 7px; + margin:3px 0; + border-radius:2px; + background-color:var(--field-bg-color); + background-clip:padding-box; + border:1px solid var(--field-border-color); + box-shadow:none; + color:var(--field-color); + font-size:12px; + line-height:16px; + outline:none; +} + +.toolbarField:focus{ + border-color:#0a84ff; +} + +#pageNumber{ + -moz-appearance:textfield; + text-align:end; + width:40px; + background-size:0 0; + transition-property:none; +} + +#pageNumber::-webkit-inner-spin-button{ + -webkit-appearance:none; +} + +.loadingInput:has( > .loading:is(#pageNumber))::after{ + display:inline; + visibility:visible; + + transition-property:visibility; + transition-delay:var(--loading-icon-delay); +} + +.loadingInput{ + position:relative; +} + +.loadingInput::after{ + position:absolute; + visibility:hidden; + display:none; + width:var(--icon-size); + height:var(--icon-size); + + content:""; + background-color:var(--toolbar-icon-bg-color); + -webkit-mask-size:cover; + mask-size:cover; + -webkit-mask-image:var(--loading-icon); + mask-image:var(--loading-icon); +} + +.loadingInput.start::after{ + inset-inline-start:4px; +} + +.loadingInput.end::after{ + inset-inline-end:4px; +} + +#thumbnailView, +#outlineView, +#attachmentsView, +#layersView{ + position:absolute; + width:calc(100% - 8px); + inset-block:0; + padding:4px 4px 0; + overflow:auto; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} + +#thumbnailView{ + width:calc(100% - 60px); + padding:10px 30px 0; +} + +#thumbnailView > a:is(:active, :focus){ + outline:0; +} + +.thumbnail{ + --thumbnail-width:0; + --thumbnail-height:0; + + float:var(--inline-start); + width:var(--thumbnail-width); + height:var(--thumbnail-height); + margin:0 10px 5px; + padding:1px; + border:7px solid transparent; + border-radius:2px; +} + +#thumbnailView > a:last-of-type > .thumbnail{ + margin-bottom:10px; +} + +a:focus > .thumbnail, +.thumbnail:hover{ + border-color:var(--thumbnail-hover-color); +} + +.thumbnail.selected{ + border-color:var(--thumbnail-selected-color) !important; +} + +.thumbnailImage{ + width:var(--thumbnail-width); + height:var(--thumbnail-height); + opacity:0.9; +} + +a:focus > .thumbnail > .thumbnailImage, +.thumbnail:hover > .thumbnailImage{ + opacity:0.95; +} + +.thumbnail.selected > .thumbnailImage{ + opacity:1 !important; +} + +.thumbnail:not([data-loaded]) > .thumbnailImage{ + width:calc(var(--thumbnail-width) - 2px); + height:calc(var(--thumbnail-height) - 2px); + border:1px dashed rgb(132 132 132); +} + +.treeWithDeepNesting > .treeItem, +.treeItem > .treeItems{ + margin-inline-start:20px; +} + +.treeItem > a{ + text-decoration:none; + display:inline-block; + min-width:calc(100% - 4px); + height:auto; + margin-bottom:1px; + padding:2px 0 5px; + padding-inline-start:4px; + border-radius:2px; + color:var(--treeitem-color); + font-size:13px; + line-height:15px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + white-space:normal; + cursor:pointer; +} + +#layersView .treeItem > a *{ + cursor:pointer; +} + +#layersView .treeItem > a > label{ + padding-inline-start:4px; +} + +#layersView .treeItem > a > label > input{ + float:var(--inline-start); + margin-top:1px; +} + +.treeItemToggler{ + position:relative; + float:var(--inline-start); + height:0; + width:0; + color:rgb(255 255 255 / 0.5); +} + +.treeItemToggler::before{ + inset-inline-end:4px; + -webkit-mask-image:var(--treeitem-expanded-icon); + mask-image:var(--treeitem-expanded-icon); +} + +.treeItemToggler.treeItemsHidden::before{ + -webkit-mask-image:var(--treeitem-collapsed-icon); + mask-image:var(--treeitem-collapsed-icon); + transform:scaleX(var(--dir-factor)); +} + +.treeItemToggler.treeItemsHidden ~ .treeItems{ + display:none; +} + +.treeItem.selected > a{ + background-color:var(--treeitem-selected-bg-color); + color:var(--treeitem-selected-color); +} + +.treeItemToggler:hover, +.treeItemToggler:hover + a, +.treeItemToggler:hover ~ .treeItems, +.treeItem > a:hover{ + background-color:var(--treeitem-bg-color); + background-clip:padding-box; + border-radius:2px; + color:var(--treeitem-hover-color); +} + +#outlineOptionsContainer{ + display:none; +} + +#sidebarContainer:has(#outlineView:not(.hidden)) #outlineOptionsContainer{ + display:inline flex; +} + +.dialogButton{ + width:auto; + margin:3px 4px 2px !important; + padding:2px 11px; + color:var(--main-color); + background-color:var(--dialog-button-bg-color); + border:var(--dialog-button-border) !important; +} + +dialog{ + margin:auto; + padding:15px; + border-spacing:4px; + color:var(--main-color); + font:message-box; + font-size:12px; + line-height:14px; + background-color:var(--doorhanger-bg-color); + border:1px solid rgb(0 0 0 / 0.5); + border-radius:4px; + box-shadow:0 1px 4px rgb(0 0 0 / 0.3); +} + +dialog::backdrop{ + background-color:rgb(0 0 0 / 0.2); +} + +dialog > .row{ + display:table-row; +} + +dialog > .row > *{ + display:table-cell; +} + +dialog .toolbarField{ + margin:5px 0; +} + +dialog .separator{ + display:block; + margin:4px 0; + height:0; + width:100%; + border-top:1px solid var(--separator-color); + border-bottom:none; +} + +dialog .buttonRow{ + text-align:center; + vertical-align:middle; +} + +dialog :link{ + color:rgb(255 255 255); +} + +#passwordDialog{ + text-align:center; +} + +#passwordDialog .toolbarField{ + width:200px; +} + +#documentPropertiesDialog{ + text-align:left; +} + +#documentPropertiesDialog .row > *{ + min-width:100px; + text-align:start; +} + +#documentPropertiesDialog .row > span{ + width:125px; + word-wrap:break-word; +} + +#documentPropertiesDialog .row > p{ + max-width:225px; + word-wrap:break-word; +} + +#documentPropertiesDialog .buttonRow{ + margin-top:10px; +} + +.grab-to-pan-grab{ + cursor:grab !important; +} + +.grab-to-pan-grab +*:not(input):not(textarea):not(button):not(select):not(:link){ + cursor:inherit !important; +} + +.grab-to-pan-grab:active, +.grab-to-pan-grabbing{ + cursor:grabbing !important; +} + +.grab-to-pan-grabbing{ + position:fixed; + background:rgb(0 0 0 / 0); + display:block; + inset:0; + overflow:hidden; + z-index:50000; +} + +.toolbarButton{ + height:100%; + aspect-ratio:1; + display:flex; + align-items:center; + justify-content:center; + background:none; + border:none; + color:var(--main-color); + outline:none; + border-radius:2px; + box-sizing:border-box; + font:message-box; + flex:none; + position:relative; + padding:0; +} + +.toolbarButton > span{ + display:inline-block; + width:0; + height:0; + overflow:hidden; +} + +.toolbarButton::before{ + opacity:var(--toolbar-icon-opacity); + display:inline-block; + width:var(--icon-size); + height:var(--icon-size); + content:""; + background-color:var(--toolbar-icon-bg-color); + -webkit-mask-size:cover; + mask-size:cover; + -webkit-mask-position:center; + mask-position:center; +} + +.toolbarButton.toggled{ + background-color:var(--toggled-btn-bg-color); + color:var(--toggled-btn-color); +} + +.toolbarButton.toggled::before{ + background-color:var(--toggled-btn-color); +} + +.toolbarButton.toggled:hover{ + outline:var(--toggled-hover-btn-outline) !important; +} + +.toolbarButton.toggled:hover:active{ + background-color:var(--toggled-hover-active-btn-color); +} + +.toolbarButton:is(:hover,:focus-visible){ + background-color:var(--button-hover-color); +} + +.toolbarButton:is(:hover,:focus-visible)::before{ + background-color:var(--toolbar-icon-hover-bg-color); +} + +.toolbarButton:is([disabled="disabled"],[disabled]){ + opacity:0.5; + pointer-events:none; +} + +.toolbarButton.labeled{ + width:100%; + min-height:var(--menuitem-height); + justify-content:flex-start; + gap:8px; + padding-inline-start:12px; + aspect-ratio:unset; + text-align:start; + white-space:normal; + cursor:default; +} + +.toolbarButton.labeled:is(a){ + text-decoration:none; +} + +.toolbarButton.labeled[href="#"]:is(a){ + opacity:0.5; + pointer-events:none; +} + +.toolbarButton.labeled::before{ + opacity:var(--doorhanger-icon-opacity); +} + +.toolbarButton.labeled:is(:hover,:focus-visible){ + color:var(--doorhanger-hover-color); +} + +.toolbarButton.labeled > span{ + display:inline-block; + width:-moz-max-content; + width:max-content; + height:auto; +} + +.toolbarButtonWithContainer{ + height:100%; + aspect-ratio:1; + display:inline-block; + position:relative; + flex:none; +} + +.toolbarButtonWithContainer > .toolbarButton{ + width:100%; + height:100%; +} + +.toolbarButtonWithContainer .menu{ + padding-block:5px; +} + +.toolbarButtonWithContainer .menuContainer{ + width:100%; + height:auto; + max-height:calc( + var(--viewer-container-height) - var(--toolbar-height) - + var(--doorhanger-height) + ); + display:flex; + flex-direction:column; + box-sizing:border-box; + overflow-y:auto; +} + +.toolbarButtonWithContainer .editorParamsToolbar{ + height:auto; + width:220px; + position:absolute; + z-index:30000; + cursor:default; +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) :is(#editorStampAddImage,#editorSignatureAddSignature)::before{ + -webkit-mask-image:var(--editorParams-stampAddImage-icon); + mask-image:var(--editorParams-stampAddImage-icon); +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsLabel{ + flex:none; + font:menu; + font-size:13px; + font-style:normal; + font-weight:400; + line-height:150%; + width:-moz-fit-content; + width:fit-content; + inset-inline-start:0; + color:var(--main-color); +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) button:is(:hover,:focus-visible) .editorParamsLabel{ + color:var(--doorhanger-hover-color); +} + +:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer{ + width:100%; + height:auto; + display:flex; + flex-direction:column; + box-sizing:border-box; + padding-inline:10px; + padding-block:10px; +} + +:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) > .editorParamsSetter{ + min-height:26px; + display:flex; + align-items:center; + justify-content:space-between; +} + +:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsColor{ + width:32px; + height:32px; + flex:none; + padding:0; +} + +:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider{ + background-color:transparent; + width:90px; + flex:0 1 0; + font:message-box; +} + +:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-progress{ + background-color:black; +} + +:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-webkit-slider-runnable-track,:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-track{ + background-color:black; +} + +:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-webkit-slider-thumb,:is(:is(:is(.toolbarButtonWithContainer .editorParamsToolbar) .editorParamsToolbarContainer) .editorParamsSlider)::-moz-range-thumb{ + background-color:white; +} + +#secondaryToolbar{ + height:auto; + width:220px; + position:absolute; + z-index:30000; + cursor:default; + min-height:26px; + max-height:calc(var(--viewer-container-height) - 40px); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryOpenFile::before{ + -webkit-mask-image:var(--toolbarButton-openFile-icon); + mask-image:var(--toolbarButton-openFile-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryPrint::before{ + -webkit-mask-image:var(--toolbarButton-print-icon); + mask-image:var(--toolbarButton-print-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #secondaryDownload::before{ + -webkit-mask-image:var(--toolbarButton-download-icon); + mask-image:var(--toolbarButton-download-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #presentationMode::before{ + -webkit-mask-image:var(--toolbarButton-presentationMode-icon); + mask-image:var(--toolbarButton-presentationMode-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #viewBookmark::before{ + -webkit-mask-image:var(--toolbarButton-bookmark-icon); + mask-image:var(--toolbarButton-bookmark-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #firstPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-firstPage-icon); + mask-image:var(--secondaryToolbarButton-firstPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #lastPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-lastPage-icon); + mask-image:var(--secondaryToolbarButton-lastPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCcw::before{ + -webkit-mask-image:var(--secondaryToolbarButton-rotateCcw-icon); + mask-image:var(--secondaryToolbarButton-rotateCcw-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #pageRotateCw::before{ + -webkit-mask-image:var(--secondaryToolbarButton-rotateCw-icon); + mask-image:var(--secondaryToolbarButton-rotateCw-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #cursorSelectTool::before{ + -webkit-mask-image:var(--secondaryToolbarButton-selectTool-icon); + mask-image:var(--secondaryToolbarButton-selectTool-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #cursorHandTool::before{ + -webkit-mask-image:var(--secondaryToolbarButton-handTool-icon); + mask-image:var(--secondaryToolbarButton-handTool-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollPage-icon); + mask-image:var(--secondaryToolbarButton-scrollPage-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollVertical::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollVertical-icon); + mask-image:var(--secondaryToolbarButton-scrollVertical-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollHorizontal::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); + mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #scrollWrapped::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); + mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadNone::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadNone-icon); + mask-image:var(--secondaryToolbarButton-spreadNone-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadOdd::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadOdd-icon); + mask-image:var(--secondaryToolbarButton-spreadOdd-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #spreadEven::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadEven-icon); + mask-image:var(--secondaryToolbarButton-spreadEven-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #imageAltTextSettings::before{ + -webkit-mask-image:var(--secondaryToolbarButton-imageAltTextSettings-icon); + mask-image:var(--secondaryToolbarButton-imageAltTextSettings-icon); +} + +:is(#secondaryToolbar #secondaryToolbarButtonContainer) #documentProperties::before{ + -webkit-mask-image:var(--secondaryToolbarButton-documentProperties-icon); + mask-image:var(--secondaryToolbarButton-documentProperties-icon); +} + +#findbar{ + --input-horizontal-padding:4px; + --findbar-padding:2px; + + width:-moz-max-content; + + width:max-content; + max-width:90vw; + min-height:var(--toolbar-height); + height:auto; + position:absolute; + z-index:30000; + cursor:default; + padding:0; + min-width:300px; + background-color:var(--toolbar-bg-color); + box-sizing:border-box; + flex-wrap:wrap; + justify-content:flex-start; +} + +#findbar > *{ + height:var(--toolbar-height); + padding:var(--findbar-padding); +} + +#findbar #findInputContainer{ + margin-inline-start:2px; +} + +:is(#findbar #findInputContainer) #findPreviousButton::before{ + -webkit-mask-image:var(--findbarButton-previous-icon); + mask-image:var(--findbarButton-previous-icon); +} + +:is(#findbar #findInputContainer) #findNextButton::before{ + -webkit-mask-image:var(--findbarButton-next-icon); + mask-image:var(--findbarButton-next-icon); +} + +:is(#findbar #findInputContainer) #findInput{ + width:200px; + padding:5px var(--input-horizontal-padding); +} + +:is(:is(#findbar #findInputContainer) #findInput)::-moz-placeholder{ + font-style:normal; +} + +:is(:is(#findbar #findInputContainer) #findInput)::placeholder{ + font-style:normal; +} + +.loadingInput:has( > [data-status="pending"]:is(:is(#findbar #findInputContainer) #findInput))::after{ + display:inline; + visibility:visible; + inset-inline-end:calc(var(--input-horizontal-padding) + 1px); +} + +[data-status="notFound"]:is(:is(#findbar #findInputContainer) #findInput){ + background-color:rgb(255 102 102); +} + +#findbar #findbarMessageContainer{ + display:none; + gap:4px; +} + +:is(#findbar #findbarMessageContainer):has( > :is(#findResultsCount,#findMsg):not(:empty)){ + display:inline flex; +} + +:is(#findbar #findbarMessageContainer) #findResultsCount{ + background-color:rgb(217 217 217); + color:rgb(82 82 82); + padding-block:4px; +} + +:is(:is(#findbar #findbarMessageContainer) #findResultsCount):empty{ + display:none; +} + +[data-status="notFound"]:is(:is(#findbar #findbarMessageContainer) #findMsg){ + font-weight:bold; +} + +:is(:is(#findbar #findbarMessageContainer) #findMsg):empty{ + display:none; +} + +#findbar.wrapContainers{ + flex-direction:column; + align-items:flex-start; + height:-moz-max-content; + height:max-content; +} + +#findbar.wrapContainers .toolbarLabel{ + margin:0 4px; +} + +#findbar.wrapContainers #findbarMessageContainer{ + flex-wrap:wrap; + flex-flow:column nowrap; + align-items:flex-start; + height:-moz-max-content; + height:max-content; +} + +:is(#findbar.wrapContainers #findbarMessageContainer) #findResultsCount{ + height:calc(var(--toolbar-height) - 2 * var(--findbar-padding)); +} + +:is(#findbar.wrapContainers #findbarMessageContainer) #findMsg{ + min-height:var(--toolbar-height); +} + +@page{ + margin:0; +} + +#printContainer{ + display:none; +} + +@media print{ + body{ + background:rgb(0 0 0 / 0) none; + } + + body[data-pdfjsprinting] #outerContainer{ + display:none; + } + + body[data-pdfjsprinting] #printContainer{ + display:block; + } + + #printContainer{ + height:100%; + } + #printContainer > .printedPage{ + page-break-after:always; + page-break-inside:avoid; + height:100%; + width:100%; + + display:flex; + flex-direction:column; + justify-content:center; + align-items:center; + } + + #printContainer > .xfaPrintedPage .xfaPage{ + position:absolute; + } + + #printContainer > .xfaPrintedPage{ + page-break-after:always; + page-break-inside:avoid; + width:100%; + height:100%; + position:relative; + } + + #printContainer > .printedPage :is(canvas, img){ + max-width:100%; + max-height:100%; + + direction:ltr; + display:block; + } +} + +.visibleMediumView{ + display:none !important; +} + +.toolbarLabel{ + width:-moz-max-content; + width:max-content; + min-width:16px; + height:100%; + padding-inline:4px; + margin:2px; + border-radius:2px; + color:var(--main-color); + font-size:12px; + line-height:14px; + text-align:left; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; + box-sizing:border-box; + + display:inline flex; + flex-direction:column; + align-items:center; + justify-content:center; +} + +.toolbarLabel > label{ + width:100%; +} + +.toolbarHorizontalGroup{ + height:100%; + display:inline flex; + flex-direction:row; + align-items:center; + justify-content:space-between; + gap:1px; + box-sizing:border-box; +} + +.dropdownToolbarButton{ + display:inline flex; + flex-direction:row; + align-items:center; + justify-content:center; + position:relative; + + width:-moz-fit-content; + + width:fit-content; + min-width:140px; + padding:0; + background-color:var(--dropdown-btn-bg-color); + border:var(--dropdown-btn-border); + border-radius:2px; + color:var(--main-color); + font-size:12px; + line-height:14px; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + cursor:default; + box-sizing:border-box; + outline:none; +} + +.dropdownToolbarButton:hover{ + background-color:var(--button-hover-color); +} + +.dropdownToolbarButton > select{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + width:inherit; + min-width:inherit; + height:28px; + font:message-box; + font-size:12px; + color:var(--main-color); + margin:0; + padding-block:1px 2px; + padding-inline:6px 38px; + border:none; + outline:none; + background-color:var(--dropdown-btn-bg-color); +} + +:is(.dropdownToolbarButton > select) > option{ + background:var(--doorhanger-bg-color); + color:var(--main-color); +} + +:is(.dropdownToolbarButton > select):is(:hover,:focus-visible){ + background-color:var(--button-hover-color); + color:var(--toggled-btn-color); +} + +.dropdownToolbarButton::after{ + position:absolute; + display:inline; + width:var(--icon-size); + height:var(--icon-size); + + content:""; + background-color:var(--toolbar-icon-bg-color); + -webkit-mask-size:cover; + mask-size:cover; + + inset-inline-end:4px; + pointer-events:none; + -webkit-mask-image:var(--toolbarButton-menuArrow-icon); + mask-image:var(--toolbarButton-menuArrow-icon); +} + +.dropdownToolbarButton:is(:hover,:focus-visible,:active)::after{ + background-color:var(--toolbar-icon-hover-bg-color); +} + +#toolbarContainer{ + --menuitem-height:calc(var(--toolbar-height) - 6px); + + width:100%; + height:var(--toolbar-height); + padding:var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); + position:relative; + box-sizing:border-box; + font:message-box; + background-color:var(--toolbar-bg-color); + box-shadow:var(--toolbar-box-shadow); + border-bottom:var(--toolbar-border-bottom); +} + +#toolbarContainer #toolbarViewer{ + width:100%; + height:100%; + justify-content:space-between; +} + +:is(#toolbarContainer #toolbarViewer) > *{ + flex:none; +} + +:is(#toolbarContainer #toolbarViewer) input{ + font:message-box; +} + +:is(#toolbarContainer #toolbarViewer) .toolbarButtonSpacer{ + width:30px; + display:block; + height:1px; +} + +:is(#toolbarContainer #toolbarViewer) #toolbarViewerLeft #numPages.toolbarLabel{ + padding-inline-start:3px; + flex:none; +} + +#toolbarContainer #loadingBar{ + --progressBar-percent:0%; + --progressBar-end-offset:0; + + position:absolute; + top:var(--toolbar-height); + inset-inline:0 var(--progressBar-end-offset); + height:4px; + background-color:var(--progressBar-bg-color); + border-bottom:1px solid var(--toolbar-border-color); + transition-property:inset-inline-start; + transition-duration:var(--sidebar-transition-duration); + transition-timing-function:var(--sidebar-transition-timing-function); +} + +:is(#toolbarContainer #loadingBar) .progress{ + position:absolute; + top:0; + inset-inline-start:0; + width:100%; + transform:scaleX(var(--progressBar-percent)); + transform-origin:calc(50% - 50% * var(--dir-factor)) 0; + height:100%; + background-color:var(--progressBar-color); + overflow:hidden; + transition:transform 200ms; +} + +.indeterminate:is(#toolbarContainer #loadingBar) .progress{ + transform:none; + background-color:var(--progressBar-bg-color); + transition:none; +} + +:is(.indeterminate:is(#toolbarContainer #loadingBar) .progress) .glimmer{ + position:absolute; + top:0; + inset-inline-start:0; + height:100%; + width:calc(100% + 150px); + background:repeating-linear-gradient( + 135deg, + var(--progressBar-blend-color) 0, + var(--progressBar-bg-color) 5px, + var(--progressBar-bg-color) 45px, + var(--progressBar-color) 55px, + var(--progressBar-color) 95px, + var(--progressBar-blend-color) 100px + ); + animation:progressIndeterminate 1s linear infinite; +} + +#secondaryToolbar #firstPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-firstPage-icon); + mask-image:var(--secondaryToolbarButton-firstPage-icon); +} + +#secondaryToolbar #lastPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-lastPage-icon); + mask-image:var(--secondaryToolbarButton-lastPage-icon); +} + +#secondaryToolbar #pageRotateCcw::before{ + -webkit-mask-image:var(--secondaryToolbarButton-rotateCcw-icon); + mask-image:var(--secondaryToolbarButton-rotateCcw-icon); +} + +#secondaryToolbar #pageRotateCw::before{ + -webkit-mask-image:var(--secondaryToolbarButton-rotateCw-icon); + mask-image:var(--secondaryToolbarButton-rotateCw-icon); +} + +#secondaryToolbar #cursorSelectTool::before{ + -webkit-mask-image:var(--secondaryToolbarButton-selectTool-icon); + mask-image:var(--secondaryToolbarButton-selectTool-icon); +} + +#secondaryToolbar #cursorHandTool::before{ + -webkit-mask-image:var(--secondaryToolbarButton-handTool-icon); + mask-image:var(--secondaryToolbarButton-handTool-icon); +} + +#secondaryToolbar #scrollPage::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollPage-icon); + mask-image:var(--secondaryToolbarButton-scrollPage-icon); +} + +#secondaryToolbar #scrollVertical::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollVertical-icon); + mask-image:var(--secondaryToolbarButton-scrollVertical-icon); +} + +#secondaryToolbar #scrollHorizontal::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); + mask-image:var(--secondaryToolbarButton-scrollHorizontal-icon); +} + +#secondaryToolbar #scrollWrapped::before{ + -webkit-mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); + mask-image:var(--secondaryToolbarButton-scrollWrapped-icon); +} + +#secondaryToolbar #spreadNone::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadNone-icon); + mask-image:var(--secondaryToolbarButton-spreadNone-icon); +} + +#secondaryToolbar #spreadOdd::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadOdd-icon); + mask-image:var(--secondaryToolbarButton-spreadOdd-icon); +} + +#secondaryToolbar #spreadEven::before{ + -webkit-mask-image:var(--secondaryToolbarButton-spreadEven-icon); + mask-image:var(--secondaryToolbarButton-spreadEven-icon); +} + +#secondaryToolbar #documentProperties::before{ + -webkit-mask-image:var(--secondaryToolbarButton-documentProperties-icon); + mask-image:var(--secondaryToolbarButton-documentProperties-icon); +} + +@media all and (max-width: 840px){ + #sidebarContainer{ + background-color:var(--sidebar-narrow-bg-color); + } + #outerContainer.sidebarOpen #viewerContainer{ + inset-inline-start:0 !important; + } +} + +@media all and (max-width: 750px){ + #outerContainer .hiddenMediumView{ + display:none !important; + } + #outerContainer .visibleMediumView:not(.hidden, [hidden]){ + display:inline-block !important; + } +} + +@media all and (max-width: 690px){ + .hiddenSmallView, + .hiddenSmallView *{ + display:none !important; + } + + #toolbarContainer #toolbarViewer .toolbarButtonSpacer{ + width:0; + } +} + +@media all and (max-width: 560px){ + #scaleSelectContainer{ + display:none; + } +} \ No newline at end of file diff --git a/public/assets/pdfjs/viewer.mjs b/public/assets/pdfjs/viewer.mjs new file mode 100644 index 0000000..03417e6 --- /dev/null +++ b/public/assets/pdfjs/viewer.mjs @@ -0,0 +1,16311 @@ +/** + * @licstart The following is the entire license notice for the + * JavaScript code in this page + * + * Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @licend The above is the entire license notice for the + * JavaScript code in this page + */ + +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { + /******/ // define getter functions for harmony exports + /******/ __webpack_require__.d = (exports, definition) => { + /******/ for (var key in definition) { + /******/ if ( + __webpack_require__.o(definition, key) && + !__webpack_require__.o(exports, key) + ) { + /******/ Object.defineProperty(exports, key, { + enumerable: true, + get: definition[key], + }); + /******/ + } + /******/ + } + /******/ + }; + /******/ +})(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { + /******/ __webpack_require__.o = (obj, prop) => + Object.prototype.hasOwnProperty.call(obj, prop); + /******/ +})(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + PDFViewerApplication: () => /* reexport */ PDFViewerApplication, + PDFViewerApplicationConstants: () => /* binding */ AppConstants, + PDFViewerApplicationOptions: () => /* reexport */ AppOptions, +}); // ./web/ui_utils.js +const pageParams = new URLSearchParams(window.location.search); +const DEFAULT_SCALE_VALUE = "auto"; +const DEFAULT_SCALE = 1.0; +const DEFAULT_SCALE_DELTA = 1.1; +const MIN_SCALE = 0.1; +const MAX_SCALE = 10.0; +const UNKNOWN_SCALE = 0; +const MAX_AUTO_SCALE = 1.25; +const SCROLLBAR_PADDING = 40; +const VERTICAL_PADDING = 5; +const RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3, +}; +const PresentationModeState = { + UNKNOWN: 0, + NORMAL: 1, + CHANGING: 2, + FULLSCREEN: 3, +}; +const SidebarView = { + UNKNOWN: -1, + NONE: 0, + THUMBS: 1, + OUTLINE: 2, + ATTACHMENTS: 3, + LAYERS: 4, +}; +const TextLayerMode = { + DISABLE: 0, + ENABLE: 1, + ENABLE_PERMISSIONS: 2, +}; +const ScrollMode = { + UNKNOWN: -1, + VERTICAL: 0, + HORIZONTAL: 1, + WRAPPED: 2, + PAGE: 3, +}; +const SpreadMode = { + UNKNOWN: -1, + NONE: 0, + ODD: 1, + EVEN: 2, +}; +const CursorTool = { + SELECT: 0, + HAND: 1, + ZOOM: 2, +}; +const AutoPrintRegExp = /\bprint\s*\(/; +function scrollIntoView(element, spot, scrollMatches = false) { + let parent = element.offsetParent; + if (!parent) { + console.error("offsetParent is not set -- cannot scroll"); + return; + } + let offsetY = element.offsetTop + element.clientTop; + let offsetX = element.offsetLeft + element.clientLeft; + while ( + (parent.clientHeight === parent.scrollHeight && + parent.clientWidth === parent.scrollWidth) || + (scrollMatches && + (parent.classList.contains("markedContent") || + getComputedStyle(parent).overflow === "hidden")) + ) { + offsetY += parent.offsetTop; + offsetX += parent.offsetLeft; + parent = parent.offsetParent; + if (!parent) { + return; + } + } + if (spot) { + if (spot.top !== undefined) { + offsetY += spot.top; + } + if (spot.left !== undefined) { + offsetX += spot.left; + parent.scrollLeft = offsetX; + } + } + parent.scrollTop = offsetY; +} +function watchScroll(viewAreaElement, callback, abortSignal = undefined) { + const debounceScroll = function (evt) { + if (rAF) { + return; + } + rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { + rAF = null; + const currentX = viewAreaElement.scrollLeft; + const lastX = state.lastX; + if (currentX !== lastX) { + state.right = currentX > lastX; + } + state.lastX = currentX; + const currentY = viewAreaElement.scrollTop; + const lastY = state.lastY; + if (currentY !== lastY) { + state.down = currentY > lastY; + } + state.lastY = currentY; + callback(state); + }); + }; + const state = { + right: true, + down: true, + lastX: viewAreaElement.scrollLeft, + lastY: viewAreaElement.scrollTop, + _eventHandler: debounceScroll, + }; + let rAF = null; + viewAreaElement.addEventListener("scroll", debounceScroll, { + useCapture: true, + signal: abortSignal, + }); + abortSignal?.addEventListener( + "abort", + () => window.cancelAnimationFrame(rAF), + { + once: true, + }, + ); + return state; +} +function parseQueryString(query) { + const params = new Map(); + for (const [key, value] of new URLSearchParams(query)) { + params.set(key.toLowerCase(), value); + } + return params; +} +const InvisibleCharsRegExp = /[\x00-\x1F]/g; +function removeNullCharacters(str, replaceInvisible = false) { + if (!InvisibleCharsRegExp.test(str)) { + return str; + } + if (replaceInvisible) { + return str.replaceAll(InvisibleCharsRegExp, (m) => + m === "\x00" ? "" : " ", + ); + } + return str.replaceAll("\x00", ""); +} +function binarySearchFirstItem(items, condition, start = 0) { + let minIndex = start; + let maxIndex = items.length - 1; + if (maxIndex < 0 || !condition(items[maxIndex])) { + return items.length; + } + if (condition(items[minIndex])) { + return minIndex; + } + while (minIndex < maxIndex) { + const currentIndex = (minIndex + maxIndex) >> 1; + const currentItem = items[currentIndex]; + if (condition(currentItem)) { + maxIndex = currentIndex; + } else { + minIndex = currentIndex + 1; + } + } + return minIndex; +} +function approximateFraction(x) { + if (Math.floor(x) === x) { + return [x, 1]; + } + const xinv = 1 / x; + const limit = 8; + if (xinv > limit) { + return [1, limit]; + } else if (Math.floor(xinv) === xinv) { + return [1, xinv]; + } + const x_ = x > 1 ? xinv : x; + let a = 0, + b = 1, + c = 1, + d = 1; + while (true) { + const p = a + c, + q = b + d; + if (q > limit) { + break; + } + if (x_ <= p / q) { + c = p; + d = q; + } else { + a = p; + b = q; + } + } + let result; + if (x_ - a / b < c / d - x_) { + result = x_ === x ? [a, b] : [b, a]; + } else { + result = x_ === x ? [c, d] : [d, c]; + } + return result; +} +function floorToDivide(x, div) { + return x - (x % div); +} +function getPageSizeInches({ view, userUnit, rotate }) { + const [x1, y1, x2, y2] = view; + const changeOrientation = rotate % 180 !== 0; + const width = ((x2 - x1) / 72) * userUnit; + const height = ((y2 - y1) / 72) * userUnit; + return { + width: changeOrientation ? height : width, + height: changeOrientation ? width : height, + }; +} +function backtrackBeforeAllVisibleElements(index, views, top) { + if (index < 2) { + return index; + } + let elt = views[index].div; + let pageTop = elt.offsetTop + elt.clientTop; + if (pageTop >= top) { + elt = views[index - 1].div; + pageTop = elt.offsetTop + elt.clientTop; + } + for (let i = index - 2; i >= 0; --i) { + elt = views[i].div; + if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) { + break; + } + index = i; + } + return index; +} +function getVisibleElements({ + scrollEl, + views, + sortByVisibility = false, + horizontal = false, + rtl = false, +}) { + const top = scrollEl.scrollTop, + bottom = top + scrollEl.clientHeight; + const left = scrollEl.scrollLeft, + right = left + scrollEl.clientWidth; + function isElementBottomAfterViewTop(view) { + const element = view.div; + const elementBottom = + element.offsetTop + element.clientTop + element.clientHeight; + return elementBottom > top; + } + function isElementNextAfterViewHorizontally(view) { + const element = view.div; + const elementLeft = element.offsetLeft + element.clientLeft; + const elementRight = elementLeft + element.clientWidth; + return rtl ? elementLeft < right : elementRight > left; + } + const visible = [], + ids = new Set(), + numViews = views.length; + let firstVisibleElementInd = binarySearchFirstItem( + views, + horizontal + ? isElementNextAfterViewHorizontally + : isElementBottomAfterViewTop, + ); + if ( + firstVisibleElementInd > 0 && + firstVisibleElementInd < numViews && + !horizontal + ) { + firstVisibleElementInd = backtrackBeforeAllVisibleElements( + firstVisibleElementInd, + views, + top, + ); + } + let lastEdge = horizontal ? right : -1; + for (let i = firstVisibleElementInd; i < numViews; i++) { + const view = views[i], + element = view.div; + const currentWidth = element.offsetLeft + element.clientLeft; + const currentHeight = element.offsetTop + element.clientTop; + const viewWidth = element.clientWidth, + viewHeight = element.clientHeight; + const viewRight = currentWidth + viewWidth; + const viewBottom = currentHeight + viewHeight; + if (lastEdge === -1) { + if (viewBottom >= bottom) { + lastEdge = viewBottom; + } + } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) { + break; + } + if ( + viewBottom <= top || + currentHeight >= bottom || + viewRight <= left || + currentWidth >= right + ) { + continue; + } + const hiddenHeight = + Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom); + const hiddenWidth = + Math.max(0, left - currentWidth) + Math.max(0, viewRight - right); + const fractionHeight = (viewHeight - hiddenHeight) / viewHeight, + fractionWidth = (viewWidth - hiddenWidth) / viewWidth; + const percent = (fractionHeight * fractionWidth * 100) | 0; + visible.push({ + id: view.id, + x: currentWidth, + y: currentHeight, + view, + percent, + widthPercent: (fractionWidth * 100) | 0, + }); + ids.add(view.id); + } + const first = visible[0], + last = visible.at(-1); + if (sortByVisibility) { + visible.sort(function (a, b) { + const pc = a.percent - b.percent; + if (Math.abs(pc) > 0.001) { + return -pc; + } + return a.id - b.id; + }); + } + return { + first, + last, + views: visible, + ids, + }; +} +function normalizeWheelEventDirection(evt) { + let delta = Math.hypot(evt.deltaX, evt.deltaY); + const angle = Math.atan2(evt.deltaY, evt.deltaX); + if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) { + delta = -delta; + } + return delta; +} +function normalizeWheelEventDelta(evt) { + const deltaMode = evt.deltaMode; + let delta = normalizeWheelEventDirection(evt); + const MOUSE_PIXELS_PER_LINE = 30; + const MOUSE_LINES_PER_PAGE = 30; + if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE; + } else if (deltaMode === WheelEvent.DOM_DELTA_LINE) { + delta /= MOUSE_LINES_PER_PAGE; + } + return delta; +} +function isValidRotation(angle) { + return Number.isInteger(angle) && angle % 90 === 0; +} +function isValidScrollMode(mode) { + return ( + Number.isInteger(mode) && + Object.values(ScrollMode).includes(mode) && + mode !== ScrollMode.UNKNOWN + ); +} +function isValidSpreadMode(mode) { + return ( + Number.isInteger(mode) && + Object.values(SpreadMode).includes(mode) && + mode !== SpreadMode.UNKNOWN + ); +} +function isPortraitOrientation(size) { + return size.width <= size.height; +} +const animationStarted = new Promise(function (resolve) { + window.requestAnimationFrame(resolve); +}); +const docStyle = document.documentElement.style; +function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); +} +class ProgressBar { + #classList = null; + #disableAutoFetchTimeout = null; + #percent = 0; + #style = null; + #visible = true; + constructor(bar) { + this.#classList = bar.classList; + this.#style = bar.style; + } + get percent() { + return this.#percent; + } + set percent(val) { + this.#percent = clamp(val, 0, 100); + if (isNaN(val)) { + this.#classList.add("indeterminate"); + return; + } + this.#classList.remove("indeterminate"); + this.#style.setProperty("--progressBar-percent", `${this.#percent}%`); + } + setWidth(viewer) { + if (!viewer) { + return; + } + const container = viewer.parentNode; + const scrollbarWidth = container.offsetWidth - viewer.offsetWidth; + if (scrollbarWidth > 0) { + this.#style.setProperty( + "--progressBar-end-offset", + `${scrollbarWidth}px`, + ); + } + } + setDisableAutoFetch(delay = 5000) { + if (this.#percent === 100 || isNaN(this.#percent)) { + return; + } + if (this.#disableAutoFetchTimeout) { + clearTimeout(this.#disableAutoFetchTimeout); + } + this.show(); + this.#disableAutoFetchTimeout = setTimeout(() => { + this.#disableAutoFetchTimeout = null; + this.hide(); + }, delay); + } + hide() { + if (!this.#visible) { + return; + } + this.#visible = false; + this.#classList.add("hidden"); + } + show() { + if (this.#visible) { + return; + } + this.#visible = true; + this.#classList.remove("hidden"); + } +} +function getActiveOrFocusedElement() { + let curRoot = document; + let curActiveOrFocused = + curRoot.activeElement || curRoot.querySelector(":focus"); + while (curActiveOrFocused?.shadowRoot) { + curRoot = curActiveOrFocused.shadowRoot; + curActiveOrFocused = + curRoot.activeElement || curRoot.querySelector(":focus"); + } + return curActiveOrFocused; +} +function apiPageLayoutToViewerModes(layout) { + let scrollMode = ScrollMode.VERTICAL, + spreadMode = SpreadMode.NONE; + switch (layout) { + case "SinglePage": + scrollMode = ScrollMode.PAGE; + break; + case "OneColumn": + break; + case "TwoPageLeft": + scrollMode = ScrollMode.PAGE; + case "TwoColumnLeft": + spreadMode = SpreadMode.ODD; + break; + case "TwoPageRight": + scrollMode = ScrollMode.PAGE; + case "TwoColumnRight": + spreadMode = SpreadMode.EVEN; + break; + } + return { + scrollMode, + spreadMode, + }; +} +function apiPageModeToSidebarView(mode) { + switch (mode) { + case "UseNone": + return SidebarView.NONE; + case "UseThumbs": + return SidebarView.THUMBS; + case "UseOutlines": + return SidebarView.OUTLINE; + case "UseAttachments": + return SidebarView.ATTACHMENTS; + case "UseOC": + return SidebarView.LAYERS; + } + return SidebarView.NONE; +} +function toggleCheckedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-checked", toggle); + view?.classList.toggle("hidden", !toggle); +} +function toggleExpandedBtn(button, toggle, view = null) { + button.classList.toggle("toggled", toggle); + button.setAttribute("aria-expanded", toggle); + view?.classList.toggle("hidden", !toggle); +} +const calcRound = (function () { + const e = document.createElement("div"); + e.style.width = "round(down, calc(1.6666666666666665 * 792px), 1px)"; + return e.style.width === "calc(1320px)" ? Math.fround : (x) => x; +})(); // ./web/app_options.js + +{ + var compatParams = new Map(); + const userAgent = navigator.userAgent || ""; + const platform = navigator.platform || ""; + const maxTouchPoints = navigator.maxTouchPoints || 1; + const isAndroid = /Android/.test(userAgent); + const isIOS = + /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || + (platform === "MacIntel" && maxTouchPoints > 1); + (function () { + if (isIOS || isAndroid) { + compatParams.set("maxCanvasPixels", 5242880); + } + })(); + (function () { + if (isAndroid) { + compatParams.set("useSystemFonts", false); + } + })(); +} +const OptionKind = { + BROWSER: 0x01, + VIEWER: 0x02, + API: 0x04, + WORKER: 0x08, + EVENT_DISPATCH: 0x10, + PREFERENCE: 0x80, +}; +const Type = { + BOOLEAN: 0x01, + NUMBER: 0x02, + OBJECT: 0x04, + STRING: 0x08, + UNDEFINED: 0x10, +}; +const defaultOptions = { + allowedGlobalEvents: { + value: null, + kind: OptionKind.BROWSER, + }, + canvasMaxAreaInBytes: { + value: -1, + kind: OptionKind.BROWSER + OptionKind.API, + }, + isInAutomation: { + value: false, + kind: OptionKind.BROWSER, + }, + localeProperties: { + value: { + lang: pageParams.get("lng") || navigator.language || "en-US", + }, + kind: OptionKind.BROWSER, + }, + nimbusDataStr: { + value: "", + kind: OptionKind.BROWSER, + }, + supportsCaretBrowsingMode: { + value: false, + kind: OptionKind.BROWSER, + }, + supportsDocumentFonts: { + value: true, + kind: OptionKind.BROWSER, + }, + supportsIntegratedFind: { + value: false, + kind: OptionKind.BROWSER, + }, + supportsMouseWheelZoomCtrlKey: { + value: true, + kind: OptionKind.BROWSER, + }, + supportsMouseWheelZoomMetaKey: { + value: true, + kind: OptionKind.BROWSER, + }, + supportsPinchToZoom: { + value: true, + kind: OptionKind.BROWSER, + }, + toolbarDensity: { + value: 0, + kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH, + }, + altTextLearnMoreUrl: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + annotationEditorMode: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + annotationMode: { + value: 2, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + cursorToolOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + debuggerSrc: { + value: "./debugger.mjs", + kind: OptionKind.VIEWER, + }, + defaultZoomDelay: { + value: 400, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + defaultZoomValue: { + value: "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + disableHistory: { + value: false, + kind: OptionKind.VIEWER, + }, + disablePageLabels: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableAltText: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableAltTextModelDownload: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH, + }, + enableGuessAltText: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH, + }, + enableHighlightFloatingButton: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableNewAltTextWhenAddingImage: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enablePermissions: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enablePrintAutoRotate: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableScripting: { + value: true, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableUpdatedAddImage: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + externalLinkRel: { + value: "noopener noreferrer nofollow", + kind: OptionKind.VIEWER, + }, + externalLinkTarget: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + highlightEditorColors: { + value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + historyUpdateUrl: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + ignoreDestinationZoom: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + imageResourcesPath: { + value: "./images/", + kind: OptionKind.VIEWER, + }, + maxCanvasPixels: { + value: 2 ** 25, + kind: OptionKind.VIEWER, + }, + forcePageColors: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + pageColorsBackground: { + value: "Canvas", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + pageColorsForeground: { + value: "CanvasText", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + pdfBugEnabled: { + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + printResolution: { + value: 150, + kind: OptionKind.VIEWER, + }, + sidebarViewOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + scrollModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + spreadModeOnLoad: { + value: -1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + textLayerMode: { + value: 1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + viewOnLoad: { + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + cMapPacked: { + value: true, + kind: OptionKind.API, + }, + cMapUrl: { + value: "/assets/pdfjs/cmaps/", + kind: OptionKind.API, + }, + disableAutoFetch: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE, + }, + disableFontFace: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE, + }, + disableRange: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE, + }, + disableStream: { + value: false, + kind: OptionKind.API + OptionKind.PREFERENCE, + }, + docBaseUrl: { + value: "", + kind: OptionKind.API, + }, + enableHWA: { + value: true, + kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + enableXfa: { + value: true, + kind: OptionKind.API + OptionKind.PREFERENCE, + }, + fontExtraProperties: { + value: false, + kind: OptionKind.API, + }, + isEvalSupported: { + value: true, + kind: OptionKind.API, + }, + isOffscreenCanvasSupported: { + value: true, + kind: OptionKind.API, + }, + maxImageSize: { + value: -1, + kind: OptionKind.API, + }, + pdfBug: { + value: false, + kind: OptionKind.API, + }, + standardFontDataUrl: { + value: "/assets/pdfjs/standard_fonts/", + kind: OptionKind.API, + }, + useSystemFonts: { + value: undefined, + kind: OptionKind.API, + type: Type.BOOLEAN + Type.UNDEFINED, + }, + verbosity: { + value: 1, + kind: OptionKind.API, + }, + workerPort: { + value: null, + kind: OptionKind.WORKER, + }, + workerSrc: { + value: "/assets/pdfjs/pdf.worker.mjs", + kind: OptionKind.WORKER, + }, +}; +{ + defaultOptions.defaultUrl = { + value: "compressed.tracemonkey-pldi-09.pdf", + kind: OptionKind.VIEWER, + }; + defaultOptions.sandboxBundleSrc = { + value: "/assets/pdfjs/pdf.sandbox.mjs", + kind: OptionKind.VIEWER, + }; + defaultOptions.viewerCssTheme = { + value: parseInt(pageParams.get("darkMode")) || 1, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }; + defaultOptions.enableFakeMLManager = { + value: true, + kind: OptionKind.VIEWER, + }; +} +{ + defaultOptions.disablePreferences = { + value: false, + kind: OptionKind.VIEWER, + }; +} +class AppOptions { + static eventBus; + static #opts = new Map(); + static { + for (const name in defaultOptions) { + this.#opts.set(name, defaultOptions[name].value); + } + for (const [name, value] of compatParams) { + this.#opts.set(name, value); + } + this._hasInvokedSet = false; + this._checkDisablePreferences = () => { + if (this.get("disablePreferences")) { + return true; + } + if (this._hasInvokedSet) { + console.warn( + "The Preferences may override manually set AppOptions; " + + 'please use the "disablePreferences"-option to prevent that.', + ); + } + return false; + }; + } + static get(name) { + return this.#opts.get(name); + } + static getAll(kind = null, defaultOnly = false) { + const options = Object.create(null); + for (const name in defaultOptions) { + const defaultOpt = defaultOptions[name]; + if (kind && !(kind & defaultOpt.kind)) { + continue; + } + options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value; + } + return options; + } + static set(name, value) { + this.setAll({ + [name]: value, + }); + } + static setAll(options, prefs = false) { + this._hasInvokedSet ||= true; + let events; + for (const name in options) { + const defaultOpt = defaultOptions[name], + userOpt = options[name]; + if ( + !defaultOpt || + !( + typeof userOpt === typeof defaultOpt.value || + Type[(typeof userOpt).toUpperCase()] & defaultOpt.type + ) + ) { + continue; + } + const { kind } = defaultOpt; + if ( + prefs && + !(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE) + ) { + continue; + } + if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { + (events ||= new Map()).set(name, userOpt); + } + this.#opts.set(name, userOpt); + } + if (events) { + for (const [name, value] of events) { + this.eventBus.dispatch(name.toLowerCase(), { + source: this, + value, + }); + } + } + } +} // ./web/pdf_link_service.js + +const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; +const LinkTarget = { + NONE: 0, + SELF: 1, + BLANK: 2, + PARENT: 3, + TOP: 4, +}; +class PDFLinkService { + externalLinkEnabled = true; + constructor({ + eventBus, + externalLinkTarget = null, + externalLinkRel = null, + ignoreDestinationZoom = false, + } = {}) { + this.eventBus = eventBus; + this.externalLinkTarget = externalLinkTarget; + this.externalLinkRel = externalLinkRel; + this._ignoreDestinationZoom = ignoreDestinationZoom; + this.baseUrl = null; + this.pdfDocument = null; + this.pdfViewer = null; + this.pdfHistory = null; + } + setDocument(pdfDocument, baseUrl = null) { + this.baseUrl = baseUrl; + this.pdfDocument = pdfDocument; + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setHistory(pdfHistory) { + this.pdfHistory = pdfHistory; + } + get pagesCount() { + return this.pdfDocument ? this.pdfDocument.numPages : 0; + } + get page() { + return this.pdfDocument ? this.pdfViewer.currentPageNumber : 1; + } + set page(value) { + if (this.pdfDocument) { + this.pdfViewer.currentPageNumber = value; + } + } + get rotation() { + return this.pdfDocument ? this.pdfViewer.pagesRotation : 0; + } + set rotation(value) { + if (this.pdfDocument) { + this.pdfViewer.pagesRotation = value; + } + } + get isInPresentationMode() { + return this.pdfDocument ? this.pdfViewer.isInPresentationMode : false; + } + async goToDestination(dest) { + if (!this.pdfDocument) { + return; + } + let namedDest, explicitDest, pageNumber; + if (typeof dest === "string") { + namedDest = dest; + explicitDest = await this.pdfDocument.getDestination(dest); + } else { + namedDest = null; + explicitDest = await dest; + } + if (!Array.isArray(explicitDest)) { + console.error( + `goToDestination: "${explicitDest}" is not a valid destination array, for dest="${dest}".`, + ); + return; + } + const [destRef] = explicitDest; + if (destRef && typeof destRef === "object") { + pageNumber = this.pdfDocument.cachedPageNumber(destRef); + if (!pageNumber) { + try { + pageNumber = (await this.pdfDocument.getPageIndex(destRef)) + 1; + } catch { + console.error( + `goToDestination: "${destRef}" is not a valid page reference, for dest="${dest}".`, + ); + return; + } + } + } else if (Number.isInteger(destRef)) { + pageNumber = destRef + 1; + } + if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) { + console.error( + `goToDestination: "${pageNumber}" is not a valid page number, for dest="${dest}".`, + ); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.push({ + namedDest, + explicitDest, + pageNumber, + }); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber, + destArray: explicitDest, + ignoreDestinationZoom: this._ignoreDestinationZoom, + }); + } + goToPage(val) { + if (!this.pdfDocument) { + return; + } + const pageNumber = + (typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val)) || + val | 0; + if ( + !( + Number.isInteger(pageNumber) && + pageNumber > 0 && + pageNumber <= this.pagesCount + ) + ) { + console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`); + return; + } + if (this.pdfHistory) { + this.pdfHistory.pushCurrentPosition(); + this.pdfHistory.pushPage(pageNumber); + } + this.pdfViewer.scrollPageIntoView({ + pageNumber, + }); + } + addLinkAttributes(link, url, newWindow = false) { + if (!url || typeof url !== "string") { + throw new Error('A valid "url" parameter must provided.'); + } + const target = newWindow ? LinkTarget.BLANK : this.externalLinkTarget, + rel = this.externalLinkRel; + if (this.externalLinkEnabled) { + link.href = link.title = url; + } else { + link.href = ""; + link.title = `Disabled: ${url}`; + link.onclick = () => false; + } + let targetStr = ""; + switch (target) { + case LinkTarget.NONE: + break; + case LinkTarget.SELF: + targetStr = "_self"; + break; + case LinkTarget.BLANK: + targetStr = "_blank"; + break; + case LinkTarget.PARENT: + targetStr = "_parent"; + break; + case LinkTarget.TOP: + targetStr = "_top"; + break; + } + link.target = targetStr; + link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL; + } + getDestinationHash(dest) { + if (typeof dest === "string") { + if (dest.length > 0) { + return this.getAnchorUrl("#" + escape(dest)); + } + } else if (Array.isArray(dest)) { + const str = JSON.stringify(dest); + if (str.length > 0) { + return this.getAnchorUrl("#" + escape(str)); + } + } + return this.getAnchorUrl(""); + } + getAnchorUrl(anchor) { + return this.baseUrl ? this.baseUrl + anchor : anchor; + } + setHash(hash) { + if (!this.pdfDocument) { + return; + } + let pageNumber, dest; + if (hash.includes("=")) { + const params = parseQueryString(hash); + if (params.has("search")) { + const query = params.get("search").replaceAll('"', ""), + phrase = params.get("phrase") === "true"; + this.eventBus.dispatch("findfromurlhash", { + source: this, + query: phrase ? query : query.match(/\S+/g), + }); + } + if (params.has("page")) { + pageNumber = params.get("page") | 0 || 1; + } + if (params.has("zoom")) { + const zoomArgs = params.get("zoom").split(","); + const zoomArg = zoomArgs[0]; + const zoomArgNumber = parseFloat(zoomArg); + if (!zoomArg.includes("Fit")) { + dest = [ + null, + { + name: "XYZ", + }, + zoomArgs.length > 1 ? zoomArgs[1] | 0 : null, + zoomArgs.length > 2 ? zoomArgs[2] | 0 : null, + zoomArgNumber ? zoomArgNumber / 100 : zoomArg, + ]; + } else if (zoomArg === "Fit" || zoomArg === "FitB") { + dest = [ + null, + { + name: zoomArg, + }, + ]; + } else if ( + zoomArg === "FitH" || + zoomArg === "FitBH" || + zoomArg === "FitV" || + zoomArg === "FitBV" + ) { + dest = [ + null, + { + name: zoomArg, + }, + zoomArgs.length > 1 ? zoomArgs[1] | 0 : null, + ]; + } else if (zoomArg === "FitR") { + if (zoomArgs.length !== 5) { + console.error( + 'PDFLinkService.setHash: Not enough parameters for "FitR".', + ); + } else { + dest = [ + null, + { + name: zoomArg, + }, + zoomArgs[1] | 0, + zoomArgs[2] | 0, + zoomArgs[3] | 0, + zoomArgs[4] | 0, + ]; + } + } else { + console.error( + `PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`, + ); + } + } + if (dest) { + this.pdfViewer.scrollPageIntoView({ + pageNumber: pageNumber || this.page, + destArray: dest, + allowNegativeOffset: true, + }); + } else if (pageNumber) { + this.page = pageNumber; + } + if (params.has("pagemode")) { + this.eventBus.dispatch("pagemode", { + source: this, + mode: params.get("pagemode"), + }); + } + if (params.has("nameddest")) { + this.goToDestination(params.get("nameddest")); + } + return; + } + dest = unescape(hash); + try { + dest = JSON.parse(dest); + if (!Array.isArray(dest)) { + dest = dest.toString(); + } + } catch {} + if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) { + this.goToDestination(dest); + return; + } + console.error( + `PDFLinkService.setHash: "${unescape(hash)}" is not a valid destination.`, + ); + } + executeNamedAction(action) { + if (!this.pdfDocument) { + return; + } + switch (action) { + case "GoBack": + this.pdfHistory?.back(); + break; + case "GoForward": + this.pdfHistory?.forward(); + break; + case "NextPage": + this.pdfViewer.nextPage(); + break; + case "PrevPage": + this.pdfViewer.previousPage(); + break; + case "LastPage": + this.page = this.pagesCount; + break; + case "FirstPage": + this.page = 1; + break; + default: + break; + } + this.eventBus.dispatch("namedaction", { + source: this, + action, + }); + } + async executeSetOCGState(action) { + if (!this.pdfDocument) { + return; + } + const pdfDocument = this.pdfDocument, + optionalContentConfig = await this.pdfViewer.optionalContentConfigPromise; + if (pdfDocument !== this.pdfDocument) { + return; + } + optionalContentConfig.setOCGState(action); + this.pdfViewer.optionalContentConfigPromise = Promise.resolve( + optionalContentConfig, + ); + } + static #isValidExplicitDest(dest) { + if (!Array.isArray(dest) || dest.length < 2) { + return false; + } + const [page, zoom, ...args] = dest; + if ( + !( + typeof page === "object" && + Number.isInteger(page?.num) && + Number.isInteger(page?.gen) + ) && + !Number.isInteger(page) + ) { + return false; + } + if (!(typeof zoom === "object" && typeof zoom?.name === "string")) { + return false; + } + const argsLen = args.length; + let allowNull = true; + switch (zoom.name) { + case "XYZ": + if (argsLen < 2 || argsLen > 3) { + return false; + } + break; + case "Fit": + case "FitB": + return argsLen === 0; + case "FitH": + case "FitBH": + case "FitV": + case "FitBV": + if (argsLen > 1) { + return false; + } + break; + case "FitR": + if (argsLen !== 4) { + return false; + } + allowNull = false; + break; + default: + return false; + } + for (const arg of args) { + if (!(typeof arg === "number" || (allowNull && arg === null))) { + return false; + } + } + return true; + } +} +class SimpleLinkService extends PDFLinkService { + setDocument(pdfDocument, baseUrl = null) {} +} // ./web/pdfjs.js + +const { + AbortException, + AnnotationEditorLayer, + AnnotationEditorParamsType, + AnnotationEditorType, + AnnotationEditorUIManager, + AnnotationLayer, + AnnotationMode, + build, + ColorPicker, + createValidAbsoluteUrl, + DOMSVGFactory, + DrawLayer, + FeatureTest, + fetchData, + getDocument, + getFilenameFromUrl, + getPdfFilenameFromUrl: pdfjs_getPdfFilenameFromUrl, + getXfaPageViewport, + GlobalWorkerOptions, + ImageKind, + InvalidPDFException, + isDataScheme, + isPdfFile, + MissingPDFException, + noContextMenu, + normalizeUnicode, + OPS, + OutputScale, + PasswordResponses, + PDFDataRangeTransport, + PDFDateString, + PDFWorker, + PermissionFlag, + PixelsPerInch, + RenderingCancelledException, + setLayerDimensions, + shadow, + stopEvent, + TextLayer, + TouchManager, + UnexpectedResponseException, + Util, + VerbosityLevel, + version, + XfaLayer, +} = globalThis.pdfjsLib; // ./web/event_utils.js + +const WaitOnType = { + EVENT: "event", + TIMEOUT: "timeout", +}; +async function waitOnEventOrTimeout({ target, name, delay = 0 }) { + if ( + typeof target !== "object" || + !(name && typeof name === "string") || + !(Number.isInteger(delay) && delay >= 0) + ) { + throw new Error("waitOnEventOrTimeout - invalid parameters."); + } + const { promise, resolve } = Promise.withResolvers(); + const ac = new AbortController(); + function handler(type) { + ac.abort(); + clearTimeout(timeout); + resolve(type); + } + const evtMethod = target instanceof EventBus ? "_on" : "addEventListener"; + target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), { + signal: ac.signal, + }); + const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay); + return promise; +} +class EventBus { + #listeners = Object.create(null); + on(eventName, listener, options = null) { + this._on(eventName, listener, { + external: true, + once: options?.once, + signal: options?.signal, + }); + } + off(eventName, listener, options = null) { + this._off(eventName, listener); + } + dispatch(eventName, data) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; + } + let externalListeners; + for (const { listener, external, once } of eventListeners.slice(0)) { + if (once) { + this._off(eventName, listener); + } + if (external) { + (externalListeners ||= []).push(listener); + continue; + } + listener(data); + } + if (externalListeners) { + for (const listener of externalListeners) { + listener(data); + } + externalListeners = null; + } + } + _on(eventName, listener, options = null) { + let rmAbort = null; + if (options?.signal instanceof AbortSignal) { + const { signal } = options; + if (signal.aborted) { + console.error("Cannot use an `aborted` signal."); + return; + } + const onAbort = () => this._off(eventName, listener); + rmAbort = () => signal.removeEventListener("abort", onAbort); + signal.addEventListener("abort", onAbort); + } + const eventListeners = (this.#listeners[eventName] ||= []); + eventListeners.push({ + listener, + external: options?.external === true, + once: options?.once === true, + rmAbort, + }); + } + _off(eventName, listener, options = null) { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners) { + return; + } + for (let i = 0, ii = eventListeners.length; i < ii; i++) { + const evt = eventListeners[i]; + if (evt.listener === listener) { + evt.rmAbort?.(); + eventListeners.splice(i, 1); + return; + } + } + } +} +class FirefoxEventBus extends EventBus { + #externalServices; + #globalEventNames; + #isInAutomation; + constructor(globalEventNames, externalServices, isInAutomation) { + super(); + this.#globalEventNames = globalEventNames; + this.#externalServices = externalServices; + this.#isInAutomation = isInAutomation; + } + dispatch(eventName, data) { + throw new Error("Not implemented: FirefoxEventBus.dispatch"); + } +} // ./web/external_services.js + +class BaseExternalServices { + updateFindControlState(data) {} + updateFindMatchesCount(data) {} + initPassiveLoading() {} + reportTelemetry(data) {} + async createL10n() { + throw new Error("Not implemented: createL10n"); + } + createScripting() { + throw new Error("Not implemented: createScripting"); + } + updateEditorStates(data) { + throw new Error("Not implemented: updateEditorStates"); + } + dispatchGlobalEvent(_event) {} +} // ./web/preferences.js + +class BasePreferences { + #defaults = Object.freeze({ + altTextLearnMoreUrl: "", + annotationEditorMode: 0, + annotationMode: 2, + cursorToolOnLoad: 0, + defaultZoomDelay: 400, + defaultZoomValue: "", + disablePageLabels: false, + enableAltText: false, + enableAltTextModelDownload: true, + enableGuessAltText: true, + enableHighlightFloatingButton: false, + enableNewAltTextWhenAddingImage: true, + enablePermissions: false, + enablePrintAutoRotate: true, + enableScripting: true, + enableUpdatedAddImage: false, + externalLinkTarget: 0, + highlightEditorColors: + "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", + historyUpdateUrl: false, + ignoreDestinationZoom: false, + forcePageColors: false, + pageColorsBackground: "Canvas", + pageColorsForeground: "CanvasText", + pdfBugEnabled: false, + sidebarViewOnLoad: -1, + scrollModeOnLoad: -1, + spreadModeOnLoad: -1, + textLayerMode: 1, + viewOnLoad: 0, + disableAutoFetch: false, + disableFontFace: false, + disableRange: false, + disableStream: false, + enableHWA: true, + enableXfa: true, + viewerCssTheme: 0, + }); + #initializedPromise = null; + constructor() { + this.#initializedPromise = this._readFromStorage(this.#defaults).then( + ({ browserPrefs, prefs }) => { + if (AppOptions._checkDisablePreferences()) { + return; + } + AppOptions.setAll( + { + ...browserPrefs, + ...prefs, + }, + true, + ); + }, + ); + } + async _writeToStorage(prefObj) { + throw new Error("Not implemented: _writeToStorage"); + } + async _readFromStorage(prefObj) { + throw new Error("Not implemented: _readFromStorage"); + } + async reset() { + await this.#initializedPromise; + AppOptions.setAll(this.#defaults, true); + await this._writeToStorage(this.#defaults); + } + async set(name, value) { + await this.#initializedPromise; + AppOptions.setAll( + { + [name]: value, + }, + true, + ); + await this._writeToStorage(AppOptions.getAll(OptionKind.PREFERENCE)); + } + async get(name) { + await this.#initializedPromise; + return AppOptions.get(name); + } + get initializedPromise() { + return this.#initializedPromise; + } +} // ./node_modules/@fluent/bundle/esm/types.js + +class FluentType { + constructor(value) { + this.value = value; + } + valueOf() { + return this.value; + } +} +class FluentNone extends FluentType { + constructor(value = "???") { + super(value); + } + toString(scope) { + return `{${this.value}}`; + } +} +class FluentNumber extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts); + return nf.format(this.value); + } catch (err) { + scope.reportError(err); + return this.value.toString(10); + } + } +} +class FluentDateTime extends FluentType { + constructor(value, opts = {}) { + super(value); + this.opts = opts; + } + toString(scope) { + try { + const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts); + return dtf.format(this.value); + } catch (err) { + scope.reportError(err); + return new Date(this.value).toISOString(); + } + } +} // ./node_modules/@fluent/bundle/esm/resolver.js +const MAX_PLACEABLES = 100; +const FSI = "\u2068"; +const PDI = "\u2069"; +function match(scope, selector, key) { + if (key === selector) { + return true; + } + if ( + key instanceof FluentNumber && + selector instanceof FluentNumber && + key.value === selector.value + ) { + return true; + } + if (selector instanceof FluentNumber && typeof key === "string") { + let category = scope + .memoizeIntlObject(Intl.PluralRules, selector.opts) + .select(selector.value); + if (key === category) { + return true; + } + } + return false; +} +function getDefault(scope, variants, star) { + if (variants[star]) { + return resolvePattern(scope, variants[star].value); + } + scope.reportError(new RangeError("No default")); + return new FluentNone(); +} +function getArguments(scope, args) { + const positional = []; + const named = Object.create(null); + for (const arg of args) { + if (arg.type === "narg") { + named[arg.name] = resolveExpression(scope, arg.value); + } else { + positional.push(resolveExpression(scope, arg)); + } + } + return { + positional, + named, + }; +} +function resolveExpression(scope, expr) { + switch (expr.type) { + case "str": + return expr.value; + case "num": + return new FluentNumber(expr.value, { + minimumFractionDigits: expr.precision, + }); + case "var": + return resolveVariableReference(scope, expr); + case "mesg": + return resolveMessageReference(scope, expr); + case "term": + return resolveTermReference(scope, expr); + case "func": + return resolveFunctionReference(scope, expr); + case "select": + return resolveSelectExpression(scope, expr); + default: + return new FluentNone(); + } +} +function resolveVariableReference(scope, { name }) { + let arg; + if (scope.params) { + if (Object.prototype.hasOwnProperty.call(scope.params, name)) { + arg = scope.params[name]; + } else { + return new FluentNone(`$${name}`); + } + } else if ( + scope.args && + Object.prototype.hasOwnProperty.call(scope.args, name) + ) { + arg = scope.args[name]; + } else { + scope.reportError(new ReferenceError(`Unknown variable: $${name}`)); + return new FluentNone(`$${name}`); + } + if (arg instanceof FluentType) { + return arg; + } + switch (typeof arg) { + case "string": + return arg; + case "number": + return new FluentNumber(arg); + case "object": + if (arg instanceof Date) { + return new FluentDateTime(arg.getTime()); + } + default: + scope.reportError( + new TypeError(`Variable type not supported: $${name}, ${typeof arg}`), + ); + return new FluentNone(`$${name}`); + } +} +function resolveMessageReference(scope, { name, attr }) { + const message = scope.bundle._messages.get(name); + if (!message) { + scope.reportError(new ReferenceError(`Unknown message: ${name}`)); + return new FluentNone(name); + } + if (attr) { + const attribute = message.attributes[attr]; + if (attribute) { + return resolvePattern(scope, attribute); + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${name}.${attr}`); + } + if (message.value) { + return resolvePattern(scope, message.value); + } + scope.reportError(new ReferenceError(`No value: ${name}`)); + return new FluentNone(name); +} +function resolveTermReference(scope, { name, attr, args }) { + const id = `-${name}`; + const term = scope.bundle._terms.get(id); + if (!term) { + scope.reportError(new ReferenceError(`Unknown term: ${id}`)); + return new FluentNone(id); + } + if (attr) { + const attribute = term.attributes[attr]; + if (attribute) { + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, attribute); + scope.params = null; + return resolved; + } + scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`)); + return new FluentNone(`${id}.${attr}`); + } + scope.params = getArguments(scope, args).named; + const resolved = resolvePattern(scope, term.value); + scope.params = null; + return resolved; +} +function resolveFunctionReference(scope, { name, args }) { + let func = scope.bundle._functions[name]; + if (!func) { + scope.reportError(new ReferenceError(`Unknown function: ${name}()`)); + return new FluentNone(`${name}()`); + } + if (typeof func !== "function") { + scope.reportError(new TypeError(`Function ${name}() is not callable`)); + return new FluentNone(`${name}()`); + } + try { + let resolved = getArguments(scope, args); + return func(resolved.positional, resolved.named); + } catch (err) { + scope.reportError(err); + return new FluentNone(`${name}()`); + } +} +function resolveSelectExpression(scope, { selector, variants, star }) { + let sel = resolveExpression(scope, selector); + if (sel instanceof FluentNone) { + return getDefault(scope, variants, star); + } + for (const variant of variants) { + const key = resolveExpression(scope, variant.key); + if (match(scope, sel, key)) { + return resolvePattern(scope, variant.value); + } + } + return getDefault(scope, variants, star); +} +function resolveComplexPattern(scope, ptn) { + if (scope.dirty.has(ptn)) { + scope.reportError(new RangeError("Cyclic reference")); + return new FluentNone(); + } + scope.dirty.add(ptn); + const result = []; + const useIsolating = scope.bundle._useIsolating && ptn.length > 1; + for (const elem of ptn) { + if (typeof elem === "string") { + result.push(scope.bundle._transform(elem)); + continue; + } + scope.placeables++; + if (scope.placeables > MAX_PLACEABLES) { + scope.dirty.delete(ptn); + throw new RangeError( + `Too many placeables expanded: ${scope.placeables}, ` + + `max allowed is ${MAX_PLACEABLES}`, + ); + } + if (useIsolating) { + result.push(FSI); + } + result.push(resolveExpression(scope, elem).toString(scope)); + if (useIsolating) { + result.push(PDI); + } + } + scope.dirty.delete(ptn); + return result.join(""); +} +function resolvePattern(scope, value) { + if (typeof value === "string") { + return scope.bundle._transform(value); + } + return resolveComplexPattern(scope, value); +} // ./node_modules/@fluent/bundle/esm/scope.js +class Scope { + constructor(bundle, errors, args) { + this.dirty = new WeakSet(); + this.params = null; + this.placeables = 0; + this.bundle = bundle; + this.errors = errors; + this.args = args; + } + reportError(error) { + if (!this.errors || !(error instanceof Error)) { + throw error; + } + this.errors.push(error); + } + memoizeIntlObject(ctor, opts) { + let cache = this.bundle._intls.get(ctor); + if (!cache) { + cache = {}; + this.bundle._intls.set(ctor, cache); + } + let id = JSON.stringify(opts); + if (!cache[id]) { + cache[id] = new ctor(this.bundle.locales, opts); + } + return cache[id]; + } +} // ./node_modules/@fluent/bundle/esm/builtins.js +function values(opts, allowed) { + const unwrapped = Object.create(null); + for (const [name, opt] of Object.entries(opts)) { + if (allowed.includes(name)) { + unwrapped[name] = opt.valueOf(); + } + } + return unwrapped; +} +const NUMBER_ALLOWED = [ + "unitDisplay", + "currencyDisplay", + "useGrouping", + "minimumIntegerDigits", + "minimumFractionDigits", + "maximumFractionDigits", + "minimumSignificantDigits", + "maximumSignificantDigits", +]; +function NUMBER(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`NUMBER(${arg.valueOf()})`); + } + if (arg instanceof FluentNumber) { + return new FluentNumber(arg.valueOf(), { + ...arg.opts, + ...values(opts, NUMBER_ALLOWED), + }); + } + if (arg instanceof FluentDateTime) { + return new FluentNumber(arg.valueOf(), { + ...values(opts, NUMBER_ALLOWED), + }); + } + throw new TypeError("Invalid argument to NUMBER"); +} +const DATETIME_ALLOWED = [ + "dateStyle", + "timeStyle", + "fractionalSecondDigits", + "dayPeriod", + "hour12", + "weekday", + "era", + "year", + "month", + "day", + "hour", + "minute", + "second", + "timeZoneName", +]; +function DATETIME(args, opts) { + let arg = args[0]; + if (arg instanceof FluentNone) { + return new FluentNone(`DATETIME(${arg.valueOf()})`); + } + if (arg instanceof FluentDateTime) { + return new FluentDateTime(arg.valueOf(), { + ...arg.opts, + ...values(opts, DATETIME_ALLOWED), + }); + } + if (arg instanceof FluentNumber) { + return new FluentDateTime(arg.valueOf(), { + ...values(opts, DATETIME_ALLOWED), + }); + } + throw new TypeError("Invalid argument to DATETIME"); +} // ./node_modules/@fluent/bundle/esm/memoizer.js +const cache = new Map(); +function getMemoizerForLocale(locales) { + const stringLocale = Array.isArray(locales) ? locales.join(" ") : locales; + let memoizer = cache.get(stringLocale); + if (memoizer === undefined) { + memoizer = new Map(); + cache.set(stringLocale, memoizer); + } + return memoizer; +} // ./node_modules/@fluent/bundle/esm/bundle.js +class FluentBundle { + constructor( + locales, + { functions, useIsolating = true, transform = (v) => v } = {}, + ) { + this._terms = new Map(); + this._messages = new Map(); + this.locales = Array.isArray(locales) ? locales : [locales]; + this._functions = { + NUMBER: NUMBER, + DATETIME: DATETIME, + ...functions, + }; + this._useIsolating = useIsolating; + this._transform = transform; + this._intls = getMemoizerForLocale(locales); + } + hasMessage(id) { + return this._messages.has(id); + } + getMessage(id) { + return this._messages.get(id); + } + addResource(res, { allowOverrides = false } = {}) { + const errors = []; + for (let i = 0; i < res.body.length; i++) { + let entry = res.body[i]; + if (entry.id.startsWith("-")) { + if (allowOverrides === false && this._terms.has(entry.id)) { + errors.push( + new Error(`Attempt to override an existing term: "${entry.id}"`), + ); + continue; + } + this._terms.set(entry.id, entry); + } else { + if (allowOverrides === false && this._messages.has(entry.id)) { + errors.push( + new Error(`Attempt to override an existing message: "${entry.id}"`), + ); + continue; + } + this._messages.set(entry.id, entry); + } + } + return errors; + } + formatPattern(pattern, args = null, errors = null) { + if (typeof pattern === "string") { + return this._transform(pattern); + } + let scope = new Scope(this, errors, args); + try { + let value = resolveComplexPattern(scope, pattern); + return value.toString(scope); + } catch (err) { + if (scope.errors && err instanceof Error) { + scope.errors.push(err); + return new FluentNone().toString(scope); + } + throw err; + } + } +} // ./node_modules/@fluent/bundle/esm/resource.js +const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm; +const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y; +const RE_VARIANT_START = /\*?\[/y; +const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y; +const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y; +const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y; +const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/; +const RE_TEXT_RUN = /([^{}\n\r]+)/y; +const RE_STRING_RUN = /([^\\"\n\r]*)/y; +const RE_STRING_ESCAPE = /\\([\\"])/y; +const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y; +const RE_LEADING_NEWLINES = /^\n+/; +const RE_TRAILING_SPACES = / +$/; +const RE_BLANK_LINES = / *\r?\n/g; +const RE_INDENT = /( *)$/; +const TOKEN_BRACE_OPEN = /{\s*/y; +const TOKEN_BRACE_CLOSE = /\s*}/y; +const TOKEN_BRACKET_OPEN = /\[\s*/y; +const TOKEN_BRACKET_CLOSE = /\s*] */y; +const TOKEN_PAREN_OPEN = /\s*\(\s*/y; +const TOKEN_ARROW = /\s*->\s*/y; +const TOKEN_COLON = /\s*:\s*/y; +const TOKEN_COMMA = /\s*,?\s*/y; +const TOKEN_BLANK = /\s+/y; +class FluentResource { + constructor(source) { + this.body = []; + RE_MESSAGE_START.lastIndex = 0; + let cursor = 0; + while (true) { + let next = RE_MESSAGE_START.exec(source); + if (next === null) { + break; + } + cursor = RE_MESSAGE_START.lastIndex; + try { + this.body.push(parseMessage(next[1])); + } catch (err) { + if (err instanceof SyntaxError) { + continue; + } + throw err; + } + } + function test(re) { + re.lastIndex = cursor; + return re.test(source); + } + function consumeChar(char, errorClass) { + if (source[cursor] === char) { + cursor++; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${char}`); + } + return false; + } + function consumeToken(re, errorClass) { + if (test(re)) { + cursor = re.lastIndex; + return true; + } + if (errorClass) { + throw new errorClass(`Expected ${re.toString()}`); + } + return false; + } + function match(re) { + re.lastIndex = cursor; + let result = re.exec(source); + if (result === null) { + throw new SyntaxError(`Expected ${re.toString()}`); + } + cursor = re.lastIndex; + return result; + } + function match1(re) { + return match(re)[1]; + } + function parseMessage(id) { + let value = parsePattern(); + let attributes = parseAttributes(); + if (value === null && Object.keys(attributes).length === 0) { + throw new SyntaxError("Expected message value or attributes"); + } + return { + id, + value, + attributes, + }; + } + function parseAttributes() { + let attrs = Object.create(null); + while (test(RE_ATTRIBUTE_START)) { + let name = match1(RE_ATTRIBUTE_START); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected attribute value"); + } + attrs[name] = value; + } + return attrs; + } + function parsePattern() { + let first; + if (test(RE_TEXT_RUN)) { + first = match1(RE_TEXT_RUN); + } + if (source[cursor] === "{" || source[cursor] === "}") { + return parsePatternElements(first ? [first] : [], Infinity); + } + let indent = parseIndent(); + if (indent) { + if (first) { + return parsePatternElements([first, indent], indent.length); + } + indent.value = trim(indent.value, RE_LEADING_NEWLINES); + return parsePatternElements([indent], indent.length); + } + if (first) { + return trim(first, RE_TRAILING_SPACES); + } + return null; + } + function parsePatternElements(elements = [], commonIndent) { + while (true) { + if (test(RE_TEXT_RUN)) { + elements.push(match1(RE_TEXT_RUN)); + continue; + } + if (source[cursor] === "{") { + elements.push(parsePlaceable()); + continue; + } + if (source[cursor] === "}") { + throw new SyntaxError("Unbalanced closing brace"); + } + let indent = parseIndent(); + if (indent) { + elements.push(indent); + commonIndent = Math.min(commonIndent, indent.length); + continue; + } + break; + } + let lastIndex = elements.length - 1; + let lastElement = elements[lastIndex]; + if (typeof lastElement === "string") { + elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES); + } + let baked = []; + for (let element of elements) { + if (element instanceof Indent) { + element = element.value.slice(0, element.value.length - commonIndent); + } + if (element) { + baked.push(element); + } + } + return baked; + } + function parsePlaceable() { + consumeToken(TOKEN_BRACE_OPEN, SyntaxError); + let selector = parseInlineExpression(); + if (consumeToken(TOKEN_BRACE_CLOSE)) { + return selector; + } + if (consumeToken(TOKEN_ARROW)) { + let variants = parseVariants(); + consumeToken(TOKEN_BRACE_CLOSE, SyntaxError); + return { + type: "select", + selector, + ...variants, + }; + } + throw new SyntaxError("Unclosed placeable"); + } + function parseInlineExpression() { + if (source[cursor] === "{") { + return parsePlaceable(); + } + if (test(RE_REFERENCE)) { + let [, sigil, name, attr = null] = match(RE_REFERENCE); + if (sigil === "$") { + return { + type: "var", + name, + }; + } + if (consumeToken(TOKEN_PAREN_OPEN)) { + let args = parseArguments(); + if (sigil === "-") { + return { + type: "term", + name, + attr, + args, + }; + } + if (RE_FUNCTION_NAME.test(name)) { + return { + type: "func", + name, + args, + }; + } + throw new SyntaxError("Function names must be all upper-case"); + } + if (sigil === "-") { + return { + type: "term", + name, + attr, + args: [], + }; + } + return { + type: "mesg", + name, + attr, + }; + } + return parseLiteral(); + } + function parseArguments() { + let args = []; + while (true) { + switch (source[cursor]) { + case ")": + cursor++; + return args; + case undefined: + throw new SyntaxError("Unclosed argument list"); + } + args.push(parseArgument()); + consumeToken(TOKEN_COMMA); + } + } + function parseArgument() { + let expr = parseInlineExpression(); + if (expr.type !== "mesg") { + return expr; + } + if (consumeToken(TOKEN_COLON)) { + return { + type: "narg", + name: expr.name, + value: parseLiteral(), + }; + } + return expr; + } + function parseVariants() { + let variants = []; + let count = 0; + let star; + while (test(RE_VARIANT_START)) { + if (consumeChar("*")) { + star = count; + } + let key = parseVariantKey(); + let value = parsePattern(); + if (value === null) { + throw new SyntaxError("Expected variant value"); + } + variants[count++] = { + key, + value, + }; + } + if (count === 0) { + return null; + } + if (star === undefined) { + throw new SyntaxError("Expected default variant"); + } + return { + variants, + star, + }; + } + function parseVariantKey() { + consumeToken(TOKEN_BRACKET_OPEN, SyntaxError); + let key; + if (test(RE_NUMBER_LITERAL)) { + key = parseNumberLiteral(); + } else { + key = { + type: "str", + value: match1(RE_IDENTIFIER), + }; + } + consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError); + return key; + } + function parseLiteral() { + if (test(RE_NUMBER_LITERAL)) { + return parseNumberLiteral(); + } + if (source[cursor] === '"') { + return parseStringLiteral(); + } + throw new SyntaxError("Invalid expression"); + } + function parseNumberLiteral() { + let [, value, fraction = ""] = match(RE_NUMBER_LITERAL); + let precision = fraction.length; + return { + type: "num", + value: parseFloat(value), + precision, + }; + } + function parseStringLiteral() { + consumeChar('"', SyntaxError); + let value = ""; + while (true) { + value += match1(RE_STRING_RUN); + if (source[cursor] === "\\") { + value += parseEscapeSequence(); + continue; + } + if (consumeChar('"')) { + return { + type: "str", + value, + }; + } + throw new SyntaxError("Unclosed string literal"); + } + } + function parseEscapeSequence() { + if (test(RE_STRING_ESCAPE)) { + return match1(RE_STRING_ESCAPE); + } + if (test(RE_UNICODE_ESCAPE)) { + let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE); + let codepoint = parseInt(codepoint4 || codepoint6, 16); + return codepoint <= 0xd7ff || 0xe000 <= codepoint + ? String.fromCodePoint(codepoint) + : "�"; + } + throw new SyntaxError("Unknown escape sequence"); + } + function parseIndent() { + let start = cursor; + consumeToken(TOKEN_BLANK); + switch (source[cursor]) { + case ".": + case "[": + case "*": + case "}": + case undefined: + return false; + case "{": + return makeIndent(source.slice(start, cursor)); + } + if (source[cursor - 1] === " ") { + return makeIndent(source.slice(start, cursor)); + } + return false; + } + function trim(text, re) { + return text.replace(re, ""); + } + function makeIndent(blank) { + let value = blank.replace(RE_BLANK_LINES, "\n"); + let length = RE_INDENT.exec(blank)[1].length; + return new Indent(value, length); + } + } +} +class Indent { + constructor(value, length) { + this.value = value; + this.length = length; + } +} // ./node_modules/@fluent/bundle/esm/index.js +// ./node_modules/@fluent/dom/esm/overlay.js +const reOverlay = /<|&#?\w+;/; +const TEXT_LEVEL_ELEMENTS = { + "http://www.w3.org/1999/xhtml": [ + "em", + "strong", + "small", + "s", + "cite", + "q", + "dfn", + "abbr", + "data", + "time", + "code", + "var", + "samp", + "kbd", + "sub", + "sup", + "i", + "b", + "u", + "mark", + "bdi", + "bdo", + "span", + "br", + "wbr", + ], +}; +const LOCALIZABLE_ATTRIBUTES = { + "http://www.w3.org/1999/xhtml": { + global: ["title", "aria-label", "aria-valuetext"], + a: ["download"], + area: ["download", "alt"], + input: ["alt", "placeholder"], + menuitem: ["label"], + menu: ["label"], + optgroup: ["label"], + option: ["label"], + track: ["label"], + img: ["alt"], + textarea: ["placeholder"], + th: ["abbr"], + }, + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": { + global: [ + "accesskey", + "aria-label", + "aria-valuetext", + "label", + "title", + "tooltiptext", + ], + description: ["value"], + key: ["key", "keycode"], + label: ["value"], + textbox: ["placeholder", "value"], + }, +}; +function translateElement(element, translation) { + const { value } = translation; + if (typeof value === "string") { + if ( + element.localName === "title" && + element.namespaceURI === "http://www.w3.org/1999/xhtml" + ) { + element.textContent = value; + } else if (!reOverlay.test(value)) { + element.textContent = value; + } else { + const templateElement = element.ownerDocument.createElementNS( + "http://www.w3.org/1999/xhtml", + "template", + ); + templateElement.innerHTML = value; + overlayChildNodes(templateElement.content, element); + } + } + overlayAttributes(translation, element); +} +function overlayChildNodes(fromFragment, toElement) { + for (const childNode of fromFragment.childNodes) { + if (childNode.nodeType === childNode.TEXT_NODE) { + continue; + } + if (childNode.hasAttribute("data-l10n-name")) { + const sanitized = getNodeForNamedElement(toElement, childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + if (isElementAllowed(childNode)) { + const sanitized = createSanitizedElement(childNode); + fromFragment.replaceChild(sanitized, childNode); + continue; + } + console.warn( + `An element of forbidden type "${childNode.localName}" was found in ` + + "the translation. Only safe text-level elements and elements with " + + "data-l10n-name are allowed.", + ); + fromFragment.replaceChild( + createTextNodeFromTextContent(childNode), + childNode, + ); + } + toElement.textContent = ""; + toElement.appendChild(fromFragment); +} +function hasAttribute(attributes, name) { + if (!attributes) { + return false; + } + for (let attr of attributes) { + if (attr.name === name) { + return true; + } + } + return false; +} +function overlayAttributes(fromElement, toElement) { + const explicitlyAllowed = toElement.hasAttribute("data-l10n-attrs") + ? toElement + .getAttribute("data-l10n-attrs") + .split(",") + .map((i) => i.trim()) + : null; + for (const attr of Array.from(toElement.attributes)) { + if ( + isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && + !hasAttribute(fromElement.attributes, attr.name) + ) { + toElement.removeAttribute(attr.name); + } + } + if (!fromElement.attributes) { + return; + } + for (const attr of Array.from(fromElement.attributes)) { + if ( + isAttrNameLocalizable(attr.name, toElement, explicitlyAllowed) && + toElement.getAttribute(attr.name) !== attr.value + ) { + toElement.setAttribute(attr.name, attr.value); + } + } +} +function getNodeForNamedElement(sourceElement, translatedChild) { + const childName = translatedChild.getAttribute("data-l10n-name"); + const sourceChild = sourceElement.querySelector( + `[data-l10n-name="${childName}"]`, + ); + if (!sourceChild) { + console.warn(`An element named "${childName}" wasn't found in the source.`); + return createTextNodeFromTextContent(translatedChild); + } + if (sourceChild.localName !== translatedChild.localName) { + console.warn( + `An element named "${childName}" was found in the translation ` + + `but its type ${translatedChild.localName} didn't match the ` + + `element found in the source (${sourceChild.localName}).`, + ); + return createTextNodeFromTextContent(translatedChild); + } + sourceElement.removeChild(sourceChild); + const clone = sourceChild.cloneNode(false); + return shallowPopulateUsing(translatedChild, clone); +} +function createSanitizedElement(element) { + const clone = element.ownerDocument.createElement(element.localName); + return shallowPopulateUsing(element, clone); +} +function createTextNodeFromTextContent(element) { + return element.ownerDocument.createTextNode(element.textContent); +} +function isElementAllowed(element) { + const allowed = TEXT_LEVEL_ELEMENTS[element.namespaceURI]; + return allowed && allowed.includes(element.localName); +} +function isAttrNameLocalizable(name, element, explicitlyAllowed = null) { + if (explicitlyAllowed && explicitlyAllowed.includes(name)) { + return true; + } + const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI]; + if (!allowed) { + return false; + } + const attrName = name.toLowerCase(); + const elemName = element.localName; + if (allowed.global.includes(attrName)) { + return true; + } + if (!allowed[elemName]) { + return false; + } + if (allowed[elemName].includes(attrName)) { + return true; + } + if ( + element.namespaceURI === "http://www.w3.org/1999/xhtml" && + elemName === "input" && + attrName === "value" + ) { + const type = element.type.toLowerCase(); + if (type === "submit" || type === "button" || type === "reset") { + return true; + } + } + return false; +} +function shallowPopulateUsing(fromElement, toElement) { + toElement.textContent = fromElement.textContent; + overlayAttributes(fromElement, toElement); + return toElement; +} // ./node_modules/cached-iterable/src/cached_iterable.mjs +class CachedIterable extends Array { + static from(iterable) { + if (iterable instanceof this) { + return iterable; + } + return new this(iterable); + } +} // ./node_modules/cached-iterable/src/cached_sync_iterable.mjs +class CachedSyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.iterator]() { + const cached = this; + let cur = 0; + return { + next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + }, + }; + } + touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && last.done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} // ./node_modules/cached-iterable/src/cached_async_iterable.mjs +class CachedAsyncIterable extends CachedIterable { + constructor(iterable) { + super(); + if (Symbol.asyncIterator in Object(iterable)) { + this.iterator = iterable[Symbol.asyncIterator](); + } else if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + } + [Symbol.asyncIterator]() { + const cached = this; + let cur = 0; + return { + async next() { + if (cached.length <= cur) { + cached.push(cached.iterator.next()); + } + return cached[cur++]; + }, + }; + } + async touchNext(count = 1) { + let idx = 0; + while (idx++ < count) { + const last = this[this.length - 1]; + if (last && (await last).done) { + break; + } + this.push(this.iterator.next()); + } + return this[this.length - 1]; + } +} // ./node_modules/cached-iterable/src/index.mjs +// ./node_modules/@fluent/dom/esm/localization.js +class Localization { + constructor(resourceIds = [], generateBundles) { + this.resourceIds = resourceIds; + this.generateBundles = generateBundles; + this.onChange(true); + } + addResourceIds(resourceIds, eager = false) { + this.resourceIds.push(...resourceIds); + this.onChange(eager); + return this.resourceIds.length; + } + removeResourceIds(resourceIds) { + this.resourceIds = this.resourceIds.filter((r) => !resourceIds.includes(r)); + this.onChange(); + return this.resourceIds.length; + } + async formatWithFallback(keys, method) { + const translations = []; + let hasAtLeastOneBundle = false; + for await (const bundle of this.bundles) { + hasAtLeastOneBundle = true; + const missingIds = keysFromBundle(method, bundle, keys, translations); + if (missingIds.size === 0) { + break; + } + if (typeof console !== "undefined") { + const locale = bundle.locales[0]; + const ids = Array.from(missingIds).join(", "); + console.warn(`[fluent] Missing translations in ${locale}: ${ids}`); + } + } + if (!hasAtLeastOneBundle && typeof console !== "undefined") { + console.warn(`[fluent] Request for keys failed because no resource bundles got generated. + keys: ${JSON.stringify(keys)}. + resourceIds: ${JSON.stringify(this.resourceIds)}.`); + } + return translations; + } + formatMessages(keys) { + return this.formatWithFallback(keys, messageFromBundle); + } + formatValues(keys) { + return this.formatWithFallback(keys, valueFromBundle); + } + async formatValue(id, args) { + const [val] = await this.formatValues([ + { + id, + args, + }, + ]); + return val; + } + handleEvent() { + this.onChange(); + } + onChange(eager = false) { + this.bundles = CachedAsyncIterable.from( + this.generateBundles(this.resourceIds), + ); + if (eager) { + this.bundles.touchNext(2); + } + } +} +function valueFromBundle(bundle, errors, message, args) { + if (message.value) { + return bundle.formatPattern(message.value, args, errors); + } + return null; +} +function messageFromBundle(bundle, errors, message, args) { + const formatted = { + value: null, + attributes: null, + }; + if (message.value) { + formatted.value = bundle.formatPattern(message.value, args, errors); + } + let attrNames = Object.keys(message.attributes); + if (attrNames.length > 0) { + formatted.attributes = new Array(attrNames.length); + for (let [i, name] of attrNames.entries()) { + let value = bundle.formatPattern(message.attributes[name], args, errors); + formatted.attributes[i] = { + name, + value, + }; + } + } + return formatted; +} +function keysFromBundle(method, bundle, keys, translations) { + const messageErrors = []; + const missingIds = new Set(); + keys.forEach(({ id, args }, i) => { + if (translations[i] !== undefined) { + return; + } + let message = bundle.getMessage(id); + if (message) { + messageErrors.length = 0; + translations[i] = method(bundle, messageErrors, message, args); + if (messageErrors.length > 0 && typeof console !== "undefined") { + const locale = bundle.locales[0]; + const errors = messageErrors.join(", "); + console.warn( + `[fluent][resolver] errors in ${locale}/${id}: ${errors}.`, + ); + } + } else { + missingIds.add(id); + } + }); + return missingIds; +} // ./node_modules/@fluent/dom/esm/dom_localization.js +const L10NID_ATTR_NAME = "data-l10n-id"; +const L10NARGS_ATTR_NAME = "data-l10n-args"; +const L10N_ELEMENT_QUERY = `[${L10NID_ATTR_NAME}]`; +class DOMLocalization extends Localization { + constructor(resourceIds, generateBundles) { + super(resourceIds, generateBundles); + this.roots = new Set(); + this.pendingrAF = null; + this.pendingElements = new Set(); + this.windowElement = null; + this.mutationObserver = null; + this.observerConfig = { + attributes: true, + characterData: false, + childList: true, + subtree: true, + attributeFilter: [L10NID_ATTR_NAME, L10NARGS_ATTR_NAME], + }; + } + onChange(eager = false) { + super.onChange(eager); + if (this.roots) { + this.translateRoots(); + } + } + setAttributes(element, id, args) { + element.setAttribute(L10NID_ATTR_NAME, id); + if (args) { + element.setAttribute(L10NARGS_ATTR_NAME, JSON.stringify(args)); + } else { + element.removeAttribute(L10NARGS_ATTR_NAME); + } + return element; + } + getAttributes(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null), + }; + } + connectRoot(newRoot) { + for (const root of this.roots) { + if ( + root === newRoot || + root.contains(newRoot) || + newRoot.contains(root) + ) { + throw new Error("Cannot add a root that overlaps with existing root."); + } + } + if (this.windowElement) { + if (this.windowElement !== newRoot.ownerDocument.defaultView) { + throw new Error(`Cannot connect a root: + DOMLocalization already has a root from a different window.`); + } + } else { + this.windowElement = newRoot.ownerDocument.defaultView; + this.mutationObserver = new this.windowElement.MutationObserver( + (mutations) => this.translateMutations(mutations), + ); + } + this.roots.add(newRoot); + this.mutationObserver.observe(newRoot, this.observerConfig); + } + disconnectRoot(root) { + this.roots.delete(root); + this.pauseObserving(); + if (this.roots.size === 0) { + this.mutationObserver = null; + if (this.windowElement && this.pendingrAF) { + this.windowElement.cancelAnimationFrame(this.pendingrAF); + } + this.windowElement = null; + this.pendingrAF = null; + this.pendingElements.clear(); + return true; + } + this.resumeObserving(); + return false; + } + translateRoots() { + const roots = Array.from(this.roots); + return Promise.all(roots.map((root) => this.translateFragment(root))); + } + pauseObserving() { + if (!this.mutationObserver) { + return; + } + this.translateMutations(this.mutationObserver.takeRecords()); + this.mutationObserver.disconnect(); + } + resumeObserving() { + if (!this.mutationObserver) { + return; + } + for (const root of this.roots) { + this.mutationObserver.observe(root, this.observerConfig); + } + } + translateMutations(mutations) { + for (const mutation of mutations) { + switch (mutation.type) { + case "attributes": + if (mutation.target.hasAttribute("data-l10n-id")) { + this.pendingElements.add(mutation.target); + } + break; + case "childList": + for (const addedNode of mutation.addedNodes) { + if (addedNode.nodeType === addedNode.ELEMENT_NODE) { + if (addedNode.childElementCount) { + for (const element of this.getTranslatables(addedNode)) { + this.pendingElements.add(element); + } + } else if (addedNode.hasAttribute(L10NID_ATTR_NAME)) { + this.pendingElements.add(addedNode); + } + } + } + break; + } + } + if (this.pendingElements.size > 0) { + if (this.pendingrAF === null) { + this.pendingrAF = this.windowElement.requestAnimationFrame(() => { + this.translateElements(Array.from(this.pendingElements)); + this.pendingElements.clear(); + this.pendingrAF = null; + }); + } + } + } + translateFragment(frag) { + return this.translateElements(this.getTranslatables(frag)); + } + async translateElements(elements) { + if (!elements.length) { + return undefined; + } + const keys = elements.map(this.getKeysForElement); + const translations = await this.formatMessages(keys); + return this.applyTranslations(elements, translations); + } + applyTranslations(elements, translations) { + this.pauseObserving(); + for (let i = 0; i < elements.length; i++) { + if (translations[i] !== undefined) { + translateElement(elements[i], translations[i]); + } + } + this.resumeObserving(); + } + getTranslatables(element) { + const nodes = Array.from(element.querySelectorAll(L10N_ELEMENT_QUERY)); + if ( + typeof element.hasAttribute === "function" && + element.hasAttribute(L10NID_ATTR_NAME) + ) { + nodes.push(element); + } + return nodes; + } + getKeysForElement(element) { + return { + id: element.getAttribute(L10NID_ATTR_NAME), + args: JSON.parse(element.getAttribute(L10NARGS_ATTR_NAME) || null), + }; + } +} // ./node_modules/@fluent/dom/esm/index.js +// ./web/l10n.js +class L10n { + #dir; + #elements; + #lang; + #l10n; + constructor({ lang, isRTL }, l10n = null) { + this.#lang = L10n.#fixupLangCode(lang); + this.#l10n = l10n; + this.#dir = isRTL ?? L10n.#isRTL(this.#lang) ? "rtl" : "ltr"; + } + _setL10n(l10n) { + this.#l10n = l10n; + } + getLanguage() { + return this.#lang; + } + getDirection() { + return this.#dir; + } + async get(ids, args = null, fallback) { + if (Array.isArray(ids)) { + ids = ids.map((id) => ({ + id, + })); + const messages = await this.#l10n.formatMessages(ids); + return messages.map((message) => message.value); + } + const messages = await this.#l10n.formatMessages([ + { + id: ids, + args, + }, + ]); + return messages[0]?.value || fallback; + } + async translate(element) { + (this.#elements ||= new Set()).add(element); + try { + this.#l10n.connectRoot(element); + await this.#l10n.translateRoots(); + } catch {} + } + async translateOnce(element) { + try { + await this.#l10n.translateElements([element]); + } catch (ex) { + console.error("translateOnce:", ex); + } + } + async destroy() { + if (this.#elements) { + for (const element of this.#elements) { + this.#l10n.disconnectRoot(element); + } + this.#elements.clear(); + this.#elements = null; + } + this.#l10n.pauseObserving(); + } + pause() { + this.#l10n.pauseObserving(); + } + resume() { + this.#l10n.resumeObserving(); + } + static #fixupLangCode(langCode) { + langCode = langCode?.toLowerCase() || "en-us"; + const PARTIAL_LANG_CODES = { + en: "en-us", + es: "es-es", + fy: "fy-nl", + ga: "ga-ie", + gu: "gu-in", + hi: "hi-in", + hy: "hy-am", + nb: "nb-no", + ne: "ne-np", + nn: "nn-no", + pa: "pa-in", + pt: "pt-pt", + sv: "sv-se", + zh: "zh-cn", + }; + return PARTIAL_LANG_CODES[langCode] || langCode; + } + static #isRTL(lang) { + const shortCode = lang.split("-", 1)[0]; + return ["ar", "he", "fa", "ps", "ur"].includes(shortCode); + } +} +const GenericL10n = null; // ./web/genericl10n.js + +function createBundle(lang, text) { + const resource = new FluentResource(text); + const bundle = new FluentBundle(lang); + const errors = bundle.addResource(resource); + if (errors.length) { + console.error("L10n errors", errors); + } + return bundle; +} +class genericl10n_GenericL10n extends L10n { + constructor(lang) { + super({ + lang, + }); + const generateBundles = !lang + ? genericl10n_GenericL10n.#generateBundlesFallback.bind( + genericl10n_GenericL10n, + this.getLanguage(), + ) + : genericl10n_GenericL10n.#generateBundles.bind( + genericl10n_GenericL10n, + "en-us", + this.getLanguage(), + ); + this._setL10n(new DOMLocalization([], generateBundles)); + } + static async *#generateBundles(defaultLang, baseLang) { + const { baseURL, paths } = await this.#getPaths(); + const langs = [baseLang]; + if (defaultLang !== baseLang) { + const shortLang = baseLang.split("-", 1)[0]; + if (shortLang !== baseLang) { + langs.push(shortLang); + } + langs.push(defaultLang); + } + for (const lang of langs) { + const bundle = await this.#createBundle(lang, baseURL, paths); + if (bundle) { + yield bundle; + } else if (lang === "en-us") { + yield this.#createBundleFallback(lang); + } + } + } + static async #createBundle(lang, baseURL, paths) { + const path = paths[lang]; + if (!path) { + return null; + } + const url = new URL(path, baseURL); + const text = await fetchData(url, "text"); + return createBundle(lang, text); + } + static async #getPaths() { + try { + const { href } = document.querySelector(`link[type="application/l10n"]`); + const paths = await fetchData(href, "json"); + return { + baseURL: href.replace(/[^/]*$/, "") || "./", + paths, + }; + } catch {} + return { + baseURL: "./", + paths: Object.create(null), + }; + } + static async *#generateBundlesFallback(lang) { + yield this.#createBundleFallback(lang); + } + static async #createBundleFallback(lang) { + const text = + 'pdfjs-previous-button =\n .title = Previous Page\npdfjs-previous-button-label = Previous\npdfjs-next-button =\n .title = Next Page\npdfjs-next-button-label = Next\npdfjs-page-input =\n .title = Page\npdfjs-of-pages = of { $pagesCount }\npdfjs-page-of-pages = ({ $pageNumber } of { $pagesCount })\npdfjs-zoom-out-button =\n .title = Zoom Out\npdfjs-zoom-out-button-label = Zoom Out\npdfjs-zoom-in-button =\n .title = Zoom In\npdfjs-zoom-in-button-label = Zoom In\npdfjs-zoom-select =\n .title = Zoom\npdfjs-presentation-mode-button =\n .title = Switch to Presentation Mode\npdfjs-presentation-mode-button-label = Presentation Mode\npdfjs-open-file-button =\n .title = Open File\npdfjs-open-file-button-label = Open\npdfjs-print-button =\n .title = Print\npdfjs-print-button-label = Print\npdfjs-save-button =\n .title = Save\npdfjs-save-button-label = Save\npdfjs-download-button =\n .title = Download\npdfjs-download-button-label = Download\npdfjs-bookmark-button =\n .title = Current Page (View URL from Current Page)\npdfjs-bookmark-button-label = Current Page\npdfjs-tools-button =\n .title = Tools\npdfjs-tools-button-label = Tools\npdfjs-first-page-button =\n .title = Go to First Page\npdfjs-first-page-button-label = Go to First Page\npdfjs-last-page-button =\n .title = Go to Last Page\npdfjs-last-page-button-label = Go to Last Page\npdfjs-page-rotate-cw-button =\n .title = Rotate Clockwise\npdfjs-page-rotate-cw-button-label = Rotate Clockwise\npdfjs-page-rotate-ccw-button =\n .title = Rotate Counterclockwise\npdfjs-page-rotate-ccw-button-label = Rotate Counterclockwise\npdfjs-cursor-text-select-tool-button =\n .title = Enable Text Selection Tool\npdfjs-cursor-text-select-tool-button-label = Text Selection Tool\npdfjs-cursor-hand-tool-button =\n .title = Enable Hand Tool\npdfjs-cursor-hand-tool-button-label = Hand Tool\npdfjs-scroll-page-button =\n .title = Use Page Scrolling\npdfjs-scroll-page-button-label = Page Scrolling\npdfjs-scroll-vertical-button =\n .title = Use Vertical Scrolling\npdfjs-scroll-vertical-button-label = Vertical Scrolling\npdfjs-scroll-horizontal-button =\n .title = Use Horizontal Scrolling\npdfjs-scroll-horizontal-button-label = Horizontal Scrolling\npdfjs-scroll-wrapped-button =\n .title = Use Wrapped Scrolling\npdfjs-scroll-wrapped-button-label = Wrapped Scrolling\npdfjs-spread-none-button =\n .title = Do not join page spreads\npdfjs-spread-none-button-label = No Spreads\npdfjs-spread-odd-button =\n .title = Join page spreads starting with odd-numbered pages\npdfjs-spread-odd-button-label = Odd Spreads\npdfjs-spread-even-button =\n .title = Join page spreads starting with even-numbered pages\npdfjs-spread-even-button-label = Even Spreads\npdfjs-document-properties-button =\n .title = Document Properties\u2026\npdfjs-document-properties-button-label = Document Properties\u2026\npdfjs-document-properties-file-name = File name:\npdfjs-document-properties-file-size = File size:\npdfjs-document-properties-size-kb = { NUMBER($kb, maximumSignificantDigits: 3) } KB ({ $b } bytes)\npdfjs-document-properties-size-mb = { NUMBER($mb, maximumSignificantDigits: 3) } MB ({ $b } bytes)\npdfjs-document-properties-title = Title:\npdfjs-document-properties-author = Author:\npdfjs-document-properties-subject = Subject:\npdfjs-document-properties-keywords = Keywords:\npdfjs-document-properties-creation-date = Creation Date:\npdfjs-document-properties-modification-date = Modification Date:\npdfjs-document-properties-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }\npdfjs-document-properties-creator = Creator:\npdfjs-document-properties-producer = PDF Producer:\npdfjs-document-properties-version = PDF Version:\npdfjs-document-properties-page-count = Page Count:\npdfjs-document-properties-page-size = Page Size:\npdfjs-document-properties-page-size-unit-inches = in\npdfjs-document-properties-page-size-unit-millimeters = mm\npdfjs-document-properties-page-size-orientation-portrait = portrait\npdfjs-document-properties-page-size-orientation-landscape = landscape\npdfjs-document-properties-page-size-name-a-three = A3\npdfjs-document-properties-page-size-name-a-four = A4\npdfjs-document-properties-page-size-name-letter = Letter\npdfjs-document-properties-page-size-name-legal = Legal\npdfjs-document-properties-page-size-dimension-string = { $width } \xD7 { $height } { $unit } ({ $orientation })\npdfjs-document-properties-page-size-dimension-name-string = { $width } \xD7 { $height } { $unit } ({ $name }, { $orientation })\npdfjs-document-properties-linearized = Fast Web View:\npdfjs-document-properties-linearized-yes = Yes\npdfjs-document-properties-linearized-no = No\npdfjs-document-properties-close-button = Close\npdfjs-print-progress-message = Preparing document for printing\u2026\npdfjs-print-progress-percent = { $progress }%\npdfjs-print-progress-close-button = Cancel\npdfjs-printing-not-supported = Warning: Printing is not fully supported by this browser.\npdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing.\npdfjs-toggle-sidebar-button =\n .title = Toggle Sidebar\npdfjs-toggle-sidebar-notification-button =\n .title = Toggle Sidebar (document contains outline/attachments/layers)\npdfjs-toggle-sidebar-button-label = Toggle Sidebar\npdfjs-document-outline-button =\n .title = Show Document Outline (double-click to expand/collapse all items)\npdfjs-document-outline-button-label = Document Outline\npdfjs-attachments-button =\n .title = Show Attachments\npdfjs-attachments-button-label = Attachments\npdfjs-layers-button =\n .title = Show Layers (double-click to reset all layers to the default state)\npdfjs-layers-button-label = Layers\npdfjs-thumbs-button =\n .title = Show Thumbnails\npdfjs-thumbs-button-label = Thumbnails\npdfjs-current-outline-item-button =\n .title = Find Current Outline Item\npdfjs-current-outline-item-button-label = Current Outline Item\npdfjs-findbar-button =\n .title = Find in Document\npdfjs-findbar-button-label = Find\npdfjs-additional-layers = Additional Layers\npdfjs-thumb-page-title =\n .title = Page { $page }\npdfjs-thumb-page-canvas =\n .aria-label = Thumbnail of Page { $page }\npdfjs-find-input =\n .title = Find\n .placeholder = Find in document\u2026\npdfjs-find-previous-button =\n .title = Find the previous occurrence of the phrase\npdfjs-find-previous-button-label = Previous\npdfjs-find-next-button =\n .title = Find the next occurrence of the phrase\npdfjs-find-next-button-label = Next\npdfjs-find-highlight-checkbox = Highlight All\npdfjs-find-match-case-checkbox-label = Match Case\npdfjs-find-match-diacritics-checkbox-label = Match Diacritics\npdfjs-find-entire-word-checkbox-label = Whole Words\npdfjs-find-reached-top = Reached top of document, continued from bottom\npdfjs-find-reached-bottom = Reached end of document, continued from top\npdfjs-find-match-count =\n { $total ->\n [one] { $current } of { $total } match\n *[other] { $current } of { $total } matches\n }\npdfjs-find-match-count-limit =\n { $limit ->\n [one] More than { $limit } match\n *[other] More than { $limit } matches\n }\npdfjs-find-not-found = Phrase not found\npdfjs-page-scale-width = Page Width\npdfjs-page-scale-fit = Page Fit\npdfjs-page-scale-auto = Automatic Zoom\npdfjs-page-scale-actual = Actual Size\npdfjs-page-scale-percent = { $scale }%\npdfjs-page-landmark =\n .aria-label = Page { $page }\npdfjs-loading-error = An error occurred while loading the PDF.\npdfjs-invalid-file-error = Invalid or corrupted PDF file.\npdfjs-missing-file-error = Missing PDF file.\npdfjs-unexpected-response-error = Unexpected server response.\npdfjs-rendering-error = An error occurred while rendering the page.\npdfjs-annotation-date-time-string = { DATETIME($dateObj, dateStyle: "short", timeStyle: "medium") }\npdfjs-text-annotation-type =\n .alt = [{ $type } Annotation]\npdfjs-password-label = Enter the password to open this PDF file.\npdfjs-password-invalid = Invalid password. Please try again.\npdfjs-password-ok-button = OK\npdfjs-password-cancel-button = Cancel\npdfjs-web-fonts-disabled = Web fonts are disabled: unable to use embedded PDF fonts.\npdfjs-editor-free-text-button =\n .title = Text\npdfjs-editor-free-text-button-label = Text\npdfjs-editor-ink-button =\n .title = Draw\npdfjs-editor-ink-button-label = Draw\npdfjs-editor-stamp-button =\n .title = Add or edit images\npdfjs-editor-stamp-button-label = Add or edit images\npdfjs-editor-highlight-button =\n .title = Highlight\npdfjs-editor-highlight-button-label = Highlight\npdfjs-highlight-floating-button1 =\n .title = Highlight\n .aria-label = Highlight\npdfjs-highlight-floating-button-label = Highlight\npdfjs-editor-remove-ink-button =\n .title = Remove drawing\npdfjs-editor-remove-freetext-button =\n .title = Remove text\npdfjs-editor-remove-stamp-button =\n .title = Remove image\npdfjs-editor-remove-highlight-button =\n .title = Remove highlight\npdfjs-editor-free-text-color-input = Color\npdfjs-editor-free-text-size-input = Size\npdfjs-editor-ink-color-input = Color\npdfjs-editor-ink-thickness-input = Thickness\npdfjs-editor-ink-opacity-input = Opacity\npdfjs-editor-stamp-add-image-button =\n .title = Add image\npdfjs-editor-stamp-add-image-button-label = Add image\npdfjs-editor-free-highlight-thickness-input = Thickness\npdfjs-editor-free-highlight-thickness-title =\n .title = Change thickness when highlighting items other than text\npdfjs-free-text2 =\n .aria-label = Text Editor\n .default-content = Start typing\u2026\npdfjs-ink =\n .aria-label = Draw Editor\npdfjs-ink-canvas =\n .aria-label = User-created image\npdfjs-editor-alt-text-button =\n .aria-label = Alt text\npdfjs-editor-alt-text-button-label = Alt text\npdfjs-editor-alt-text-edit-button =\n .aria-label = Edit alt text\npdfjs-editor-alt-text-dialog-label = Choose an option\npdfjs-editor-alt-text-dialog-description = Alt text (alternative text) helps when people can\u2019t see the image or when it doesn\u2019t load.\npdfjs-editor-alt-text-add-description-label = Add a description\npdfjs-editor-alt-text-add-description-description = Aim for 1-2 sentences that describe the subject, setting, or actions.\npdfjs-editor-alt-text-mark-decorative-label = Mark as decorative\npdfjs-editor-alt-text-mark-decorative-description = This is used for ornamental images, like borders or watermarks.\npdfjs-editor-alt-text-cancel-button = Cancel\npdfjs-editor-alt-text-save-button = Save\npdfjs-editor-alt-text-decorative-tooltip = Marked as decorative\npdfjs-editor-alt-text-textarea =\n .placeholder = For example, \u201CA young man sits down at a table to eat a meal\u201D\npdfjs-editor-resizer-top-left =\n .aria-label = Top left corner \u2014 resize\npdfjs-editor-resizer-top-middle =\n .aria-label = Top middle \u2014 resize\npdfjs-editor-resizer-top-right =\n .aria-label = Top right corner \u2014 resize\npdfjs-editor-resizer-middle-right =\n .aria-label = Middle right \u2014 resize\npdfjs-editor-resizer-bottom-right =\n .aria-label = Bottom right corner \u2014 resize\npdfjs-editor-resizer-bottom-middle =\n .aria-label = Bottom middle \u2014 resize\npdfjs-editor-resizer-bottom-left =\n .aria-label = Bottom left corner \u2014 resize\npdfjs-editor-resizer-middle-left =\n .aria-label = Middle left \u2014 resize\npdfjs-editor-highlight-colorpicker-label = Highlight color\npdfjs-editor-colorpicker-button =\n .title = Change color\npdfjs-editor-colorpicker-dropdown =\n .aria-label = Color choices\npdfjs-editor-colorpicker-yellow =\n .title = Yellow\npdfjs-editor-colorpicker-green =\n .title = Green\npdfjs-editor-colorpicker-blue =\n .title = Blue\npdfjs-editor-colorpicker-pink =\n .title = Pink\npdfjs-editor-colorpicker-red =\n .title = Red\npdfjs-editor-highlight-show-all-button-label = Show all\npdfjs-editor-highlight-show-all-button =\n .title = Show all\npdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description)\npdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description)\npdfjs-editor-new-alt-text-textarea =\n .placeholder = Write your description here\u2026\npdfjs-editor-new-alt-text-description = Short description for people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-new-alt-text-disclaimer1 = This alt text was created automatically and may be inaccurate.\npdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more\npdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically\npdfjs-editor-new-alt-text-not-now-button = Not now\npdfjs-editor-new-alt-text-error-title = Couldn\u2019t create alt text automatically\npdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later.\npdfjs-editor-new-alt-text-error-close-button = Close\npdfjs-editor-new-alt-text-ai-model-downloading-progress = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\n .aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)\npdfjs-editor-new-alt-text-added-button =\n .aria-label = Alt text added\npdfjs-editor-new-alt-text-added-button-label = Alt text added\npdfjs-editor-new-alt-text-missing-button =\n .aria-label = Missing alt text\npdfjs-editor-new-alt-text-missing-button-label = Missing alt text\npdfjs-editor-new-alt-text-to-review-button =\n .aria-label = Review alt text\npdfjs-editor-new-alt-text-to-review-button-label = Review alt text\npdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText }\npdfjs-image-alt-text-settings-button =\n .title = Image alt text settings\npdfjs-image-alt-text-settings-button-label = Image alt text settings\npdfjs-editor-alt-text-settings-dialog-label = Image alt text settings\npdfjs-editor-alt-text-settings-automatic-title = Automatic alt text\npdfjs-editor-alt-text-settings-create-model-button-label = Create alt text automatically\npdfjs-editor-alt-text-settings-create-model-description = Suggests descriptions to help people who can\u2019t see the image or when the image doesn\u2019t load.\npdfjs-editor-alt-text-settings-download-model-label = Alt text AI model ({ $totalSize } MB)\npdfjs-editor-alt-text-settings-ai-model-description = Runs locally on your device so your data stays private. Required for automatic alt text.\npdfjs-editor-alt-text-settings-delete-model-button = Delete\npdfjs-editor-alt-text-settings-download-model-button = Download\npdfjs-editor-alt-text-settings-downloading-model-button = Downloading\u2026\npdfjs-editor-alt-text-settings-editor-title = Alt text editor\npdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image\npdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text.\npdfjs-editor-alt-text-settings-close-button = Close\npdfjs-editor-undo-bar-message-highlight = Highlight removed\npdfjs-editor-undo-bar-message-freetext = Text removed\npdfjs-editor-undo-bar-message-ink = Drawing removed\npdfjs-editor-undo-bar-message-stamp = Image removed\npdfjs-editor-undo-bar-message-multiple =\n { $count ->\n [one] { $count } annotation removed\n *[other] { $count } annotations removed\n }\npdfjs-editor-undo-bar-undo-button =\n .title = Undo\npdfjs-editor-undo-bar-undo-button-label = Undo\npdfjs-editor-undo-bar-close-button =\n .title = Close\npdfjs-editor-undo-bar-close-button-label = Close'; + return createBundle(lang, text); + } +} // ./web/generic_scripting.js + +async function docProperties(pdfDocument) { + const url = "", + baseUrl = url.split("#", 1)[0]; + let { info, metadata, contentDispositionFilename, contentLength } = + await pdfDocument.getMetadata(); + if (!contentLength) { + const { length } = await pdfDocument.getDownloadInfo(); + contentLength = length; + } + return { + ...info, + baseURL: baseUrl, + filesize: contentLength, + filename: contentDispositionFilename || getPdfFilenameFromUrl(url), + metadata: metadata?.getRaw(), + authors: metadata?.get("dc:creator"), + numPages: pdfDocument.numPages, + URL: url, + }; +} +class GenericScripting { + constructor(sandboxBundleSrc) { + this._ready = new Promise((resolve, reject) => { + const sandbox = import(/*webpackIgnore: true*/ sandboxBundleSrc); + sandbox + .then((pdfjsSandbox) => { + resolve(pdfjsSandbox.QuickJSSandbox()); + }) + .catch(reject); + }); + } + async createSandbox(data) { + const sandbox = await this._ready; + sandbox.create(data); + } + async dispatchEventInSandbox(event) { + const sandbox = await this._ready; + setTimeout(() => sandbox.dispatchEvent(event), 0); + } + async destroySandbox() { + const sandbox = await this._ready; + sandbox.nukeSandbox(); + } +} // ./web/genericcom.js + +function initCom(app) {} +class Preferences extends BasePreferences { + async _writeToStorage(prefObj) { + localStorage.setItem("pdfjs.preferences", JSON.stringify(prefObj)); + } + async _readFromStorage(prefObj) { + return { + prefs: JSON.parse(localStorage.getItem("pdfjs.preferences")), + }; + } +} +class ExternalServices extends BaseExternalServices { + async createL10n() { + return new genericl10n_GenericL10n( + AppOptions.get("localeProperties")?.lang, + ); + } + createScripting() { + return new GenericScripting(AppOptions.get("sandboxBundleSrc")); + } +} +class MLManager { + async isEnabledFor(_name) { + return false; + } + async deleteModel(_service) { + return null; + } + isReady(_name) { + return false; + } + guess(_data) {} + toggleService(_name, _enabled) {} + static getFakeMLManager(options) { + return new FakeMLManager(options); + } +} +class FakeMLManager { + eventBus = null; + hasProgress = false; + constructor({ enableGuessAltText, enableAltTextModelDownload }) { + this.enableGuessAltText = enableGuessAltText; + this.enableAltTextModelDownload = enableAltTextModelDownload; + } + setEventBus(eventBus, abortSignal) { + this.eventBus = eventBus; + } + async isEnabledFor(_name) { + return this.enableGuessAltText; + } + async deleteModel(_name) { + this.enableAltTextModelDownload = false; + return null; + } + async loadModel(_name) {} + async downloadModel(_name) { + this.hasProgress = true; + const { promise, resolve } = Promise.withResolvers(); + const total = 1e8; + const end = 1.5 * total; + const increment = 5e6; + let loaded = 0; + const id = setInterval(() => { + loaded += increment; + if (loaded <= end) { + this.eventBus.dispatch("loadaiengineprogress", { + source: this, + detail: { + total, + totalLoaded: loaded, + finished: loaded + increment >= end, + }, + }); + return; + } + clearInterval(id); + this.hasProgress = false; + this.enableAltTextModelDownload = true; + resolve(true); + }, 900); + return promise; + } + isReady(_name) { + return this.enableAltTextModelDownload; + } + guess({ request: { data } }) { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + data + ? { + output: "Fake alt text.", + } + : { + error: true, + }, + ); + }, 3000); + }); + } + toggleService(_name, enabled) { + this.enableGuessAltText = enabled; + } +} // ./web/new_alt_text_manager.js + +class NewAltTextManager { + #boundCancel = this.#cancel.bind(this); + #createAutomaticallyButton; + #currentEditor = null; + #cancelButton; + #descriptionContainer; + #dialog; + #disclaimer; + #downloadModel; + #downloadModelDescription; + #eventBus; + #firstTime = false; + #guessedAltText; + #hasAI = null; + #isEditing = null; + #imagePreview; + #imageData; + #isAILoading = false; + #wasAILoading = false; + #learnMore; + #notNowButton; + #overlayManager; + #textarea; + #title; + #uiManager; + #previousAltText = null; + constructor( + { + descriptionContainer, + dialog, + imagePreview, + cancelButton, + disclaimer, + notNowButton, + saveButton, + textarea, + learnMore, + errorCloseButton, + createAutomaticallyButton, + downloadModel, + downloadModelDescription, + title, + }, + overlayManager, + eventBus, + ) { + this.#cancelButton = cancelButton; + this.#createAutomaticallyButton = createAutomaticallyButton; + this.#descriptionContainer = descriptionContainer; + this.#dialog = dialog; + this.#disclaimer = disclaimer; + this.#notNowButton = notNowButton; + this.#imagePreview = imagePreview; + this.#textarea = textarea; + this.#learnMore = learnMore; + this.#title = title; + this.#downloadModel = downloadModel; + this.#downloadModelDescription = downloadModelDescription; + this.#overlayManager = overlayManager; + this.#eventBus = eventBus; + dialog.addEventListener("close", this.#close.bind(this)); + dialog.addEventListener("contextmenu", (event) => { + if (event.target !== this.#textarea) { + event.preventDefault(); + } + }); + cancelButton.addEventListener("click", this.#boundCancel); + notNowButton.addEventListener("click", this.#boundCancel); + saveButton.addEventListener("click", this.#save.bind(this)); + errorCloseButton.addEventListener("click", () => { + this.#toggleError(false); + }); + createAutomaticallyButton.addEventListener("click", async () => { + const checked = + createAutomaticallyButton.getAttribute("aria-pressed") !== "true"; + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.alt_text.ai_generation_check", + data: { + status: checked, + }, + }); + if (this.#uiManager) { + this.#uiManager.setPreference("enableGuessAltText", checked); + await this.#uiManager.mlManager.toggleService("altText", checked); + } + this.#toggleGuessAltText(checked, false); + }); + textarea.addEventListener("focus", () => { + this.#wasAILoading = this.#isAILoading; + this.#toggleLoading(false); + this.#toggleTitleAndDisclaimer(); + }); + textarea.addEventListener("blur", () => { + if (!textarea.value) { + this.#toggleLoading(this.#wasAILoading); + } + this.#toggleTitleAndDisclaimer(); + }); + textarea.addEventListener("input", () => { + this.#toggleTitleAndDisclaimer(); + }); + eventBus._on("enableguessalttext", ({ value }) => { + this.#toggleGuessAltText(value, false); + }); + this.#overlayManager.register(dialog); + this.#learnMore.addEventListener("click", () => { + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.alt_text.info", + data: { + topic: "alt_text", + }, + }); + }); + } + #toggleLoading(value) { + if (!this.#uiManager || this.#isAILoading === value) { + return; + } + this.#isAILoading = value; + this.#descriptionContainer.classList.toggle("loading", value); + } + #toggleError(value) { + if (!this.#uiManager) { + return; + } + this.#dialog.classList.toggle("error", value); + } + async #toggleGuessAltText(value, isInitial = false) { + if (!this.#uiManager) { + return; + } + this.#dialog.classList.toggle("aiDisabled", !value); + this.#createAutomaticallyButton.setAttribute("aria-pressed", value); + if (value) { + const { altTextLearnMoreUrl } = this.#uiManager.mlManager; + if (altTextLearnMoreUrl) { + this.#learnMore.href = altTextLearnMoreUrl; + } + this.#mlGuessAltText(isInitial); + } else { + this.#toggleLoading(false); + this.#isAILoading = false; + this.#toggleTitleAndDisclaimer(); + } + } + #toggleNotNow() { + this.#notNowButton.classList.toggle("hidden", !this.#firstTime); + this.#cancelButton.classList.toggle("hidden", this.#firstTime); + } + #toggleAI(value) { + if (!this.#uiManager || this.#hasAI === value) { + return; + } + this.#hasAI = value; + this.#dialog.classList.toggle("noAi", !value); + this.#toggleTitleAndDisclaimer(); + } + #toggleTitleAndDisclaimer() { + const visible = + this.#isAILoading || + (this.#guessedAltText && this.#guessedAltText === this.#textarea.value); + this.#disclaimer.hidden = !visible; + const isEditing = this.#isAILoading || !!this.#textarea.value; + if (this.#isEditing === isEditing) { + return; + } + this.#isEditing = isEditing; + this.#title.setAttribute( + "data-l10n-id", + isEditing + ? "pdfjs-editor-new-alt-text-dialog-edit-label" + : "pdfjs-editor-new-alt-text-dialog-add-label", + ); + } + async #mlGuessAltText(isInitial) { + if (this.#isAILoading) { + return; + } + if (this.#textarea.value) { + return; + } + if (isInitial && this.#previousAltText !== null) { + return; + } + this.#guessedAltText = this.#currentEditor.guessedAltText; + if (this.#previousAltText === null && this.#guessedAltText) { + this.#addAltText(this.#guessedAltText); + return; + } + this.#toggleLoading(true); + this.#toggleTitleAndDisclaimer(); + let hasError = false; + try { + const altText = await this.#currentEditor.mlGuessAltText( + this.#imageData, + false, + ); + if (altText) { + this.#guessedAltText = altText; + this.#wasAILoading = this.#isAILoading; + if (this.#isAILoading) { + this.#addAltText(altText); + } + } + } catch (e) { + console.error(e); + hasError = true; + } + this.#toggleLoading(false); + this.#toggleTitleAndDisclaimer(); + if (hasError && this.#uiManager) { + this.#toggleError(true); + } + } + #addAltText(altText) { + if (!this.#uiManager || this.#textarea.value) { + return; + } + this.#textarea.value = altText; + this.#toggleTitleAndDisclaimer(); + } + #setProgress() { + this.#downloadModel.classList.toggle("hidden", false); + const callback = async ({ detail: { finished, total, totalLoaded } }) => { + const ONE_MEGA_BYTES = 1e6; + totalLoaded = Math.min(0.99 * total, totalLoaded); + const totalSize = (this.#downloadModelDescription.ariaValueMax = + Math.round(total / ONE_MEGA_BYTES)); + const downloadedSize = (this.#downloadModelDescription.ariaValueNow = + Math.round(totalLoaded / ONE_MEGA_BYTES)); + this.#downloadModelDescription.setAttribute( + "data-l10n-args", + JSON.stringify({ + totalSize, + downloadedSize, + }), + ); + if (!finished) { + return; + } + this.#eventBus._off("loadaiengineprogress", callback); + this.#downloadModel.classList.toggle("hidden", true); + this.#toggleAI(true); + if (!this.#uiManager) { + return; + } + const { mlManager } = this.#uiManager; + mlManager.toggleService("altText", true); + this.#toggleGuessAltText(await mlManager.isEnabledFor("altText"), true); + }; + this.#eventBus._on("loadaiengineprogress", callback); + } + async editAltText(uiManager, editor, firstTime) { + if (this.#currentEditor || !editor) { + return; + } + if (firstTime && editor.hasAltTextData()) { + editor.altTextFinish(); + return; + } + this.#firstTime = firstTime; + let { mlManager } = uiManager; + let hasAI = !!mlManager; + this.#toggleTitleAndDisclaimer(); + if (mlManager && !mlManager.isReady("altText")) { + hasAI = false; + if (mlManager.hasProgress) { + this.#setProgress(); + } else { + mlManager = null; + } + } else { + this.#downloadModel.classList.toggle("hidden", true); + } + const isAltTextEnabledPromise = mlManager?.isEnabledFor("altText"); + this.#currentEditor = editor; + this.#uiManager = uiManager; + this.#uiManager.removeEditListeners(); + ({ altText: this.#previousAltText } = editor.altTextData); + this.#textarea.value = this.#previousAltText ?? ""; + const AI_MAX_IMAGE_DIMENSION = 224; + const MAX_PREVIEW_DIMENSION = 180; + let canvas, width, height; + if (mlManager) { + ({ + canvas, + width, + height, + imageData: this.#imageData, + } = editor.copyCanvas( + AI_MAX_IMAGE_DIMENSION, + MAX_PREVIEW_DIMENSION, + true, + )); + if (hasAI) { + this.#toggleGuessAltText(await isAltTextEnabledPromise, true); + } + } else { + ({ canvas, width, height } = editor.copyCanvas( + AI_MAX_IMAGE_DIMENSION, + MAX_PREVIEW_DIMENSION, + false, + )); + } + canvas.setAttribute("role", "presentation"); + const { style } = canvas; + style.width = `${width}px`; + style.height = `${height}px`; + this.#imagePreview.append(canvas); + this.#toggleNotNow(); + this.#toggleAI(hasAI); + this.#toggleError(false); + try { + await this.#overlayManager.open(this.#dialog); + } catch (ex) { + this.#close(); + throw ex; + } + } + #cancel() { + this.#currentEditor.altTextData = { + cancel: true, + }; + const altText = this.#textarea.value.trim(); + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.alt_text.dismiss", + data: { + alt_text_type: altText ? "present" : "empty", + flow: this.#firstTime ? "image_add" : "alt_text_edit", + }, + }); + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.image_added", + data: { + alt_text_modal: true, + alt_text_type: "skipped", + }, + }); + this.#finish(); + } + #finish() { + if (this.#overlayManager.active === this.#dialog) { + this.#overlayManager.close(this.#dialog); + } + } + #close() { + const canvas = this.#imagePreview.firstChild; + canvas.remove(); + canvas.width = canvas.height = 0; + this.#imageData = null; + this.#toggleLoading(false); + this.#uiManager?.addEditListeners(); + this.#currentEditor.altTextFinish(); + this.#uiManager?.setSelected(this.#currentEditor); + this.#currentEditor = null; + this.#uiManager = null; + } + #extractWords(text) { + return new Set( + text + .toLowerCase() + .split(/[^\p{L}\p{N}]+/gu) + .filter((x) => !!x), + ); + } + #save() { + const altText = this.#textarea.value.trim(); + this.#currentEditor.altTextData = { + altText, + decorative: false, + }; + this.#currentEditor.altTextData.guessedAltText = this.#guessedAltText; + if (this.#guessedAltText && this.#guessedAltText !== altText) { + const guessedWords = this.#extractWords(this.#guessedAltText); + const words = this.#extractWords(altText); + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.alt_text.user_edit", + data: { + total_words: guessedWords.size, + words_removed: guessedWords.difference(words).size, + words_added: words.difference(guessedWords).size, + }, + }); + } + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.image_added", + data: { + alt_text_modal: true, + alt_text_type: altText ? "present" : "empty", + }, + }); + this.#currentEditor._reportTelemetry({ + action: "pdfjs.image.alt_text.save", + data: { + alt_text_type: altText ? "present" : "empty", + flow: this.#firstTime ? "image_add" : "alt_text_edit", + }, + }); + this.#finish(); + } + destroy() { + this.#uiManager = null; + this.#finish(); + } +} +class ImageAltTextSettings { + #aiModelSettings; + #createModelButton; + #downloadModelButton; + #dialog; + #eventBus; + #mlManager; + #overlayManager; + #showAltTextDialogButton; + constructor( + { + dialog, + createModelButton, + aiModelSettings, + learnMore, + closeButton, + deleteModelButton, + downloadModelButton, + showAltTextDialogButton, + }, + overlayManager, + eventBus, + mlManager, + ) { + this.#dialog = dialog; + this.#aiModelSettings = aiModelSettings; + this.#createModelButton = createModelButton; + this.#downloadModelButton = downloadModelButton; + this.#showAltTextDialogButton = showAltTextDialogButton; + this.#overlayManager = overlayManager; + this.#eventBus = eventBus; + this.#mlManager = mlManager; + const { altTextLearnMoreUrl } = mlManager; + if (altTextLearnMoreUrl) { + learnMore.href = altTextLearnMoreUrl; + } + dialog.addEventListener("contextmenu", noContextMenu); + createModelButton.addEventListener("click", async (e) => { + const checked = this.#togglePref("enableGuessAltText", e); + await mlManager.toggleService("altText", checked); + this.#reportTelemetry({ + type: "stamp", + action: "pdfjs.image.alt_text.settings_ai_generation_check", + data: { + status: checked, + }, + }); + }); + showAltTextDialogButton.addEventListener("click", (e) => { + const checked = this.#togglePref("enableNewAltTextWhenAddingImage", e); + this.#reportTelemetry({ + type: "stamp", + action: "pdfjs.image.alt_text.settings_edit_alt_text_check", + data: { + status: checked, + }, + }); + }); + deleteModelButton.addEventListener("click", this.#delete.bind(this, true)); + downloadModelButton.addEventListener( + "click", + this.#download.bind(this, true), + ); + closeButton.addEventListener("click", this.#finish.bind(this)); + learnMore.addEventListener("click", () => { + this.#reportTelemetry({ + type: "stamp", + action: "pdfjs.image.alt_text.info", + data: { + topic: "ai_generation", + }, + }); + }); + eventBus._on("enablealttextmodeldownload", ({ value }) => { + if (value) { + this.#download(false); + } else { + this.#delete(false); + } + }); + this.#overlayManager.register(dialog); + } + #reportTelemetry(data) { + this.#eventBus.dispatch("reporttelemetry", { + source: this, + details: { + type: "editing", + data, + }, + }); + } + async #download(isFromUI = false) { + if (isFromUI) { + this.#downloadModelButton.disabled = true; + const span = this.#downloadModelButton.firstChild; + span.setAttribute( + "data-l10n-id", + "pdfjs-editor-alt-text-settings-downloading-model-button", + ); + await this.#mlManager.downloadModel("altText"); + span.setAttribute( + "data-l10n-id", + "pdfjs-editor-alt-text-settings-download-model-button", + ); + this.#createModelButton.disabled = false; + this.#setPref("enableGuessAltText", true); + this.#mlManager.toggleService("altText", true); + this.#setPref("enableAltTextModelDownload", true); + this.#downloadModelButton.disabled = false; + } + this.#aiModelSettings.classList.toggle("download", false); + this.#createModelButton.setAttribute("aria-pressed", true); + } + async #delete(isFromUI = false) { + if (isFromUI) { + await this.#mlManager.deleteModel("altText"); + this.#setPref("enableGuessAltText", false); + this.#setPref("enableAltTextModelDownload", false); + } + this.#aiModelSettings.classList.toggle("download", true); + this.#createModelButton.disabled = true; + this.#createModelButton.setAttribute("aria-pressed", false); + } + async open({ enableGuessAltText, enableNewAltTextWhenAddingImage }) { + const { enableAltTextModelDownload } = this.#mlManager; + this.#createModelButton.disabled = !enableAltTextModelDownload; + this.#createModelButton.setAttribute( + "aria-pressed", + enableAltTextModelDownload && enableGuessAltText, + ); + this.#showAltTextDialogButton.setAttribute( + "aria-pressed", + enableNewAltTextWhenAddingImage, + ); + this.#aiModelSettings.classList.toggle( + "download", + !enableAltTextModelDownload, + ); + await this.#overlayManager.open(this.#dialog); + this.#reportTelemetry({ + type: "stamp", + action: "pdfjs.image.alt_text.settings_displayed", + }); + } + #togglePref(name, { target }) { + const checked = target.getAttribute("aria-pressed") !== "true"; + this.#setPref(name, checked); + target.setAttribute("aria-pressed", checked); + return checked; + } + #setPref(name, value) { + this.#eventBus.dispatch("setpreference", { + source: this, + name, + value, + }); + } + #finish() { + if (this.#overlayManager.active === this.#dialog) { + this.#overlayManager.close(this.#dialog); + } + } +} // ./web/alt_text_manager.js + +class AltTextManager { + #clickAC = null; + #currentEditor = null; + #cancelButton; + #dialog; + #eventBus; + #hasUsedPointer = false; + #optionDescription; + #optionDecorative; + #overlayManager; + #saveButton; + #textarea; + #uiManager; + #previousAltText = null; + #resizeAC = null; + #svgElement = null; + #rectElement = null; + #container; + #telemetryData = null; + constructor( + { + dialog, + optionDescription, + optionDecorative, + textarea, + cancelButton, + saveButton, + }, + container, + overlayManager, + eventBus, + ) { + this.#dialog = dialog; + this.#optionDescription = optionDescription; + this.#optionDecorative = optionDecorative; + this.#textarea = textarea; + this.#cancelButton = cancelButton; + this.#saveButton = saveButton; + this.#overlayManager = overlayManager; + this.#eventBus = eventBus; + this.#container = container; + const onUpdateUIState = this.#updateUIState.bind(this); + dialog.addEventListener("close", this.#close.bind(this)); + dialog.addEventListener("contextmenu", (event) => { + if (event.target !== this.#textarea) { + event.preventDefault(); + } + }); + cancelButton.addEventListener("click", this.#finish.bind(this)); + saveButton.addEventListener("click", this.#save.bind(this)); + optionDescription.addEventListener("change", onUpdateUIState); + optionDecorative.addEventListener("change", onUpdateUIState); + this.#overlayManager.register(dialog); + } + #createSVGElement() { + if (this.#svgElement) { + return; + } + const svgFactory = new DOMSVGFactory(); + const svg = (this.#svgElement = svgFactory.createElement("svg")); + svg.setAttribute("width", "0"); + svg.setAttribute("height", "0"); + const defs = svgFactory.createElement("defs"); + svg.append(defs); + const mask = svgFactory.createElement("mask"); + defs.append(mask); + mask.setAttribute("id", "alttext-manager-mask"); + mask.setAttribute("maskContentUnits", "objectBoundingBox"); + let rect = svgFactory.createElement("rect"); + mask.append(rect); + rect.setAttribute("fill", "white"); + rect.setAttribute("width", "1"); + rect.setAttribute("height", "1"); + rect.setAttribute("x", "0"); + rect.setAttribute("y", "0"); + rect = this.#rectElement = svgFactory.createElement("rect"); + mask.append(rect); + rect.setAttribute("fill", "black"); + this.#dialog.append(svg); + } + async editAltText(uiManager, editor) { + if (this.#currentEditor || !editor) { + return; + } + this.#createSVGElement(); + this.#hasUsedPointer = false; + this.#clickAC = new AbortController(); + const clickOpts = { + signal: this.#clickAC.signal, + }, + onClick = this.#onClick.bind(this); + for (const element of [ + this.#optionDescription, + this.#optionDecorative, + this.#textarea, + this.#saveButton, + this.#cancelButton, + ]) { + element.addEventListener("click", onClick, clickOpts); + } + const { altText, decorative } = editor.altTextData; + if (decorative === true) { + this.#optionDecorative.checked = true; + this.#optionDescription.checked = false; + } else { + this.#optionDecorative.checked = false; + this.#optionDescription.checked = true; + } + this.#previousAltText = this.#textarea.value = altText?.trim() || ""; + this.#updateUIState(); + this.#currentEditor = editor; + this.#uiManager = uiManager; + this.#uiManager.removeEditListeners(); + this.#resizeAC = new AbortController(); + this.#eventBus._on("resize", this.#setPosition.bind(this), { + signal: this.#resizeAC.signal, + }); + try { + await this.#overlayManager.open(this.#dialog); + this.#setPosition(); + } catch (ex) { + this.#close(); + throw ex; + } + } + #setPosition() { + if (!this.#currentEditor) { + return; + } + const dialog = this.#dialog; + const { style } = dialog; + const { + x: containerX, + y: containerY, + width: containerW, + height: containerH, + } = this.#container.getBoundingClientRect(); + const { innerWidth: windowW, innerHeight: windowH } = window; + const { width: dialogW, height: dialogH } = dialog.getBoundingClientRect(); + const { x, y, width, height } = this.#currentEditor.getClientDimensions(); + const MARGIN = 10; + const isLTR = this.#uiManager.direction === "ltr"; + const xs = Math.max(x, containerX); + const xe = Math.min(x + width, containerX + containerW); + const ys = Math.max(y, containerY); + const ye = Math.min(y + height, containerY + containerH); + this.#rectElement.setAttribute("width", `${(xe - xs) / windowW}`); + this.#rectElement.setAttribute("height", `${(ye - ys) / windowH}`); + this.#rectElement.setAttribute("x", `${xs / windowW}`); + this.#rectElement.setAttribute("y", `${ys / windowH}`); + let left = null; + let top = Math.max(y, 0); + top += Math.min(windowH - (top + dialogH), 0); + if (isLTR) { + if (x + width + MARGIN + dialogW < windowW) { + left = x + width + MARGIN; + } else if (x > dialogW + MARGIN) { + left = x - dialogW - MARGIN; + } + } else if (x > dialogW + MARGIN) { + left = x - dialogW - MARGIN; + } else if (x + width + MARGIN + dialogW < windowW) { + left = x + width + MARGIN; + } + if (left === null) { + top = null; + left = Math.max(x, 0); + left += Math.min(windowW - (left + dialogW), 0); + if (y > dialogH + MARGIN) { + top = y - dialogH - MARGIN; + } else if (y + height + MARGIN + dialogH < windowH) { + top = y + height + MARGIN; + } + } + if (top !== null) { + dialog.classList.add("positioned"); + if (isLTR) { + style.left = `${left}px`; + } else { + style.right = `${windowW - left - dialogW}px`; + } + style.top = `${top}px`; + } else { + dialog.classList.remove("positioned"); + style.left = ""; + style.top = ""; + } + } + #finish() { + if (this.#overlayManager.active === this.#dialog) { + this.#overlayManager.close(this.#dialog); + } + } + #close() { + this.#currentEditor._reportTelemetry( + this.#telemetryData || { + action: "alt_text_cancel", + alt_text_keyboard: !this.#hasUsedPointer, + }, + ); + this.#telemetryData = null; + this.#removeOnClickListeners(); + this.#uiManager?.addEditListeners(); + this.#resizeAC?.abort(); + this.#resizeAC = null; + this.#currentEditor.altTextFinish(); + this.#currentEditor = null; + this.#uiManager = null; + } + #updateUIState() { + this.#textarea.disabled = this.#optionDecorative.checked; + } + #save() { + const altText = this.#textarea.value.trim(); + const decorative = this.#optionDecorative.checked; + this.#currentEditor.altTextData = { + altText, + decorative, + }; + this.#telemetryData = { + action: "alt_text_save", + alt_text_description: !!altText, + alt_text_edit: + !!this.#previousAltText && this.#previousAltText !== altText, + alt_text_decorative: decorative, + alt_text_keyboard: !this.#hasUsedPointer, + }; + this.#finish(); + } + #onClick(evt) { + if (evt.detail === 0) { + return; + } + this.#hasUsedPointer = true; + this.#removeOnClickListeners(); + } + #removeOnClickListeners() { + this.#clickAC?.abort(); + this.#clickAC = null; + } + destroy() { + this.#uiManager = null; + this.#finish(); + this.#svgElement?.remove(); + this.#svgElement = this.#rectElement = null; + } +} // ./web/annotation_editor_params.js + +class AnnotationEditorParams { + constructor(options, eventBus) { + this.eventBus = eventBus; + this.#bindListeners(options); + } + #bindListeners({ + editorFreeTextFontSize, + editorFreeTextColor, + editorInkColor, + editorInkThickness, + editorInkOpacity, + editorStampAddImage, + editorFreeHighlightThickness, + editorHighlightShowAll, + }) { + const dispatchEvent = (typeStr, value) => { + this.eventBus.dispatch("switchannotationeditorparams", { + source: this, + type: AnnotationEditorParamsType[typeStr], + value, + }); + }; + editorFreeTextFontSize.addEventListener("input", function () { + dispatchEvent("FREETEXT_SIZE", this.valueAsNumber); + }); + editorFreeTextColor.addEventListener("input", function () { + dispatchEvent("FREETEXT_COLOR", this.value); + }); + editorInkColor.addEventListener("input", function () { + dispatchEvent("INK_COLOR", this.value); + }); + editorInkThickness.addEventListener("input", function () { + dispatchEvent("INK_THICKNESS", this.valueAsNumber); + }); + editorInkOpacity.addEventListener("input", function () { + dispatchEvent("INK_OPACITY", this.valueAsNumber); + }); + editorStampAddImage.addEventListener("click", () => { + this.eventBus.dispatch("reporttelemetry", { + source: this, + details: { + type: "editing", + data: { + action: "pdfjs.image.add_image_click", + }, + }, + }); + dispatchEvent("CREATE"); + }); + editorFreeHighlightThickness.addEventListener("input", function () { + dispatchEvent("HIGHLIGHT_THICKNESS", this.valueAsNumber); + }); + editorHighlightShowAll.addEventListener("click", function () { + const checked = this.getAttribute("aria-pressed") === "true"; + this.setAttribute("aria-pressed", !checked); + dispatchEvent("HIGHLIGHT_SHOW_ALL", !checked); + }); + this.eventBus._on("annotationeditorparamschanged", (evt) => { + for (const [type, value] of evt.details) { + switch (type) { + case AnnotationEditorParamsType.FREETEXT_SIZE: + editorFreeTextFontSize.value = value; + break; + case AnnotationEditorParamsType.FREETEXT_COLOR: + editorFreeTextColor.value = value; + break; + case AnnotationEditorParamsType.INK_COLOR: + editorInkColor.value = value; + break; + case AnnotationEditorParamsType.INK_THICKNESS: + editorInkThickness.value = value; + break; + case AnnotationEditorParamsType.INK_OPACITY: + editorInkOpacity.value = value; + break; + case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS: + editorFreeHighlightThickness.value = value; + break; + case AnnotationEditorParamsType.HIGHLIGHT_FREE: + editorFreeHighlightThickness.disabled = !value; + break; + case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL: + editorHighlightShowAll.setAttribute("aria-pressed", value); + break; + } + } + }); + } +} // ./web/caret_browsing.js + +const PRECISION = 1e-1; +class CaretBrowsingMode { + #mainContainer; + #toolBarHeight = 0; + #viewerContainer; + constructor(abortSignal, mainContainer, viewerContainer, toolbarContainer) { + this.#mainContainer = mainContainer; + this.#viewerContainer = viewerContainer; + if (!toolbarContainer) { + return; + } + this.#toolBarHeight = toolbarContainer.getBoundingClientRect().height; + const toolbarObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === toolbarContainer) { + this.#toolBarHeight = Math.floor(entry.borderBoxSize[0].blockSize); + break; + } + } + }); + toolbarObserver.observe(toolbarContainer); + abortSignal.addEventListener("abort", () => toolbarObserver.disconnect(), { + once: true, + }); + } + #isOnSameLine(rect1, rect2) { + const top1 = rect1.y; + const bot1 = rect1.bottom; + const mid1 = rect1.y + rect1.height / 2; + const top2 = rect2.y; + const bot2 = rect2.bottom; + const mid2 = rect2.y + rect2.height / 2; + return (top1 <= mid2 && mid2 <= bot1) || (top2 <= mid1 && mid1 <= bot2); + } + #isUnderOver(rect, x, y, isUp) { + const midY = rect.y + rect.height / 2; + return ( + (isUp ? y >= midY : y <= midY) && + rect.x - PRECISION <= x && + x <= rect.right + PRECISION + ); + } + #isVisible(rect) { + return ( + rect.top >= this.#toolBarHeight && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } + #getCaretPosition(selection, isUp) { + const { focusNode, focusOffset } = selection; + const range = document.createRange(); + range.setStart(focusNode, focusOffset); + range.setEnd(focusNode, focusOffset); + const rect = range.getBoundingClientRect(); + return [rect.x, isUp ? rect.top : rect.bottom]; + } + static #caretPositionFromPoint(x, y) { + if (!document.caretPositionFromPoint) { + const { startContainer: offsetNode, startOffset: offset } = + document.caretRangeFromPoint(x, y); + return { + offsetNode, + offset, + }; + } + return document.caretPositionFromPoint(x, y); + } + #setCaretPositionHelper(selection, caretX, select, element, rect) { + rect ||= element.getBoundingClientRect(); + if (caretX <= rect.x + PRECISION) { + if (select) { + selection.extend(element.firstChild, 0); + } else { + selection.setPosition(element.firstChild, 0); + } + return; + } + if (rect.right - PRECISION <= caretX) { + const { lastChild } = element; + if (select) { + selection.extend(lastChild, lastChild.length); + } else { + selection.setPosition(lastChild, lastChild.length); + } + return; + } + const midY = rect.y + rect.height / 2; + let caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY); + let parentElement = caretPosition.offsetNode?.parentElement; + if (parentElement && parentElement !== element) { + const elementsAtPoint = document.elementsFromPoint(caretX, midY); + const savedVisibilities = []; + for (const el of elementsAtPoint) { + if (el === element) { + break; + } + const { style } = el; + savedVisibilities.push([el, style.visibility]); + style.visibility = "hidden"; + } + caretPosition = CaretBrowsingMode.#caretPositionFromPoint(caretX, midY); + parentElement = caretPosition.offsetNode?.parentElement; + for (const [el, visibility] of savedVisibilities) { + el.style.visibility = visibility; + } + } + if (parentElement !== element) { + if (select) { + selection.extend(element.firstChild, 0); + } else { + selection.setPosition(element.firstChild, 0); + } + return; + } + if (select) { + selection.extend(caretPosition.offsetNode, caretPosition.offset); + } else { + selection.setPosition(caretPosition.offsetNode, caretPosition.offset); + } + } + #setCaretPosition( + select, + selection, + newLineElement, + newLineElementRect, + caretX, + ) { + if (this.#isVisible(newLineElementRect)) { + this.#setCaretPositionHelper( + selection, + caretX, + select, + newLineElement, + newLineElementRect, + ); + return; + } + this.#mainContainer.addEventListener( + "scrollend", + this.#setCaretPositionHelper.bind( + this, + selection, + caretX, + select, + newLineElement, + null, + ), + { + once: true, + }, + ); + newLineElement.scrollIntoView(); + } + #getNodeOnNextPage(textLayer, isUp) { + while (true) { + const page = textLayer.closest(".page"); + const pageNumber = parseInt(page.getAttribute("data-page-number")); + const nextPage = isUp ? pageNumber - 1 : pageNumber + 1; + textLayer = this.#viewerContainer.querySelector( + `.page[data-page-number="${nextPage}"] .textLayer`, + ); + if (!textLayer) { + return null; + } + const walker = document.createTreeWalker(textLayer, NodeFilter.SHOW_TEXT); + const node = isUp ? walker.lastChild() : walker.firstChild(); + if (node) { + return node; + } + } + } + moveCaret(isUp, select) { + const selection = document.getSelection(); + if (selection.rangeCount === 0) { + return; + } + const { focusNode } = selection; + const focusElement = + focusNode.nodeType !== Node.ELEMENT_NODE + ? focusNode.parentElement + : focusNode; + const root = focusElement.closest(".textLayer"); + if (!root) { + return; + } + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + walker.currentNode = focusNode; + const focusRect = focusElement.getBoundingClientRect(); + let newLineElement = null; + const nodeIterator = ( + isUp ? walker.previousSibling : walker.nextSibling + ).bind(walker); + while (nodeIterator()) { + const element = walker.currentNode.parentElement; + if (!this.#isOnSameLine(focusRect, element.getBoundingClientRect())) { + newLineElement = element; + break; + } + } + if (!newLineElement) { + const node = this.#getNodeOnNextPage(root, isUp); + if (!node) { + return; + } + if (select) { + const lastNode = + (isUp ? walker.firstChild() : walker.lastChild()) || focusNode; + selection.extend(lastNode, isUp ? 0 : lastNode.length); + const range = document.createRange(); + range.setStart(node, isUp ? node.length : 0); + range.setEnd(node, isUp ? node.length : 0); + selection.addRange(range); + return; + } + const [caretX] = this.#getCaretPosition(selection, isUp); + const { parentElement } = node; + this.#setCaretPosition( + select, + selection, + parentElement, + parentElement.getBoundingClientRect(), + caretX, + ); + return; + } + const [caretX, caretY] = this.#getCaretPosition(selection, isUp); + const newLineElementRect = newLineElement.getBoundingClientRect(); + if (this.#isUnderOver(newLineElementRect, caretX, caretY, isUp)) { + this.#setCaretPosition( + select, + selection, + newLineElement, + newLineElementRect, + caretX, + ); + return; + } + while (nodeIterator()) { + const element = walker.currentNode.parentElement; + const elementRect = element.getBoundingClientRect(); + if (!this.#isOnSameLine(newLineElementRect, elementRect)) { + break; + } + if (this.#isUnderOver(elementRect, caretX, caretY, isUp)) { + this.#setCaretPosition(select, selection, element, elementRect, caretX); + return; + } + } + this.#setCaretPosition( + select, + selection, + newLineElement, + newLineElementRect, + caretX, + ); + } +} // ./web/download_manager.js + +function download(blobUrl, filename) { + const a = document.createElement("a"); + if (!a.click) { + throw new Error('DownloadManager: "a.click()" is not supported.'); + } + a.href = blobUrl; + a.target = "_parent"; + if ("download" in a) { + a.download = filename; + } + (document.body || document.documentElement).append(a); + a.click(); + a.remove(); +} +class DownloadManager { + #openBlobUrls = new WeakMap(); + downloadData(data, filename, contentType) { + const blobUrl = URL.createObjectURL( + new Blob([data], { + type: contentType, + }), + ); + download(blobUrl, filename); + } + openOrDownloadData(data, filename, dest = null) { + const isPdfData = isPdfFile(filename); + const contentType = isPdfData ? "application/pdf" : ""; + if (isPdfData) { + let blobUrl = this.#openBlobUrls.get(data); + if (!blobUrl) { + blobUrl = URL.createObjectURL( + new Blob([data], { + type: contentType, + }), + ); + this.#openBlobUrls.set(data, blobUrl); + } + let viewerUrl; + viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename); + if (dest) { + viewerUrl += `#${escape(dest)}`; + } + try { + window.open(viewerUrl); + return true; + } catch (ex) { + console.error("openOrDownloadData:", ex); + URL.revokeObjectURL(blobUrl); + this.#openBlobUrls.delete(data); + } + } + this.downloadData(data, filename, contentType); + return false; + } + download(data, url, filename) { + let blobUrl; + if (data) { + blobUrl = URL.createObjectURL( + new Blob([data], { + type: "application/pdf", + }), + ); + } else { + if (!createValidAbsoluteUrl(url, "http://example.com")) { + console.error(`download - not a valid URL: ${url}`); + return; + } + blobUrl = url + "#pdfjs.action=download"; + } + download(blobUrl, filename); + } +} // ./web/editor_undo_bar.js + +class EditorUndoBar { + #closeButton = null; + #container; + #eventBus = null; + #focusTimeout = null; + #initController = null; + isOpen = false; + #message; + #showController = null; + #undoButton; + static #l10nMessages = Object.freeze({ + highlight: "pdfjs-editor-undo-bar-message-highlight", + freetext: "pdfjs-editor-undo-bar-message-freetext", + stamp: "pdfjs-editor-undo-bar-message-stamp", + ink: "pdfjs-editor-undo-bar-message-ink", + _multiple: "pdfjs-editor-undo-bar-message-multiple", + }); + constructor({ container, message, undoButton, closeButton }, eventBus) { + this.#container = container; + this.#message = message; + this.#undoButton = undoButton; + this.#closeButton = closeButton; + this.#eventBus = eventBus; + } + destroy() { + this.#initController?.abort(); + this.#initController = null; + this.hide(); + } + show(undoAction, messageData) { + if (!this.#initController) { + this.#initController = new AbortController(); + const opts = { + signal: this.#initController.signal, + }; + const boundHide = this.hide.bind(this); + this.#container.addEventListener("contextmenu", noContextMenu, opts); + this.#closeButton.addEventListener("click", boundHide, opts); + this.#eventBus._on("beforeprint", boundHide, opts); + this.#eventBus._on("download", boundHide, opts); + } + this.hide(); + if (typeof messageData === "string") { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages[messageData], + ); + } else { + this.#message.setAttribute( + "data-l10n-id", + EditorUndoBar.#l10nMessages._multiple, + ); + this.#message.setAttribute( + "data-l10n-args", + JSON.stringify({ + count: messageData, + }), + ); + } + this.isOpen = true; + this.#container.hidden = false; + this.#showController = new AbortController(); + this.#undoButton.addEventListener( + "click", + () => { + undoAction(); + this.hide(); + }, + { + signal: this.#showController.signal, + }, + ); + this.#focusTimeout = setTimeout(() => { + this.#container.focus(); + this.#focusTimeout = null; + }, 100); + } + hide() { + if (!this.isOpen) { + return; + } + this.isOpen = false; + this.#container.hidden = true; + this.#showController?.abort(); + this.#showController = null; + if (this.#focusTimeout) { + clearTimeout(this.#focusTimeout); + this.#focusTimeout = null; + } + } +} // ./web/overlay_manager.js + +class OverlayManager { + #overlays = new WeakMap(); + #active = null; + get active() { + return this.#active; + } + async register(dialog, canForceClose = false) { + if (typeof dialog !== "object") { + throw new Error("Not enough parameters."); + } else if (this.#overlays.has(dialog)) { + throw new Error("The overlay is already registered."); + } + this.#overlays.set(dialog, { + canForceClose, + }); + dialog.addEventListener("cancel", (evt) => { + this.#active = null; + }); + } + async open(dialog) { + if (!this.#overlays.has(dialog)) { + throw new Error("The overlay does not exist."); + } else if (this.#active) { + if (this.#active === dialog) { + throw new Error("The overlay is already active."); + } else if (this.#overlays.get(dialog).canForceClose) { + await this.close(); + } else { + throw new Error("Another overlay is currently active."); + } + } + this.#active = dialog; + dialog.showModal(); + } + async close(dialog = this.#active) { + if (!this.#overlays.has(dialog)) { + throw new Error("The overlay does not exist."); + } else if (!this.#active) { + throw new Error("The overlay is currently not active."); + } else if (this.#active !== dialog) { + throw new Error("Another overlay is currently active."); + } + dialog.close(); + this.#active = null; + } +} // ./web/password_prompt.js + +class PasswordPrompt { + #activeCapability = null; + #updateCallback = null; + #reason = null; + constructor(options, overlayManager, isViewerEmbedded = false) { + this.dialog = options.dialog; + this.label = options.label; + this.input = options.input; + this.submitButton = options.submitButton; + this.cancelButton = options.cancelButton; + this.overlayManager = overlayManager; + this._isViewerEmbedded = isViewerEmbedded; + this.submitButton.addEventListener("click", this.#verify.bind(this)); + this.cancelButton.addEventListener("click", this.close.bind(this)); + this.input.addEventListener("keydown", (e) => { + if (e.keyCode === 13) { + this.#verify(); + } + }); + this.overlayManager.register(this.dialog, true); + this.dialog.addEventListener("close", this.#cancel.bind(this)); + } + async open() { + await this.#activeCapability?.promise; + this.#activeCapability = Promise.withResolvers(); + try { + await this.overlayManager.open(this.dialog); + } catch (ex) { + this.#activeCapability.resolve(); + throw ex; + } + const passwordIncorrect = + this.#reason === PasswordResponses.INCORRECT_PASSWORD; + if (!this._isViewerEmbedded || passwordIncorrect) { + this.input.focus(); + } + this.label.setAttribute( + "data-l10n-id", + passwordIncorrect ? "pdfjs-password-invalid" : "pdfjs-password-label", + ); + } + async close() { + if (this.overlayManager.active === this.dialog) { + this.overlayManager.close(this.dialog); + } + } + #verify() { + const password = this.input.value; + if (password?.length > 0) { + this.#invokeCallback(password); + } + } + #cancel() { + this.#invokeCallback(new Error("PasswordPrompt cancelled.")); + this.#activeCapability.resolve(); + } + #invokeCallback(password) { + if (!this.#updateCallback) { + return; + } + this.close(); + this.input.value = ""; + this.#updateCallback(password); + this.#updateCallback = null; + } + async setUpdateCallback(updateCallback, reason) { + if (this.#activeCapability) { + await this.#activeCapability.promise; + } + this.#updateCallback = updateCallback; + this.#reason = reason; + } +} // ./web/base_tree_viewer.js + +const TREEITEM_OFFSET_TOP = -100; +const TREEITEM_SELECTED_CLASS = "selected"; +class BaseTreeViewer { + constructor(options) { + this.container = options.container; + this.eventBus = options.eventBus; + this._l10n = options.l10n; + this.reset(); + } + reset() { + this._pdfDocument = null; + this._lastToggleIsShow = true; + this._currentTreeItem = null; + this.container.textContent = ""; + this.container.classList.remove("treeWithDeepNesting"); + } + _dispatchEvent(count) { + throw new Error("Not implemented: _dispatchEvent"); + } + _bindLink(element, params) { + throw new Error("Not implemented: _bindLink"); + } + _normalizeTextContent(str) { + return removeNullCharacters(str, true) || "\u2013"; + } + _addToggleButton(div, hidden = false) { + const toggler = document.createElement("div"); + toggler.className = "treeItemToggler"; + if (hidden) { + toggler.classList.add("treeItemsHidden"); + } + toggler.onclick = (evt) => { + evt.stopPropagation(); + toggler.classList.toggle("treeItemsHidden"); + if (evt.shiftKey) { + const shouldShowAll = !toggler.classList.contains("treeItemsHidden"); + this._toggleTreeItem(div, shouldShowAll); + } + }; + div.prepend(toggler); + } + _toggleTreeItem(root, show = false) { + this._l10n.pause(); + this._lastToggleIsShow = show; + for (const toggler of root.querySelectorAll(".treeItemToggler")) { + toggler.classList.toggle("treeItemsHidden", !show); + } + this._l10n.resume(); + } + _toggleAllTreeItems() { + this._toggleTreeItem(this.container, !this._lastToggleIsShow); + } + _finishRendering(fragment, count, hasAnyNesting = false) { + if (hasAnyNesting) { + this.container.classList.add("treeWithDeepNesting"); + this._lastToggleIsShow = !fragment.querySelector(".treeItemsHidden"); + } + this._l10n.pause(); + this.container.append(fragment); + this._l10n.resume(); + this._dispatchEvent(count); + } + render(params) { + throw new Error("Not implemented: render"); + } + _updateCurrentTreeItem(treeItem = null) { + if (this._currentTreeItem) { + this._currentTreeItem.classList.remove(TREEITEM_SELECTED_CLASS); + this._currentTreeItem = null; + } + if (treeItem) { + treeItem.classList.add(TREEITEM_SELECTED_CLASS); + this._currentTreeItem = treeItem; + } + } + _scrollToCurrentTreeItem(treeItem) { + if (!treeItem) { + return; + } + this._l10n.pause(); + let currentNode = treeItem.parentNode; + while (currentNode && currentNode !== this.container) { + if (currentNode.classList.contains("treeItem")) { + const toggler = currentNode.firstElementChild; + toggler?.classList.remove("treeItemsHidden"); + } + currentNode = currentNode.parentNode; + } + this._l10n.resume(); + this._updateCurrentTreeItem(treeItem); + this.container.scrollTo( + treeItem.offsetLeft, + treeItem.offsetTop + TREEITEM_OFFSET_TOP, + ); + } +} // ./web/pdf_attachment_viewer.js + +class PDFAttachmentViewer extends BaseTreeViewer { + constructor(options) { + super(options); + this.downloadManager = options.downloadManager; + this.eventBus._on( + "fileattachmentannotation", + this.#appendAttachment.bind(this), + ); + } + reset(keepRenderedCapability = false) { + super.reset(); + this._attachments = null; + if (!keepRenderedCapability) { + this._renderedCapability = Promise.withResolvers(); + } + this._pendingDispatchEvent = false; + } + async _dispatchEvent(attachmentsCount) { + this._renderedCapability.resolve(); + if (attachmentsCount === 0 && !this._pendingDispatchEvent) { + this._pendingDispatchEvent = true; + await waitOnEventOrTimeout({ + target: this.eventBus, + name: "annotationlayerrendered", + delay: 1000, + }); + if (!this._pendingDispatchEvent) { + return; + } + } + this._pendingDispatchEvent = false; + this.eventBus.dispatch("attachmentsloaded", { + source: this, + attachmentsCount, + }); + } + _bindLink(element, { content, description, filename }) { + if (description) { + element.title = description; + } + element.onclick = () => { + this.downloadManager.openOrDownloadData(content, filename); + return false; + }; + } + render({ attachments, keepRenderedCapability = false }) { + if (this._attachments) { + this.reset(keepRenderedCapability); + } + this._attachments = attachments || null; + if (!attachments) { + this._dispatchEvent(0); + return; + } + const fragment = document.createDocumentFragment(); + let attachmentsCount = 0; + for (const name in attachments) { + const item = attachments[name]; + const div = document.createElement("div"); + div.className = "treeItem"; + const element = document.createElement("a"); + this._bindLink(element, item); + element.textContent = this._normalizeTextContent(item.filename); + div.append(element); + fragment.append(div); + attachmentsCount++; + } + this._finishRendering(fragment, attachmentsCount); + } + #appendAttachment(item) { + const renderedPromise = this._renderedCapability.promise; + renderedPromise.then(() => { + if (renderedPromise !== this._renderedCapability.promise) { + return; + } + const attachments = this._attachments || Object.create(null); + for (const name in attachments) { + if (item.filename === name) { + return; + } + } + attachments[item.filename] = item; + this.render({ + attachments, + keepRenderedCapability: true, + }); + }); + } +} // ./web/grab_to_pan.js + +const CSS_CLASS_GRAB = "grab-to-pan-grab"; +class GrabToPan { + #activateAC = null; + #mouseDownAC = null; + #scrollAC = null; + constructor({ element }) { + this.element = element; + this.document = element.ownerDocument; + const overlay = (this.overlay = document.createElement("div")); + overlay.className = "grab-to-pan-grabbing"; + } + activate() { + if (!this.#activateAC) { + this.#activateAC = new AbortController(); + this.element.addEventListener("mousedown", this.#onMouseDown.bind(this), { + capture: true, + signal: this.#activateAC.signal, + }); + this.element.classList.add(CSS_CLASS_GRAB); + } + } + deactivate() { + if (this.#activateAC) { + this.#activateAC.abort(); + this.#activateAC = null; + this.#endPan(); + this.element.classList.remove(CSS_CLASS_GRAB); + } + } + toggle() { + if (this.#activateAC) { + this.deactivate(); + } else { + this.activate(); + } + } + ignoreTarget(node) { + return node.matches( + "a[href], a[href] *, input, textarea, button, button *, select, option", + ); + } + #onMouseDown(event) { + if (event.button !== 0 || this.ignoreTarget(event.target)) { + return; + } + if (event.originalTarget) { + try { + event.originalTarget.tagName; + } catch { + return; + } + } + this.scrollLeftStart = this.element.scrollLeft; + this.scrollTopStart = this.element.scrollTop; + this.clientXStart = event.clientX; + this.clientYStart = event.clientY; + this.#mouseDownAC = new AbortController(); + const boundEndPan = this.#endPan.bind(this), + mouseOpts = { + capture: true, + signal: this.#mouseDownAC.signal, + }; + this.document.addEventListener( + "mousemove", + this.#onMouseMove.bind(this), + mouseOpts, + ); + this.document.addEventListener("mouseup", boundEndPan, mouseOpts); + this.#scrollAC = new AbortController(); + this.element.addEventListener("scroll", boundEndPan, { + capture: true, + signal: this.#scrollAC.signal, + }); + stopEvent(event); + const focusedElement = document.activeElement; + if (focusedElement && !focusedElement.contains(event.target)) { + focusedElement.blur(); + } + } + #onMouseMove(event) { + this.#scrollAC?.abort(); + this.#scrollAC = null; + if (!(event.buttons & 1)) { + this.#endPan(); + return; + } + const xDiff = event.clientX - this.clientXStart; + const yDiff = event.clientY - this.clientYStart; + this.element.scrollTo({ + top: this.scrollTopStart - yDiff, + left: this.scrollLeftStart - xDiff, + behavior: "instant", + }); + if (!this.overlay.parentNode) { + document.body.append(this.overlay); + } + } + #endPan() { + this.#mouseDownAC?.abort(); + this.#mouseDownAC = null; + this.#scrollAC?.abort(); + this.#scrollAC = null; + this.overlay.remove(); + } +} // ./web/pdf_cursor_tools.js + +class PDFCursorTools { + #active = CursorTool.SELECT; + #prevActive = null; + constructor({ container, eventBus, cursorToolOnLoad = CursorTool.SELECT }) { + this.container = container; + this.eventBus = eventBus; + this.#addEventListeners(); + Promise.resolve().then(() => { + this.switchTool(cursorToolOnLoad); + }); + } + get activeTool() { + return this.#active; + } + switchTool(tool) { + if (this.#prevActive !== null) { + return; + } + this.#switchTool(tool); + } + #switchTool(tool, disabled = false) { + if (tool === this.#active) { + if (this.#prevActive !== null) { + this.eventBus.dispatch("cursortoolchanged", { + source: this, + tool, + disabled, + }); + } + return; + } + const disableActiveTool = () => { + switch (this.#active) { + case CursorTool.SELECT: + break; + case CursorTool.HAND: + this._handTool.deactivate(); + break; + case CursorTool.ZOOM: + } + }; + switch (tool) { + case CursorTool.SELECT: + disableActiveTool(); + break; + case CursorTool.HAND: + disableActiveTool(); + this._handTool.activate(); + break; + case CursorTool.ZOOM: + default: + console.error(`switchTool: "${tool}" is an unsupported value.`); + return; + } + this.#active = tool; + this.eventBus.dispatch("cursortoolchanged", { + source: this, + tool, + disabled, + }); + } + #addEventListeners() { + this.eventBus._on("switchcursortool", (evt) => { + if (!evt.reset) { + this.switchTool(evt.tool); + } else if (this.#prevActive !== null) { + annotationEditorMode = AnnotationEditorType.NONE; + presentationModeState = PresentationModeState.NORMAL; + enableActive(); + } + }); + let annotationEditorMode = AnnotationEditorType.NONE, + presentationModeState = PresentationModeState.NORMAL; + const disableActive = () => { + this.#prevActive ??= this.#active; + this.#switchTool(CursorTool.SELECT, true); + }; + const enableActive = () => { + if ( + this.#prevActive !== null && + annotationEditorMode === AnnotationEditorType.NONE && + presentationModeState === PresentationModeState.NORMAL + ) { + this.#switchTool(this.#prevActive); + this.#prevActive = null; + } + }; + this.eventBus._on("annotationeditormodechanged", ({ mode }) => { + annotationEditorMode = mode; + if (mode === AnnotationEditorType.NONE) { + enableActive(); + } else { + disableActive(); + } + }); + this.eventBus._on("presentationmodechanged", ({ state }) => { + presentationModeState = state; + if (state === PresentationModeState.NORMAL) { + enableActive(); + } else if (state === PresentationModeState.FULLSCREEN) { + disableActive(); + } + }); + } + get _handTool() { + return shadow( + this, + "_handTool", + new GrabToPan({ + element: this.container, + }), + ); + } +} // ./web/pdf_document_properties.js + +const NON_METRIC_LOCALES = ["en-us", "en-lr", "my"]; +const US_PAGE_NAMES = { + "8.5x11": "pdfjs-document-properties-page-size-name-letter", + "8.5x14": "pdfjs-document-properties-page-size-name-legal", +}; +const METRIC_PAGE_NAMES = { + "297x420": "pdfjs-document-properties-page-size-name-a-three", + "210x297": "pdfjs-document-properties-page-size-name-a-four", +}; +function getPageName(size, isPortrait, pageNames) { + const width = isPortrait ? size.width : size.height; + const height = isPortrait ? size.height : size.width; + return pageNames[`${width}x${height}`]; +} +class PDFDocumentProperties { + #fieldData = null; + constructor( + { dialog, fields, closeButton }, + overlayManager, + eventBus, + l10n, + fileNameLookup, + ) { + this.dialog = dialog; + this.fields = fields; + this.overlayManager = overlayManager; + this.l10n = l10n; + this._fileNameLookup = fileNameLookup; + this.#reset(); + closeButton.addEventListener("click", this.close.bind(this)); + this.overlayManager.register(this.dialog); + eventBus._on("pagechanging", (evt) => { + this._currentPageNumber = evt.pageNumber; + }); + eventBus._on("rotationchanging", (evt) => { + this._pagesRotation = evt.pagesRotation; + }); + } + async open() { + await Promise.all([ + this.overlayManager.open(this.dialog), + this._dataAvailableCapability.promise, + ]); + const currentPageNumber = this._currentPageNumber; + const pagesRotation = this._pagesRotation; + if ( + this.#fieldData && + currentPageNumber === this.#fieldData._currentPageNumber && + pagesRotation === this.#fieldData._pagesRotation + ) { + this.#updateUI(); + return; + } + const [{ info, contentLength }, pdfPage] = await Promise.all([ + this.pdfDocument.getMetadata(), + this.pdfDocument.getPage(currentPageNumber), + ]); + const [ + fileName, + fileSize, + creationDate, + modificationDate, + pageSize, + isLinearized, + ] = await Promise.all([ + this._fileNameLookup(), + this.#parseFileSize(contentLength), + this.#parseDate(info.CreationDate), + this.#parseDate(info.ModDate), + this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation), + this.#parseLinearization(info.IsLinearized), + ]); + this.#fieldData = Object.freeze({ + fileName, + fileSize, + title: info.Title, + author: info.Author, + subject: info.Subject, + keywords: info.Keywords, + creationDate, + modificationDate, + creator: info.Creator, + producer: info.Producer, + version: info.PDFFormatVersion, + pageCount: this.pdfDocument.numPages, + pageSize, + linearized: isLinearized, + _currentPageNumber: currentPageNumber, + _pagesRotation: pagesRotation, + }); + this.#updateUI(); + const { length } = await this.pdfDocument.getDownloadInfo(); + if (contentLength === length) { + return; + } + const data = Object.assign(Object.create(null), this.#fieldData); + data.fileSize = await this.#parseFileSize(length); + this.#fieldData = Object.freeze(data); + this.#updateUI(); + } + async close() { + this.overlayManager.close(this.dialog); + } + setDocument(pdfDocument) { + if (this.pdfDocument) { + this.#reset(); + this.#updateUI(); + } + if (!pdfDocument) { + return; + } + this.pdfDocument = pdfDocument; + this._dataAvailableCapability.resolve(); + } + #reset() { + this.pdfDocument = null; + this.#fieldData = null; + this._dataAvailableCapability = Promise.withResolvers(); + this._currentPageNumber = 1; + this._pagesRotation = 0; + } + #updateUI() { + if (this.#fieldData && this.overlayManager.active !== this.dialog) { + return; + } + for (const id in this.fields) { + const content = this.#fieldData?.[id]; + this.fields[id].textContent = content || content === 0 ? content : "-"; + } + } + async #parseFileSize(b = 0) { + const kb = b / 1024, + mb = kb / 1024; + return kb + ? this.l10n.get( + mb >= 1 + ? "pdfjs-document-properties-size-mb" + : "pdfjs-document-properties-size-kb", + { + mb, + kb, + b, + }, + ) + : undefined; + } + async #parsePageSize(pageSizeInches, pagesRotation) { + if (!pageSizeInches) { + return undefined; + } + if (pagesRotation % 180 !== 0) { + pageSizeInches = { + width: pageSizeInches.height, + height: pageSizeInches.width, + }; + } + const isPortrait = isPortraitOrientation(pageSizeInches), + nonMetric = NON_METRIC_LOCALES.includes(this.l10n.getLanguage()); + let sizeInches = { + width: Math.round(pageSizeInches.width * 100) / 100, + height: Math.round(pageSizeInches.height * 100) / 100, + }; + let sizeMillimeters = { + width: Math.round(pageSizeInches.width * 25.4 * 10) / 10, + height: Math.round(pageSizeInches.height * 25.4 * 10) / 10, + }; + let nameId = + getPageName(sizeInches, isPortrait, US_PAGE_NAMES) || + getPageName(sizeMillimeters, isPortrait, METRIC_PAGE_NAMES); + if ( + !nameId && + !( + Number.isInteger(sizeMillimeters.width) && + Number.isInteger(sizeMillimeters.height) + ) + ) { + const exactMillimeters = { + width: pageSizeInches.width * 25.4, + height: pageSizeInches.height * 25.4, + }; + const intMillimeters = { + width: Math.round(sizeMillimeters.width), + height: Math.round(sizeMillimeters.height), + }; + if ( + Math.abs(exactMillimeters.width - intMillimeters.width) < 0.1 && + Math.abs(exactMillimeters.height - intMillimeters.height) < 0.1 + ) { + nameId = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES); + if (nameId) { + sizeInches = { + width: Math.round((intMillimeters.width / 25.4) * 100) / 100, + height: Math.round((intMillimeters.height / 25.4) * 100) / 100, + }; + sizeMillimeters = intMillimeters; + } + } + } + const [{ width, height }, unit, name, orientation] = await Promise.all([ + nonMetric ? sizeInches : sizeMillimeters, + this.l10n.get( + nonMetric + ? "pdfjs-document-properties-page-size-unit-inches" + : "pdfjs-document-properties-page-size-unit-millimeters", + ), + nameId && this.l10n.get(nameId), + this.l10n.get( + isPortrait + ? "pdfjs-document-properties-page-size-orientation-portrait" + : "pdfjs-document-properties-page-size-orientation-landscape", + ), + ]); + return this.l10n.get( + name + ? "pdfjs-document-properties-page-size-dimension-name-string" + : "pdfjs-document-properties-page-size-dimension-string", + { + width, + height, + unit, + name, + orientation, + }, + ); + } + async #parseDate(inputDate) { + const dateObj = PDFDateString.toDateObject(inputDate); + return dateObj + ? this.l10n.get("pdfjs-document-properties-date-time-string", { + dateObj: dateObj.valueOf(), + }) + : undefined; + } + #parseLinearization(isLinearized) { + return this.l10n.get( + isLinearized + ? "pdfjs-document-properties-linearized-yes" + : "pdfjs-document-properties-linearized-no", + ); + } +} // ./web/pdf_find_utils.js + +const CharacterType = { + SPACE: 0, + ALPHA_LETTER: 1, + PUNCT: 2, + HAN_LETTER: 3, + KATAKANA_LETTER: 4, + HIRAGANA_LETTER: 5, + HALFWIDTH_KATAKANA_LETTER: 6, + THAI_LETTER: 7, +}; +function isAlphabeticalScript(charCode) { + return charCode < 0x2e80; +} +function isAscii(charCode) { + return (charCode & 0xff80) === 0; +} +function isAsciiAlpha(charCode) { + return ( + (charCode >= 0x61 && charCode <= 0x7a) || + (charCode >= 0x41 && charCode <= 0x5a) + ); +} +function isAsciiDigit(charCode) { + return charCode >= 0x30 && charCode <= 0x39; +} +function isAsciiSpace(charCode) { + return ( + charCode === 0x20 || + charCode === 0x09 || + charCode === 0x0d || + charCode === 0x0a + ); +} +function isHan(charCode) { + return ( + (charCode >= 0x3400 && charCode <= 0x9fff) || + (charCode >= 0xf900 && charCode <= 0xfaff) + ); +} +function isKatakana(charCode) { + return charCode >= 0x30a0 && charCode <= 0x30ff; +} +function isHiragana(charCode) { + return charCode >= 0x3040 && charCode <= 0x309f; +} +function isHalfwidthKatakana(charCode) { + return charCode >= 0xff60 && charCode <= 0xff9f; +} +function isThai(charCode) { + return (charCode & 0xff80) === 0x0e00; +} +function getCharacterType(charCode) { + if (isAlphabeticalScript(charCode)) { + if (isAscii(charCode)) { + if (isAsciiSpace(charCode)) { + return CharacterType.SPACE; + } else if ( + isAsciiAlpha(charCode) || + isAsciiDigit(charCode) || + charCode === 0x5f + ) { + return CharacterType.ALPHA_LETTER; + } + return CharacterType.PUNCT; + } else if (isThai(charCode)) { + return CharacterType.THAI_LETTER; + } else if (charCode === 0xa0) { + return CharacterType.SPACE; + } + return CharacterType.ALPHA_LETTER; + } + if (isHan(charCode)) { + return CharacterType.HAN_LETTER; + } else if (isKatakana(charCode)) { + return CharacterType.KATAKANA_LETTER; + } else if (isHiragana(charCode)) { + return CharacterType.HIRAGANA_LETTER; + } else if (isHalfwidthKatakana(charCode)) { + return CharacterType.HALFWIDTH_KATAKANA_LETTER; + } + return CharacterType.ALPHA_LETTER; +} +let NormalizeWithNFKC; +function getNormalizeWithNFKC() { + NormalizeWithNFKC ||= ` ¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰꟲ-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`; + return NormalizeWithNFKC; +} // ./web/pdf_find_controller.js + +const FindState = { + FOUND: 0, + NOT_FOUND: 1, + WRAPPED: 2, + PENDING: 3, +}; +const FIND_TIMEOUT = 250; +const MATCH_SCROLL_OFFSET_TOP = -50; +const MATCH_SCROLL_OFFSET_LEFT = -400; +const CHARACTERS_TO_NORMALIZE = { + "\u2010": "-", + "\u2018": "'", + "\u2019": "'", + "\u201A": "'", + "\u201B": "'", + "\u201C": '"', + "\u201D": '"', + "\u201E": '"', + "\u201F": '"', + "\u00BC": "1/4", + "\u00BD": "1/2", + "\u00BE": "3/4", +}; +const DIACRITICS_EXCEPTION = new Set([ + 0x3099, 0x309a, 0x094d, 0x09cd, 0x0a4d, 0x0acd, 0x0b4d, 0x0bcd, 0x0c4d, + 0x0ccd, 0x0d3b, 0x0d3c, 0x0d4d, 0x0dca, 0x0e3a, 0x0eba, 0x0f84, 0x1039, + 0x103a, 0x1714, 0x1734, 0x17d2, 0x1a60, 0x1b44, 0x1baa, 0x1bab, 0x1bf2, + 0x1bf3, 0x2d7f, 0xa806, 0xa82c, 0xa8c4, 0xa953, 0xa9c0, 0xaaf6, 0xabed, + 0x0c56, 0x0f71, 0x0f72, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f80, 0x0f74, +]); +let DIACRITICS_EXCEPTION_STR; +const DIACRITICS_REG_EXP = /\p{M}+/gu; +const SPECIAL_CHARS_REG_EXP = + /([.*+?^${}()|[\]\\])|(\p{P})|(\s+)|(\p{M})|(\p{L})/gu; +const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u; +const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u; +const SYLLABLES_REG_EXP = /[\uAC00-\uD7AF\uFA6C\uFACF-\uFAD1\uFAD5-\uFAD7]+/g; +const SYLLABLES_LENGTHS = new Map(); +const FIRST_CHAR_SYLLABLES_REG_EXP = + "[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]"; +const NFKC_CHARS_TO_NORMALIZE = new Map(); +let noSyllablesRegExp = null; +let withSyllablesRegExp = null; +function normalize(text) { + const syllablePositions = []; + let m; + while ((m = SYLLABLES_REG_EXP.exec(text)) !== null) { + let { index } = m; + for (const char of m[0]) { + let len = SYLLABLES_LENGTHS.get(char); + if (!len) { + len = char.normalize("NFD").length; + SYLLABLES_LENGTHS.set(char, len); + } + syllablePositions.push([len, index++]); + } + } + let normalizationRegex; + if (syllablePositions.length === 0 && noSyllablesRegExp) { + normalizationRegex = noSyllablesRegExp; + } else if (syllablePositions.length > 0 && withSyllablesRegExp) { + normalizationRegex = withSyllablesRegExp; + } else { + const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); + const toNormalizeWithNFKC = getNormalizeWithNFKC(); + const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])"; + const HKDiacritics = "(?:\u3099|\u309A)"; + const CompoundWord = "\\p{Ll}-\\n\\p{Lu}"; + const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(${CompoundWord})|(\\S-\\n)|(${CJK}\\n)|(\\n)`; + if (syllablePositions.length === 0) { + normalizationRegex = noSyllablesRegExp = new RegExp( + regexp + "|(\\u0000)", + "gum", + ); + } else { + normalizationRegex = withSyllablesRegExp = new RegExp( + regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`, + "gum", + ); + } + } + const rawDiacriticsPositions = []; + while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) { + rawDiacriticsPositions.push([m[0].length, m.index]); + } + let normalized = text.normalize("NFD"); + const positions = [0, 0]; + let rawDiacriticsIndex = 0; + let syllableIndex = 0; + let shift = 0; + let shiftOrigin = 0; + let eol = 0; + let hasDiacritics = false; + normalized = normalized.replace( + normalizationRegex, + (match, p1, p2, p3, p4, p5, p6, p7, p8, p9, i) => { + i -= shiftOrigin; + if (p1) { + const replacement = CHARACTERS_TO_NORMALIZE[p1]; + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p2) { + let replacement = NFKC_CHARS_TO_NORMALIZE.get(p2); + if (!replacement) { + replacement = p2.normalize("NFKC"); + NFKC_CHARS_TO_NORMALIZE.set(p2, replacement); + } + const jj = replacement.length; + for (let j = 1; j < jj; j++) { + positions.push(i - shift + j, shift - j); + } + shift -= jj - 1; + return replacement; + } + if (p3) { + hasDiacritics = true; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + ++rawDiacriticsIndex; + } else { + positions.push(i - 1 - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + } + positions.push(i - shift + 1, shift); + shiftOrigin += 1; + eol += 1; + return p3.charAt(0); + } + if (p4) { + const hasTrailingDashEOL = p4.endsWith("\n"); + const len = hasTrailingDashEOL ? p4.length - 2 : p4.length; + hasDiacritics = true; + let jj = len; + if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) { + jj -= rawDiacriticsPositions[rawDiacriticsIndex][0]; + ++rawDiacriticsIndex; + } + for (let j = 1; j <= jj; j++) { + positions.push(i - 1 - shift + j, shift - j); + } + shift -= jj; + shiftOrigin += jj; + if (hasTrailingDashEOL) { + i += len - 1; + positions.push(i - shift + 1, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p4.slice(0, len); + } + return p4; + } + if (p5) { + shiftOrigin += 1; + eol += 1; + return p5.replace("\n", ""); + } + if (p6) { + const len = p6.length - 2; + positions.push(i - shift + len, 1 + shift); + shift += 1; + shiftOrigin += 1; + eol += 1; + return p6.slice(0, -2); + } + if (p7) { + const len = p7.length - 1; + positions.push(i - shift + len, shift); + shiftOrigin += 1; + eol += 1; + return p7.slice(0, -1); + } + if (p8) { + positions.push(i - shift + 1, shift - 1); + shift -= 1; + shiftOrigin += 1; + eol += 1; + return " "; + } + if (i + eol === syllablePositions[syllableIndex]?.[1]) { + const newCharLen = syllablePositions[syllableIndex][0] - 1; + ++syllableIndex; + for (let j = 1; j <= newCharLen; j++) { + positions.push(i - (shift - j), shift - j); + } + shift -= newCharLen; + shiftOrigin += newCharLen; + } + return p9; + }, + ); + positions.push(normalized.length, shift); + const starts = new Uint32Array(positions.length >> 1); + const shifts = new Int32Array(positions.length >> 1); + for (let i = 0, ii = positions.length; i < ii; i += 2) { + starts[i >> 1] = positions[i]; + shifts[i >> 1] = positions[i + 1]; + } + return [normalized, [starts, shifts], hasDiacritics]; +} +function getOriginalIndex(diffs, pos, len) { + if (!diffs) { + return [pos, len]; + } + const [starts, shifts] = diffs; + const start = pos; + const end = pos + len - 1; + let i = binarySearchFirstItem(starts, (x) => x >= start); + if (starts[i] > start) { + --i; + } + let j = binarySearchFirstItem(starts, (x) => x >= end, i); + if (starts[j] > end) { + --j; + } + const oldStart = start + shifts[i]; + const oldEnd = end + shifts[j]; + const oldLen = oldEnd + 1 - oldStart; + return [oldStart, oldLen]; +} +class PDFFindController { + #state = null; + #updateMatchesCountOnProgress = true; + #visitedPagesCount = 0; + constructor({ linkService, eventBus, updateMatchesCountOnProgress = true }) { + this._linkService = linkService; + this._eventBus = eventBus; + this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress; + this.onIsPageVisible = null; + this.#reset(); + eventBus._on("find", this.#onFind.bind(this)); + eventBus._on("findbarclose", this.#onFindBarClose.bind(this)); + } + get highlightMatches() { + return this._highlightMatches; + } + get pageMatches() { + return this._pageMatches; + } + get pageMatchesLength() { + return this._pageMatchesLength; + } + get selected() { + return this._selected; + } + get state() { + return this.#state; + } + setDocument(pdfDocument) { + if (this._pdfDocument) { + this.#reset(); + } + if (!pdfDocument) { + return; + } + this._pdfDocument = pdfDocument; + this._firstPageCapability.resolve(); + } + #onFind(state) { + if (!state) { + return; + } + const pdfDocument = this._pdfDocument; + const { type } = state; + if (this.#state === null || this.#shouldDirtyMatch(state)) { + this._dirtyMatch = true; + } + this.#state = state; + if (type !== "highlightallchange") { + this.#updateUIState(FindState.PENDING); + } + this._firstPageCapability.promise.then(() => { + if ( + !this._pdfDocument || + (pdfDocument && this._pdfDocument !== pdfDocument) + ) { + return; + } + this.#extractText(); + const findbarClosed = !this._highlightMatches; + const pendingTimeout = !!this._findTimeout; + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (!type) { + this._findTimeout = setTimeout(() => { + this.#nextMatch(); + this._findTimeout = null; + }, FIND_TIMEOUT); + } else if (this._dirtyMatch) { + this.#nextMatch(); + } else if (type === "again") { + this.#nextMatch(); + if (findbarClosed && this.#state.highlightAll) { + this.#updateAllPages(); + } + } else if (type === "highlightallchange") { + if (pendingTimeout) { + this.#nextMatch(); + } else { + this._highlightMatches = true; + } + this.#updateAllPages(); + } else { + this.#nextMatch(); + } + }); + } + scrollMatchIntoView({ + element = null, + selectedLeft = 0, + pageIndex = -1, + matchIndex = -1, + }) { + if (!this._scrollMatches || !element) { + return; + } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { + return; + } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) { + return; + } + this._scrollMatches = false; + const spot = { + top: MATCH_SCROLL_OFFSET_TOP, + left: selectedLeft + MATCH_SCROLL_OFFSET_LEFT, + }; + scrollIntoView(element, spot, true); + } + #reset() { + this._highlightMatches = false; + this._scrollMatches = false; + this._pdfDocument = null; + this._pageMatches = []; + this._pageMatchesLength = []; + this.#visitedPagesCount = 0; + this.#state = null; + this._selected = { + pageIdx: -1, + matchIdx: -1, + }; + this._offset = { + pageIdx: null, + matchIdx: null, + wrapped: false, + }; + this._extractTextPromises = []; + this._pageContents = []; + this._pageDiffs = []; + this._hasDiacritics = []; + this._matchesCountTotal = 0; + this._pagesToSearch = null; + this._pendingFindMatches = new Set(); + this._resumePageIdx = null; + this._dirtyMatch = false; + clearTimeout(this._findTimeout); + this._findTimeout = null; + this._firstPageCapability = Promise.withResolvers(); + } + get #query() { + const { query } = this.#state; + if (typeof query === "string") { + if (query !== this._rawQuery) { + this._rawQuery = query; + [this._normalizedQuery] = normalize(query); + } + return this._normalizedQuery; + } + return (query || []).filter((q) => !!q).map((q) => normalize(q)[0]); + } + #shouldDirtyMatch(state) { + const newQuery = state.query, + prevQuery = this.#state.query; + const newType = typeof newQuery, + prevType = typeof prevQuery; + if (newType !== prevType) { + return true; + } + if (newType === "string") { + if (newQuery !== prevQuery) { + return true; + } + } else if (JSON.stringify(newQuery) !== JSON.stringify(prevQuery)) { + return true; + } + switch (state.type) { + case "again": + const pageNumber = this._selected.pageIdx + 1; + const linkService = this._linkService; + return ( + pageNumber >= 1 && + pageNumber <= linkService.pagesCount && + pageNumber !== linkService.page && + !(this.onIsPageVisible?.(pageNumber) ?? true) + ); + case "highlightallchange": + return false; + } + return true; + } + #isEntireWord(content, startIdx, length) { + let match = content + .slice(0, startIdx) + .match(NOT_DIACRITIC_FROM_END_REG_EXP); + if (match) { + const first = content.charCodeAt(startIdx); + const limit = match[1].charCodeAt(0); + if (getCharacterType(first) === getCharacterType(limit)) { + return false; + } + } + match = content + .slice(startIdx + length) + .match(NOT_DIACRITIC_FROM_START_REG_EXP); + if (match) { + const last = content.charCodeAt(startIdx + length - 1); + const limit = match[1].charCodeAt(0); + if (getCharacterType(last) === getCharacterType(limit)) { + return false; + } + } + return true; + } + #convertToRegExpString(query, hasDiacritics) { + const { matchDiacritics } = this.#state; + let isUnicode = false; + query = query.replaceAll( + SPECIAL_CHARS_REG_EXP, + (match, p1, p2, p3, p4, p5) => { + if (p1) { + return `[ ]*\\${p1}[ ]*`; + } + if (p2) { + return `[ ]*${p2}[ ]*`; + } + if (p3) { + return "[ ]+"; + } + if (matchDiacritics) { + return p4 || p5; + } + if (p4) { + return DIACRITICS_EXCEPTION.has(p4.charCodeAt(0)) ? p4 : ""; + } + if (hasDiacritics) { + isUnicode = true; + return `${p5}\\p{M}*`; + } + return p5; + }, + ); + const trailingSpaces = "[ ]*"; + if (query.endsWith(trailingSpaces)) { + query = query.slice(0, query.length - trailingSpaces.length); + } + if (matchDiacritics) { + if (hasDiacritics) { + DIACRITICS_EXCEPTION_STR ||= String.fromCharCode( + ...DIACRITICS_EXCEPTION, + ); + isUnicode = true; + query = `${query}(?=[${DIACRITICS_EXCEPTION_STR}]|[^\\p{M}]|$)`; + } + } + return [isUnicode, query]; + } + #calculateMatch(pageIndex) { + const query = this.#query; + if (query.length === 0) { + return; + } + const pageContent = this._pageContents[pageIndex]; + const matcherResult = this.match(query, pageContent, pageIndex); + const matches = (this._pageMatches[pageIndex] = []); + const matchesLength = (this._pageMatchesLength[pageIndex] = []); + const diffs = this._pageDiffs[pageIndex]; + matcherResult?.forEach(({ index, length }) => { + const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); + if (matchLen) { + matches.push(matchPos); + matchesLength.push(matchLen); + } + }); + if (this.#state.highlightAll) { + this.#updatePage(pageIndex); + } + if (this._resumePageIdx === pageIndex) { + this._resumePageIdx = null; + this.#nextPageMatch(); + } + const pageMatchesCount = matches.length; + this._matchesCountTotal += pageMatchesCount; + if (this.#updateMatchesCountOnProgress) { + if (pageMatchesCount > 0) { + this.#updateUIResultsCount(); + } + } else if (++this.#visitedPagesCount === this._linkService.pagesCount) { + this.#updateUIResultsCount(); + } + } + match(query, pageContent, pageIndex) { + const hasDiacritics = this._hasDiacritics[pageIndex]; + let isUnicode = false; + if (typeof query === "string") { + [isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics); + } else { + query = query + .sort() + .reverse() + .map((q) => { + const [isUnicodePart, queryPart] = this.#convertToRegExpString( + q, + hasDiacritics, + ); + isUnicode ||= isUnicodePart; + return `(${queryPart})`; + }) + .join("|"); + } + if (!query) { + return undefined; + } + const { caseSensitive, entireWord } = this.#state; + const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`; + query = new RegExp(query, flags); + const matches = []; + let match; + while ((match = query.exec(pageContent)) !== null) { + if ( + entireWord && + !this.#isEntireWord(pageContent, match.index, match[0].length) + ) { + continue; + } + matches.push({ + index: match.index, + length: match[0].length, + }); + } + return matches; + } + #extractText() { + if (this._extractTextPromises.length > 0) { + return; + } + let deferred = Promise.resolve(); + const textOptions = { + disableNormalization: true, + }; + for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { + const { promise, resolve } = Promise.withResolvers(); + this._extractTextPromises[i] = promise; + deferred = deferred.then(() => { + return this._pdfDocument + .getPage(i + 1) + .then((pdfPage) => pdfPage.getTextContent(textOptions)) + .then( + (textContent) => { + const strBuf = []; + for (const textItem of textContent.items) { + strBuf.push(textItem.str); + if (textItem.hasEOL) { + strBuf.push("\n"); + } + } + [ + this._pageContents[i], + this._pageDiffs[i], + this._hasDiacritics[i], + ] = normalize(strBuf.join("")); + resolve(); + }, + (reason) => { + console.error( + `Unable to get text content for page ${i + 1}`, + reason, + ); + this._pageContents[i] = ""; + this._pageDiffs[i] = null; + this._hasDiacritics[i] = false; + resolve(); + }, + ); + }); + } + } + #updatePage(index) { + if (this._scrollMatches && this._selected.pageIdx === index) { + this._linkService.page = index + 1; + } + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: index, + }); + } + #updateAllPages() { + this._eventBus.dispatch("updatetextlayermatches", { + source: this, + pageIndex: -1, + }); + } + #nextMatch() { + const previous = this.#state.findPrevious; + const currentPageIndex = this._linkService.page - 1; + const numPages = this._linkService.pagesCount; + this._highlightMatches = true; + if (this._dirtyMatch) { + this._dirtyMatch = false; + this._selected.pageIdx = this._selected.matchIdx = -1; + this._offset.pageIdx = currentPageIndex; + this._offset.matchIdx = null; + this._offset.wrapped = false; + this._resumePageIdx = null; + this._pageMatches.length = 0; + this._pageMatchesLength.length = 0; + this.#visitedPagesCount = 0; + this._matchesCountTotal = 0; + this.#updateAllPages(); + for (let i = 0; i < numPages; i++) { + if (this._pendingFindMatches.has(i)) { + continue; + } + this._pendingFindMatches.add(i); + this._extractTextPromises[i].then(() => { + this._pendingFindMatches.delete(i); + this.#calculateMatch(i); + }); + } + } + const query = this.#query; + if (query.length === 0) { + this.#updateUIState(FindState.FOUND); + return; + } + if (this._resumePageIdx) { + return; + } + const offset = this._offset; + this._pagesToSearch = numPages; + if (offset.matchIdx !== null) { + const numPageMatches = this._pageMatches[offset.pageIdx].length; + if ( + (!previous && offset.matchIdx + 1 < numPageMatches) || + (previous && offset.matchIdx > 0) + ) { + offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; + this.#updateMatch(true); + return; + } + this.#advanceOffsetPage(previous); + } + this.#nextPageMatch(); + } + #matchesReady(matches) { + const offset = this._offset; + const numMatches = matches.length; + const previous = this.#state.findPrevious; + if (numMatches) { + offset.matchIdx = previous ? numMatches - 1 : 0; + this.#updateMatch(true); + return true; + } + this.#advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (this._pagesToSearch < 0) { + this.#updateMatch(false); + return true; + } + } + return false; + } + #nextPageMatch() { + if (this._resumePageIdx !== null) { + console.error("There can only be one pending page."); + } + let matches = null; + do { + const pageIdx = this._offset.pageIdx; + matches = this._pageMatches[pageIdx]; + if (!matches) { + this._resumePageIdx = pageIdx; + break; + } + } while (!this.#matchesReady(matches)); + } + #advanceOffsetPage(previous) { + const offset = this._offset; + const numPages = this._linkService.pagesCount; + offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; + offset.matchIdx = null; + this._pagesToSearch--; + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = previous ? numPages - 1 : 0; + offset.wrapped = true; + } + } + #updateMatch(found = false) { + let state = FindState.NOT_FOUND; + const wrapped = this._offset.wrapped; + this._offset.wrapped = false; + if (found) { + const previousPage = this._selected.pageIdx; + this._selected.pageIdx = this._offset.pageIdx; + this._selected.matchIdx = this._offset.matchIdx; + state = wrapped ? FindState.WRAPPED : FindState.FOUND; + if (previousPage !== -1 && previousPage !== this._selected.pageIdx) { + this.#updatePage(previousPage); + } + } + this.#updateUIState(state, this.#state.findPrevious); + if (this._selected.pageIdx !== -1) { + this._scrollMatches = true; + this.#updatePage(this._selected.pageIdx); + } + } + #onFindBarClose(evt) { + const pdfDocument = this._pdfDocument; + this._firstPageCapability.promise.then(() => { + if ( + !this._pdfDocument || + (pdfDocument && this._pdfDocument !== pdfDocument) + ) { + return; + } + if (this._findTimeout) { + clearTimeout(this._findTimeout); + this._findTimeout = null; + } + if (this._resumePageIdx) { + this._resumePageIdx = null; + this._dirtyMatch = true; + } + this.#updateUIState(FindState.FOUND); + this._highlightMatches = false; + this.#updateAllPages(); + }); + } + #requestMatchesCount() { + const { pageIdx, matchIdx } = this._selected; + let current = 0, + total = this._matchesCountTotal; + if (matchIdx !== -1) { + for (let i = 0; i < pageIdx; i++) { + current += this._pageMatches[i]?.length || 0; + } + current += matchIdx + 1; + } + if (current < 1 || current > total) { + current = total = 0; + } + return { + current, + total, + }; + } + #updateUIResultsCount() { + this._eventBus.dispatch("updatefindmatchescount", { + source: this, + matchesCount: this.#requestMatchesCount(), + }); + } + #updateUIState(state, previous = false) { + if ( + !this.#updateMatchesCountOnProgress && + (this.#visitedPagesCount !== this._linkService.pagesCount || + state === FindState.PENDING) + ) { + return; + } + this._eventBus.dispatch("updatefindcontrolstate", { + source: this, + state, + previous, + entireWord: this.#state?.entireWord ?? null, + matchesCount: this.#requestMatchesCount(), + rawQuery: this.#state?.query ?? null, + }); + } +} // ./web/pdf_find_bar.js + +const MATCHES_COUNT_LIMIT = 1000; +class PDFFindBar { + #mainContainer; + #resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this)); + constructor(options, mainContainer, eventBus) { + this.opened = false; + this.bar = options.bar; + this.toggleButton = options.toggleButton; + this.findField = options.findField; + this.highlightAll = options.highlightAllCheckbox; + this.caseSensitive = options.caseSensitiveCheckbox; + this.matchDiacritics = options.matchDiacriticsCheckbox; + this.entireWord = options.entireWordCheckbox; + this.findMsg = options.findMsg; + this.findResultsCount = options.findResultsCount; + this.findPreviousButton = options.findPreviousButton; + this.findNextButton = options.findNextButton; + this.eventBus = eventBus; + this.#mainContainer = mainContainer; + const checkedInputs = new Map([ + [this.highlightAll, "highlightallchange"], + [this.caseSensitive, "casesensitivitychange"], + [this.entireWord, "entirewordchange"], + [this.matchDiacritics, "diacriticmatchingchange"], + ]); + this.toggleButton.addEventListener("click", () => { + this.toggle(); + }); + this.findField.addEventListener("input", () => { + this.dispatchEvent(""); + }); + this.bar.addEventListener("keydown", ({ keyCode, shiftKey, target }) => { + switch (keyCode) { + case 13: + if (target === this.findField) { + this.dispatchEvent("again", shiftKey); + } else if (checkedInputs.has(target)) { + target.checked = !target.checked; + this.dispatchEvent(checkedInputs.get(target)); + } + break; + case 27: + this.close(); + break; + } + }); + this.findPreviousButton.addEventListener("click", () => { + this.dispatchEvent("again", true); + }); + this.findNextButton.addEventListener("click", () => { + this.dispatchEvent("again", false); + }); + for (const [elem, evtName] of checkedInputs) { + elem.addEventListener("click", () => { + this.dispatchEvent(evtName); + }); + } + } + reset() { + this.updateUIState(); + } + dispatchEvent(type, findPrev = false) { + this.eventBus.dispatch("find", { + source: this, + type, + query: this.findField.value, + caseSensitive: this.caseSensitive.checked, + entireWord: this.entireWord.checked, + highlightAll: this.highlightAll.checked, + findPrevious: findPrev, + matchDiacritics: this.matchDiacritics.checked, + }); + } + updateUIState(state, previous, matchesCount) { + const { findField, findMsg } = this; + let findMsgId = "", + status = ""; + switch (state) { + case FindState.FOUND: + break; + case FindState.PENDING: + status = "pending"; + break; + case FindState.NOT_FOUND: + findMsgId = "pdfjs-find-not-found"; + status = "notFound"; + break; + case FindState.WRAPPED: + findMsgId = previous + ? "pdfjs-find-reached-top" + : "pdfjs-find-reached-bottom"; + break; + } + findField.setAttribute("data-status", status); + findField.setAttribute("aria-invalid", state === FindState.NOT_FOUND); + findMsg.setAttribute("data-status", status); + if (findMsgId) { + findMsg.setAttribute("data-l10n-id", findMsgId); + } else { + findMsg.removeAttribute("data-l10n-id"); + findMsg.textContent = ""; + } + this.updateResultsCount(matchesCount); + } + updateResultsCount({ current = 0, total = 0 } = {}) { + const { findResultsCount } = this; + if (total > 0) { + const limit = MATCHES_COUNT_LIMIT; + findResultsCount.setAttribute( + "data-l10n-id", + total > limit + ? "pdfjs-find-match-count-limit" + : "pdfjs-find-match-count", + ); + findResultsCount.setAttribute( + "data-l10n-args", + JSON.stringify({ + limit, + current, + total, + }), + ); + } else { + findResultsCount.removeAttribute("data-l10n-id"); + findResultsCount.textContent = ""; + } + } + open() { + if (!this.opened) { + this.#resizeObserver.observe(this.#mainContainer); + this.#resizeObserver.observe(this.bar); + this.opened = true; + toggleExpandedBtn(this.toggleButton, true, this.bar); + } + this.findField.select(); + this.findField.focus(); + } + close() { + if (!this.opened) { + return; + } + this.#resizeObserver.disconnect(); + this.opened = false; + toggleExpandedBtn(this.toggleButton, false, this.bar); + this.eventBus.dispatch("findbarclose", { + source: this, + }); + } + toggle() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } + #resizeObserverCallback() { + const { bar } = this; + bar.classList.remove("wrapContainers"); + const findbarHeight = bar.clientHeight; + const inputContainerHeight = bar.firstElementChild.clientHeight; + if (findbarHeight > inputContainerHeight) { + bar.classList.add("wrapContainers"); + } + } +} // ./web/pdf_history.js + +const HASH_CHANGE_TIMEOUT = 1000; +const POSITION_UPDATED_THRESHOLD = 50; +const UPDATE_VIEWAREA_TIMEOUT = 1000; +function getCurrentHash() { + return document.location.hash; +} +class PDFHistory { + #eventAbortController = null; + constructor({ linkService, eventBus }) { + this.linkService = linkService; + this.eventBus = eventBus; + this._initialized = false; + this._fingerprint = ""; + this.reset(); + this.eventBus._on("pagesinit", () => { + this._isPagesLoaded = false; + this.eventBus._on( + "pagesloaded", + (evt) => { + this._isPagesLoaded = !!evt.pagesCount; + }, + { + once: true, + }, + ); + }); + } + initialize({ fingerprint, resetHistory = false, updateUrl = false }) { + if (!fingerprint || typeof fingerprint !== "string") { + console.error( + 'PDFHistory.initialize: The "fingerprint" must be a non-empty string.', + ); + return; + } + if (this._initialized) { + this.reset(); + } + const reInitialized = + this._fingerprint !== "" && this._fingerprint !== fingerprint; + this._fingerprint = fingerprint; + this._updateUrl = updateUrl === true; + this._initialized = true; + this.#bindEvents(); + const state = window.history.state; + this._popStateInProgress = false; + this._blockHashChange = 0; + this._currentHash = getCurrentHash(); + this._numPositionUpdates = 0; + this._uid = this._maxUid = 0; + this._destination = null; + this._position = null; + if (!this.#isValidState(state, true) || resetHistory) { + const { hash, page, rotation } = this.#parseCurrentHash(true); + if (!hash || reInitialized || resetHistory) { + this.#pushOrReplaceState(null, true); + return; + } + this.#pushOrReplaceState( + { + hash, + page, + rotation, + }, + true, + ); + return; + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (destination.rotation !== undefined) { + this._initialRotation = destination.rotation; + } + if (destination.dest) { + this._initialBookmark = JSON.stringify(destination.dest); + this._destination.page = null; + } else if (destination.hash) { + this._initialBookmark = destination.hash; + } else if (destination.page) { + this._initialBookmark = `page=${destination.page}`; + } + } + reset() { + if (this._initialized) { + this.#pageHide(); + this._initialized = false; + this.#unbindEvents(); + } + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._initialBookmark = null; + this._initialRotation = null; + } + push({ namedDest = null, explicitDest, pageNumber }) { + if (!this._initialized) { + return; + } + if (namedDest && typeof namedDest !== "string") { + console.error( + "PDFHistory.push: " + + `"${namedDest}" is not a valid namedDest parameter.`, + ); + return; + } else if (!Array.isArray(explicitDest)) { + console.error( + "PDFHistory.push: " + + `"${explicitDest}" is not a valid explicitDest parameter.`, + ); + return; + } else if (!this.#isValidPage(pageNumber)) { + if (pageNumber !== null || this._destination) { + console.error( + "PDFHistory.push: " + + `"${pageNumber}" is not a valid pageNumber parameter.`, + ); + return; + } + } + const hash = namedDest || JSON.stringify(explicitDest); + if (!hash) { + return; + } + let forceReplace = false; + if ( + this._destination && + (isDestHashesEqual(this._destination.hash, hash) || + isDestArraysEqual(this._destination.dest, explicitDest)) + ) { + if (this._destination.page) { + return; + } + forceReplace = true; + } + if (this._popStateInProgress && !forceReplace) { + return; + } + this.#pushOrReplaceState( + { + dest: explicitDest, + hash, + page: pageNumber, + rotation: this.linkService.rotation, + }, + forceReplace, + ); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushPage(pageNumber) { + if (!this._initialized) { + return; + } + if (!this.#isValidPage(pageNumber)) { + console.error( + `PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`, + ); + return; + } + if (this._destination?.page === pageNumber) { + return; + } + if (this._popStateInProgress) { + return; + } + this.#pushOrReplaceState({ + dest: null, + hash: `page=${pageNumber}`, + page: pageNumber, + rotation: this.linkService.rotation, + }); + if (!this._popStateInProgress) { + this._popStateInProgress = true; + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + } + pushCurrentPosition() { + if (!this._initialized || this._popStateInProgress) { + return; + } + this.#tryPushCurrentPosition(); + } + back() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid > 0) { + window.history.back(); + } + } + forward() { + if (!this._initialized || this._popStateInProgress) { + return; + } + const state = window.history.state; + if (this.#isValidState(state) && state.uid < this._maxUid) { + window.history.forward(); + } + } + get popStateInProgress() { + return ( + this._initialized && + (this._popStateInProgress || this._blockHashChange > 0) + ); + } + get initialBookmark() { + return this._initialized ? this._initialBookmark : null; + } + get initialRotation() { + return this._initialized ? this._initialRotation : null; + } + #pushOrReplaceState(destination, forceReplace = false) { + const shouldReplace = forceReplace || !this._destination; + const newState = { + fingerprint: this._fingerprint, + uid: shouldReplace ? this._uid : this._uid + 1, + destination, + }; + this.#updateInternalState(destination, newState.uid); + let newUrl; + if (this._updateUrl && destination?.hash) { + const baseUrl = document.location.href.split("#", 1)[0]; + if (!baseUrl.startsWith("file://")) { + newUrl = `${baseUrl}#${destination.hash}`; + } + } + if (shouldReplace) { + window.history.replaceState(newState, "", newUrl); + } else { + window.history.pushState(newState, "", newUrl); + } + } + #tryPushCurrentPosition(temporary = false) { + if (!this._position) { + return; + } + let position = this._position; + if (temporary) { + position = Object.assign(Object.create(null), this._position); + position.temporary = true; + } + if (!this._destination) { + this.#pushOrReplaceState(position); + return; + } + if (this._destination.temporary) { + this.#pushOrReplaceState(position, true); + return; + } + if (this._destination.hash === position.hash) { + return; + } + if ( + !this._destination.page && + (POSITION_UPDATED_THRESHOLD <= 0 || + this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD) + ) { + return; + } + let forceReplace = false; + if ( + this._destination.page >= position.first && + this._destination.page <= position.page + ) { + if (this._destination.dest !== undefined || !this._destination.first) { + return; + } + forceReplace = true; + } + this.#pushOrReplaceState(position, forceReplace); + } + #isValidPage(val) { + return ( + Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount + ); + } + #isValidState(state, checkReload = false) { + if (!state) { + return false; + } + if (state.fingerprint !== this._fingerprint) { + if (checkReload) { + if ( + typeof state.fingerprint !== "string" || + state.fingerprint.length !== this._fingerprint.length + ) { + return false; + } + const [perfEntry] = performance.getEntriesByType("navigation"); + if (perfEntry?.type !== "reload") { + return false; + } + } else { + return false; + } + } + if (!Number.isInteger(state.uid) || state.uid < 0) { + return false; + } + if (state.destination === null || typeof state.destination !== "object") { + return false; + } + return true; + } + #updateInternalState(destination, uid, removeTemporary = false) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + if (removeTemporary && destination?.temporary) { + delete destination.temporary; + } + this._destination = destination; + this._uid = uid; + this._maxUid = Math.max(this._maxUid, uid); + this._numPositionUpdates = 0; + } + #parseCurrentHash(checkNameddest = false) { + const hash = unescape(getCurrentHash()).substring(1); + const params = parseQueryString(hash); + const nameddest = params.get("nameddest") || ""; + let page = params.get("page") | 0; + if (!this.#isValidPage(page) || (checkNameddest && nameddest.length > 0)) { + page = null; + } + return { + hash, + page, + rotation: this.linkService.rotation, + }; + } + #updateViewarea({ location }) { + if (this._updateViewareaTimeout) { + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } + this._position = { + hash: location.pdfOpenParams.substring(1), + page: this.linkService.page, + first: location.pageNumber, + rotation: location.rotation, + }; + if (this._popStateInProgress) { + return; + } + if ( + POSITION_UPDATED_THRESHOLD > 0 && + this._isPagesLoaded && + this._destination && + !this._destination.page + ) { + this._numPositionUpdates++; + } + if (UPDATE_VIEWAREA_TIMEOUT > 0) { + this._updateViewareaTimeout = setTimeout(() => { + if (!this._popStateInProgress) { + this.#tryPushCurrentPosition(true); + } + this._updateViewareaTimeout = null; + }, UPDATE_VIEWAREA_TIMEOUT); + } + } + #popState({ state }) { + const newHash = getCurrentHash(), + hashChanged = this._currentHash !== newHash; + this._currentHash = newHash; + if (!state) { + this._uid++; + const { hash, page, rotation } = this.#parseCurrentHash(); + this.#pushOrReplaceState( + { + hash, + page, + rotation, + }, + true, + ); + return; + } + if (!this.#isValidState(state)) { + return; + } + this._popStateInProgress = true; + if (hashChanged) { + this._blockHashChange++; + waitOnEventOrTimeout({ + target: window, + name: "hashchange", + delay: HASH_CHANGE_TIMEOUT, + }).then(() => { + this._blockHashChange--; + }); + } + const destination = state.destination; + this.#updateInternalState(destination, state.uid, true); + if (isValidRotation(destination.rotation)) { + this.linkService.rotation = destination.rotation; + } + if (destination.dest) { + this.linkService.goToDestination(destination.dest); + } else if (destination.hash) { + this.linkService.setHash(destination.hash); + } else if (destination.page) { + this.linkService.page = destination.page; + } + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } + #pageHide() { + if (!this._destination || this._destination.temporary) { + this.#tryPushCurrentPosition(); + } + } + #bindEvents() { + if (this.#eventAbortController) { + return; + } + this.#eventAbortController = new AbortController(); + const { signal } = this.#eventAbortController; + this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), { + signal, + }); + window.addEventListener("popstate", this.#popState.bind(this), { + signal, + }); + window.addEventListener("pagehide", this.#pageHide.bind(this), { + signal, + }); + } + #unbindEvents() { + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } +} +function isDestHashesEqual(destHash, pushHash) { + if (typeof destHash !== "string" || typeof pushHash !== "string") { + return false; + } + if (destHash === pushHash) { + return true; + } + const nameddest = parseQueryString(destHash).get("nameddest"); + if (nameddest === pushHash) { + return true; + } + return false; +} +function isDestArraysEqual(firstDest, secondDest) { + function isEntryEqual(first, second) { + if (typeof first !== typeof second) { + return false; + } + if (Array.isArray(first) || Array.isArray(second)) { + return false; + } + if (first !== null && typeof first === "object" && second !== null) { + if (Object.keys(first).length !== Object.keys(second).length) { + return false; + } + for (const key in first) { + if (!isEntryEqual(first[key], second[key])) { + return false; + } + } + return true; + } + return first === second || (Number.isNaN(first) && Number.isNaN(second)); + } + if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) { + return false; + } + if (firstDest.length !== secondDest.length) { + return false; + } + for (let i = 0, ii = firstDest.length; i < ii; i++) { + if (!isEntryEqual(firstDest[i], secondDest[i])) { + return false; + } + } + return true; +} // ./web/pdf_layer_viewer.js + +class PDFLayerViewer extends BaseTreeViewer { + constructor(options) { + super(options); + this.eventBus._on("optionalcontentconfigchanged", (evt) => { + this.#updateLayers(evt.promise); + }); + this.eventBus._on("resetlayers", () => { + this.#updateLayers(); + }); + this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this)); + } + reset() { + super.reset(); + this._optionalContentConfig = null; + this._optionalContentVisibility?.clear(); + this._optionalContentVisibility = null; + } + _dispatchEvent(layersCount) { + this.eventBus.dispatch("layersloaded", { + source: this, + layersCount, + }); + } + _bindLink(element, { groupId, input }) { + const setVisibility = () => { + const visible = input.checked; + this._optionalContentConfig.setVisibility(groupId, visible); + const cached = this._optionalContentVisibility.get(groupId); + if (cached) { + cached.visible = visible; + } + this.eventBus.dispatch("optionalcontentconfig", { + source: this, + promise: Promise.resolve(this._optionalContentConfig), + }); + }; + element.onclick = (evt) => { + if (evt.target === input) { + setVisibility(); + return true; + } else if (evt.target !== element) { + return true; + } + input.checked = !input.checked; + setVisibility(); + return false; + }; + } + _setNestedName(element, { name = null }) { + if (typeof name === "string") { + element.textContent = this._normalizeTextContent(name); + return; + } + element.setAttribute("data-l10n-id", "pdfjs-additional-layers"); + element.style.fontStyle = "italic"; + this._l10n.translateOnce(element); + } + _addToggleButton(div, { name = null }) { + super._addToggleButton(div, name === null); + } + _toggleAllTreeItems() { + if (!this._optionalContentConfig) { + return; + } + super._toggleAllTreeItems(); + } + render({ optionalContentConfig, pdfDocument }) { + if (this._optionalContentConfig) { + this.reset(); + } + this._optionalContentConfig = optionalContentConfig || null; + this._pdfDocument = pdfDocument || null; + const groups = optionalContentConfig?.getOrder(); + if (!groups) { + this._dispatchEvent(0); + return; + } + this._optionalContentVisibility = new Map(); + const fragment = document.createDocumentFragment(), + queue = [ + { + parent: fragment, + groups, + }, + ]; + let layersCount = 0, + hasAnyNesting = false; + while (queue.length > 0) { + const levelData = queue.shift(); + for (const groupId of levelData.groups) { + const div = document.createElement("div"); + div.className = "treeItem"; + const element = document.createElement("a"); + div.append(element); + if (typeof groupId === "object") { + hasAnyNesting = true; + this._addToggleButton(div, groupId); + this._setNestedName(element, groupId); + const itemsDiv = document.createElement("div"); + itemsDiv.className = "treeItems"; + div.append(itemsDiv); + queue.push({ + parent: itemsDiv, + groups: groupId.order, + }); + } else { + const group = optionalContentConfig.getGroup(groupId); + const input = document.createElement("input"); + this._bindLink(element, { + groupId, + input, + }); + input.type = "checkbox"; + input.checked = group.visible; + this._optionalContentVisibility.set(groupId, { + input, + visible: input.checked, + }); + const label = document.createElement("label"); + label.textContent = this._normalizeTextContent(group.name); + label.append(input); + element.append(label); + layersCount++; + } + levelData.parent.append(div); + } + } + this._finishRendering(fragment, layersCount, hasAnyNesting); + } + async #updateLayers(promise = null) { + if (!this._optionalContentConfig) { + return; + } + const pdfDocument = this._pdfDocument; + const optionalContentConfig = await (promise || + pdfDocument.getOptionalContentConfig({ + intent: "display", + })); + if (pdfDocument !== this._pdfDocument) { + return; + } + if (promise) { + for (const [groupId, cached] of this._optionalContentVisibility) { + const group = optionalContentConfig.getGroup(groupId); + if (group && cached.visible !== group.visible) { + cached.input.checked = cached.visible = !cached.visible; + } + } + return; + } + this.eventBus.dispatch("optionalcontentconfig", { + source: this, + promise: Promise.resolve(optionalContentConfig), + }); + this.render({ + optionalContentConfig, + pdfDocument: this._pdfDocument, + }); + } +} // ./web/pdf_outline_viewer.js + +class PDFOutlineViewer extends BaseTreeViewer { + constructor(options) { + super(options); + this.linkService = options.linkService; + this.downloadManager = options.downloadManager; + this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this)); + this.eventBus._on( + "currentoutlineitem", + this._currentOutlineItem.bind(this), + ); + this.eventBus._on("pagechanging", (evt) => { + this._currentPageNumber = evt.pageNumber; + }); + this.eventBus._on("pagesloaded", (evt) => { + this._isPagesLoaded = !!evt.pagesCount; + this._currentOutlineItemCapability?.resolve(this._isPagesLoaded); + }); + this.eventBus._on("sidebarviewchanged", (evt) => { + this._sidebarView = evt.view; + }); + } + reset() { + super.reset(); + this._outline = null; + this._pageNumberToDestHashCapability = null; + this._currentPageNumber = 1; + this._isPagesLoaded = null; + this._currentOutlineItemCapability?.resolve(false); + this._currentOutlineItemCapability = null; + } + _dispatchEvent(outlineCount) { + this._currentOutlineItemCapability = Promise.withResolvers(); + if ( + outlineCount === 0 || + this._pdfDocument?.loadingParams.disableAutoFetch + ) { + this._currentOutlineItemCapability.resolve(false); + } else if (this._isPagesLoaded !== null) { + this._currentOutlineItemCapability.resolve(this._isPagesLoaded); + } + this.eventBus.dispatch("outlineloaded", { + source: this, + outlineCount, + currentOutlineItemPromise: this._currentOutlineItemCapability.promise, + }); + } + _bindLink( + element, + { url, newWindow, action, attachment, dest, setOCGState }, + ) { + const { linkService } = this; + if (url) { + linkService.addLinkAttributes(element, url, newWindow); + return; + } + if (action) { + element.href = linkService.getAnchorUrl(""); + element.onclick = () => { + linkService.executeNamedAction(action); + return false; + }; + return; + } + if (attachment) { + element.href = linkService.getAnchorUrl(""); + element.onclick = () => { + this.downloadManager.openOrDownloadData( + attachment.content, + attachment.filename, + ); + return false; + }; + return; + } + if (setOCGState) { + element.href = linkService.getAnchorUrl(""); + element.onclick = () => { + linkService.executeSetOCGState(setOCGState); + return false; + }; + return; + } + element.href = linkService.getDestinationHash(dest); + element.onclick = (evt) => { + this._updateCurrentTreeItem(evt.target.parentNode); + if (dest) { + linkService.goToDestination(dest); + } + return false; + }; + } + _setStyles(element, { bold, italic }) { + if (bold) { + element.style.fontWeight = "bold"; + } + if (italic) { + element.style.fontStyle = "italic"; + } + } + _addToggleButton(div, { count, items }) { + let hidden = false; + if (count < 0) { + let totalCount = items.length; + if (totalCount > 0) { + const queue = [...items]; + while (queue.length > 0) { + const { count: nestedCount, items: nestedItems } = queue.shift(); + if (nestedCount > 0 && nestedItems.length > 0) { + totalCount += nestedItems.length; + queue.push(...nestedItems); + } + } + } + if (Math.abs(count) === totalCount) { + hidden = true; + } + } + super._addToggleButton(div, hidden); + } + _toggleAllTreeItems() { + if (!this._outline) { + return; + } + super._toggleAllTreeItems(); + } + render({ outline, pdfDocument }) { + if (this._outline) { + this.reset(); + } + this._outline = outline || null; + this._pdfDocument = pdfDocument || null; + if (!outline) { + this._dispatchEvent(0); + return; + } + const fragment = document.createDocumentFragment(); + const queue = [ + { + parent: fragment, + items: outline, + }, + ]; + let outlineCount = 0, + hasAnyNesting = false; + while (queue.length > 0) { + const levelData = queue.shift(); + for (const item of levelData.items) { + const div = document.createElement("div"); + div.className = "treeItem"; + const element = document.createElement("a"); + this._bindLink(element, item); + this._setStyles(element, item); + element.textContent = this._normalizeTextContent(item.title); + div.append(element); + if (item.items.length > 0) { + hasAnyNesting = true; + this._addToggleButton(div, item); + const itemsDiv = document.createElement("div"); + itemsDiv.className = "treeItems"; + div.append(itemsDiv); + queue.push({ + parent: itemsDiv, + items: item.items, + }); + } + levelData.parent.append(div); + outlineCount++; + } + } + this._finishRendering(fragment, outlineCount, hasAnyNesting); + } + async _currentOutlineItem() { + if (!this._isPagesLoaded) { + throw new Error("_currentOutlineItem: All pages have not been loaded."); + } + if (!this._outline || !this._pdfDocument) { + return; + } + const pageNumberToDestHash = await this._getPageNumberToDestHash( + this._pdfDocument, + ); + if (!pageNumberToDestHash) { + return; + } + this._updateCurrentTreeItem(null); + if (this._sidebarView !== SidebarView.OUTLINE) { + return; + } + for (let i = this._currentPageNumber; i > 0; i--) { + const destHash = pageNumberToDestHash.get(i); + if (!destHash) { + continue; + } + const linkElement = this.container.querySelector(`a[href="${destHash}"]`); + if (!linkElement) { + continue; + } + this._scrollToCurrentTreeItem(linkElement.parentNode); + break; + } + } + async _getPageNumberToDestHash(pdfDocument) { + if (this._pageNumberToDestHashCapability) { + return this._pageNumberToDestHashCapability.promise; + } + this._pageNumberToDestHashCapability = Promise.withResolvers(); + const pageNumberToDestHash = new Map(), + pageNumberNesting = new Map(); + const queue = [ + { + nesting: 0, + items: this._outline, + }, + ]; + while (queue.length > 0) { + const levelData = queue.shift(), + currentNesting = levelData.nesting; + for (const { dest, items } of levelData.items) { + let explicitDest, pageNumber; + if (typeof dest === "string") { + explicitDest = await pdfDocument.getDestination(dest); + if (pdfDocument !== this._pdfDocument) { + return null; + } + } else { + explicitDest = dest; + } + if (Array.isArray(explicitDest)) { + const [destRef] = explicitDest; + if (destRef && typeof destRef === "object") { + pageNumber = pdfDocument.cachedPageNumber(destRef); + } else if (Number.isInteger(destRef)) { + pageNumber = destRef + 1; + } + if ( + Number.isInteger(pageNumber) && + (!pageNumberToDestHash.has(pageNumber) || + currentNesting > pageNumberNesting.get(pageNumber)) + ) { + const destHash = this.linkService.getDestinationHash(dest); + pageNumberToDestHash.set(pageNumber, destHash); + pageNumberNesting.set(pageNumber, currentNesting); + } + } + if (items.length > 0) { + queue.push({ + nesting: currentNesting + 1, + items, + }); + } + } + } + this._pageNumberToDestHashCapability.resolve( + pageNumberToDestHash.size > 0 ? pageNumberToDestHash : null, + ); + return this._pageNumberToDestHashCapability.promise; + } +} // ./web/pdf_presentation_mode.js + +const DELAY_BEFORE_HIDING_CONTROLS = 3000; +const ACTIVE_SELECTOR = "pdfPresentationMode"; +const CONTROLS_SELECTOR = "pdfPresentationModeControls"; +const MOUSE_SCROLL_COOLDOWN_TIME = 50; +const PAGE_SWITCH_THRESHOLD = 0.1; +const SWIPE_MIN_DISTANCE_THRESHOLD = 50; +const SWIPE_ANGLE_THRESHOLD = Math.PI / 6; +class PDFPresentationMode { + #state = PresentationModeState.UNKNOWN; + #args = null; + #fullscreenChangeAbortController = null; + #windowAbortController = null; + constructor({ container, pdfViewer, eventBus }) { + this.container = container; + this.pdfViewer = pdfViewer; + this.eventBus = eventBus; + this.contextMenuOpen = false; + this.mouseScrollTimeStamp = 0; + this.mouseScrollDelta = 0; + this.touchSwipeState = null; + } + async request() { + const { container, pdfViewer } = this; + if (this.active || !pdfViewer.pagesCount || !container.requestFullscreen) { + return false; + } + this.#addFullscreenChangeListeners(); + this.#notifyStateChange(PresentationModeState.CHANGING); + const promise = container.requestFullscreen(); + this.#args = { + pageNumber: pdfViewer.currentPageNumber, + scaleValue: pdfViewer.currentScaleValue, + scrollMode: pdfViewer.scrollMode, + spreadMode: null, + annotationEditorMode: null, + }; + if ( + pdfViewer.spreadMode !== SpreadMode.NONE && + !(pdfViewer.pageViewsReady && pdfViewer.hasEqualPageSizes) + ) { + console.warn( + "Ignoring Spread modes when entering PresentationMode, " + + "since the document may contain varying page sizes.", + ); + this.#args.spreadMode = pdfViewer.spreadMode; + } + if (pdfViewer.annotationEditorMode !== AnnotationEditorType.DISABLE) { + this.#args.annotationEditorMode = pdfViewer.annotationEditorMode; + } + try { + await promise; + pdfViewer.focus(); + return true; + } catch { + this.#removeFullscreenChangeListeners(); + this.#notifyStateChange(PresentationModeState.NORMAL); + } + return false; + } + get active() { + return ( + this.#state === PresentationModeState.CHANGING || + this.#state === PresentationModeState.FULLSCREEN + ); + } + #mouseWheel(evt) { + if (!this.active) { + return; + } + evt.preventDefault(); + const delta = normalizeWheelEventDelta(evt); + const currentTime = Date.now(); + const storedTime = this.mouseScrollTimeStamp; + if ( + currentTime > storedTime && + currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME + ) { + return; + } + if ( + (this.mouseScrollDelta > 0 && delta < 0) || + (this.mouseScrollDelta < 0 && delta > 0) + ) { + this.#resetMouseScrollState(); + } + this.mouseScrollDelta += delta; + if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) { + const totalDelta = this.mouseScrollDelta; + this.#resetMouseScrollState(); + const success = + totalDelta > 0 + ? this.pdfViewer.previousPage() + : this.pdfViewer.nextPage(); + if (success) { + this.mouseScrollTimeStamp = currentTime; + } + } + } + #notifyStateChange(state) { + this.#state = state; + this.eventBus.dispatch("presentationmodechanged", { + source: this, + state, + }); + } + #enter() { + this.#notifyStateChange(PresentationModeState.FULLSCREEN); + this.container.classList.add(ACTIVE_SELECTOR); + setTimeout(() => { + this.pdfViewer.scrollMode = ScrollMode.PAGE; + if (this.#args.spreadMode !== null) { + this.pdfViewer.spreadMode = SpreadMode.NONE; + } + this.pdfViewer.currentPageNumber = this.#args.pageNumber; + this.pdfViewer.currentScaleValue = "page-fit"; + if (this.#args.annotationEditorMode !== null) { + this.pdfViewer.annotationEditorMode = { + mode: AnnotationEditorType.NONE, + }; + } + }, 0); + this.#addWindowListeners(); + this.#showControls(); + this.contextMenuOpen = false; + document.getSelection().empty(); + } + #exit() { + const pageNumber = this.pdfViewer.currentPageNumber; + this.container.classList.remove(ACTIVE_SELECTOR); + setTimeout(() => { + this.#removeFullscreenChangeListeners(); + this.#notifyStateChange(PresentationModeState.NORMAL); + this.pdfViewer.scrollMode = this.#args.scrollMode; + if (this.#args.spreadMode !== null) { + this.pdfViewer.spreadMode = this.#args.spreadMode; + } + this.pdfViewer.currentScaleValue = this.#args.scaleValue; + this.pdfViewer.currentPageNumber = pageNumber; + if (this.#args.annotationEditorMode !== null) { + this.pdfViewer.annotationEditorMode = { + mode: this.#args.annotationEditorMode, + }; + } + this.#args = null; + }, 0); + this.#removeWindowListeners(); + this.#hideControls(); + this.#resetMouseScrollState(); + this.contextMenuOpen = false; + } + #mouseDown(evt) { + if (this.contextMenuOpen) { + this.contextMenuOpen = false; + evt.preventDefault(); + return; + } + if (evt.button !== 0) { + return; + } + if ( + evt.target.href && + evt.target.parentNode?.hasAttribute("data-internal-link") + ) { + return; + } + evt.preventDefault(); + if (evt.shiftKey) { + this.pdfViewer.previousPage(); + } else { + this.pdfViewer.nextPage(); + } + } + #contextMenu() { + this.contextMenuOpen = true; + } + #showControls() { + if (this.controlsTimeout) { + clearTimeout(this.controlsTimeout); + } else { + this.container.classList.add(CONTROLS_SELECTOR); + } + this.controlsTimeout = setTimeout(() => { + this.container.classList.remove(CONTROLS_SELECTOR); + delete this.controlsTimeout; + }, DELAY_BEFORE_HIDING_CONTROLS); + } + #hideControls() { + if (!this.controlsTimeout) { + return; + } + clearTimeout(this.controlsTimeout); + this.container.classList.remove(CONTROLS_SELECTOR); + delete this.controlsTimeout; + } + #resetMouseScrollState() { + this.mouseScrollTimeStamp = 0; + this.mouseScrollDelta = 0; + } + #touchSwipe(evt) { + if (!this.active) { + return; + } + if (evt.touches.length > 1) { + this.touchSwipeState = null; + return; + } + switch (evt.type) { + case "touchstart": + this.touchSwipeState = { + startX: evt.touches[0].pageX, + startY: evt.touches[0].pageY, + endX: evt.touches[0].pageX, + endY: evt.touches[0].pageY, + }; + break; + case "touchmove": + if (this.touchSwipeState === null) { + return; + } + this.touchSwipeState.endX = evt.touches[0].pageX; + this.touchSwipeState.endY = evt.touches[0].pageY; + evt.preventDefault(); + break; + case "touchend": + if (this.touchSwipeState === null) { + return; + } + let delta = 0; + const dx = this.touchSwipeState.endX - this.touchSwipeState.startX; + const dy = this.touchSwipeState.endY - this.touchSwipeState.startY; + const absAngle = Math.abs(Math.atan2(dy, dx)); + if ( + Math.abs(dx) > SWIPE_MIN_DISTANCE_THRESHOLD && + (absAngle <= SWIPE_ANGLE_THRESHOLD || + absAngle >= Math.PI - SWIPE_ANGLE_THRESHOLD) + ) { + delta = dx; + } else if ( + Math.abs(dy) > SWIPE_MIN_DISTANCE_THRESHOLD && + Math.abs(absAngle - Math.PI / 2) <= SWIPE_ANGLE_THRESHOLD + ) { + delta = dy; + } + if (delta > 0) { + this.pdfViewer.previousPage(); + } else if (delta < 0) { + this.pdfViewer.nextPage(); + } + break; + } + } + #addWindowListeners() { + if (this.#windowAbortController) { + return; + } + this.#windowAbortController = new AbortController(); + const { signal } = this.#windowAbortController; + const touchSwipeBind = this.#touchSwipe.bind(this); + window.addEventListener("mousemove", this.#showControls.bind(this), { + signal, + }); + window.addEventListener("mousedown", this.#mouseDown.bind(this), { + signal, + }); + window.addEventListener("wheel", this.#mouseWheel.bind(this), { + passive: false, + signal, + }); + window.addEventListener("keydown", this.#resetMouseScrollState.bind(this), { + signal, + }); + window.addEventListener("contextmenu", this.#contextMenu.bind(this), { + signal, + }); + window.addEventListener("touchstart", touchSwipeBind, { + signal, + }); + window.addEventListener("touchmove", touchSwipeBind, { + signal, + }); + window.addEventListener("touchend", touchSwipeBind, { + signal, + }); + } + #removeWindowListeners() { + this.#windowAbortController?.abort(); + this.#windowAbortController = null; + } + #addFullscreenChangeListeners() { + if (this.#fullscreenChangeAbortController) { + return; + } + this.#fullscreenChangeAbortController = new AbortController(); + window.addEventListener( + "fullscreenchange", + () => { + if (document.fullscreenElement) { + this.#enter(); + } else { + this.#exit(); + } + }, + { + signal: this.#fullscreenChangeAbortController.signal, + }, + ); + } + #removeFullscreenChangeListeners() { + this.#fullscreenChangeAbortController?.abort(); + this.#fullscreenChangeAbortController = null; + } +} // ./web/xfa_layer_builder.js + +class XfaLayerBuilder { + constructor({ + pdfPage, + annotationStorage = null, + linkService, + xfaHtml = null, + }) { + this.pdfPage = pdfPage; + this.annotationStorage = annotationStorage; + this.linkService = linkService; + this.xfaHtml = xfaHtml; + this.div = null; + this._cancelled = false; + } + async render(viewport, intent = "display") { + if (intent === "print") { + const parameters = { + viewport: viewport.clone({ + dontFlip: true, + }), + div: this.div, + xfaHtml: this.xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent, + }; + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + const xfaHtml = await this.pdfPage.getXfa(); + if (this._cancelled || !xfaHtml) { + return { + textDivs: [], + }; + } + const parameters = { + viewport: viewport.clone({ + dontFlip: true, + }), + div: this.div, + xfaHtml, + annotationStorage: this.annotationStorage, + linkService: this.linkService, + intent, + }; + if (this.div) { + return XfaLayer.update(parameters); + } + this.div = document.createElement("div"); + parameters.div = this.div; + return XfaLayer.render(parameters); + } + cancel() { + this._cancelled = true; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } +} // ./web/print_utils.js + +function getXfaHtmlForPrinting(printContainer, pdfDocument) { + const xfaHtml = pdfDocument.allXfaHtml; + const linkService = new SimpleLinkService(); + const scale = Math.round(PixelsPerInch.PDF_TO_CSS_UNITS * 100) / 100; + for (const xfaPage of xfaHtml.children) { + const page = document.createElement("div"); + page.className = "xfaPrintedPage"; + printContainer.append(page); + const builder = new XfaLayerBuilder({ + pdfPage: null, + annotationStorage: pdfDocument.annotationStorage, + linkService, + xfaHtml: xfaPage, + }); + const viewport = getXfaPageViewport(xfaPage, { + scale, + }); + builder.render(viewport, "print"); + page.append(builder.div); + } +} // ./web/pdf_print_service.js + +let activeService = null; +let dialog = null; +let overlayManager = null; +let viewerApp = { + initialized: false, +}; +function renderPage( + activeServiceOnEntry, + pdfDocument, + pageNumber, + size, + printResolution, + optionalContentConfigPromise, + printAnnotationStoragePromise, +) { + const scratchCanvas = activeService.scratchCanvas; + const PRINT_UNITS = printResolution / PixelsPerInch.PDF; + scratchCanvas.width = Math.floor(size.width * PRINT_UNITS); + scratchCanvas.height = Math.floor(size.height * PRINT_UNITS); + const ctx = scratchCanvas.getContext("2d"); + ctx.save(); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height); + ctx.restore(); + return Promise.all([ + pdfDocument.getPage(pageNumber), + printAnnotationStoragePromise, + ]).then(function ([pdfPage, printAnnotationStorage]) { + const renderContext = { + canvasContext: ctx, + transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], + viewport: pdfPage.getViewport({ + scale: 1, + rotation: size.rotation, + }), + intent: "print", + annotationMode: AnnotationMode.ENABLE_STORAGE, + optionalContentConfigPromise, + printAnnotationStorage, + }; + const renderTask = pdfPage.render(renderContext); + return renderTask.promise.catch((reason) => { + if (!(reason instanceof RenderingCancelledException)) { + console.error(reason); + } + throw reason; + }); + }); +} +class PDFPrintService { + constructor({ + pdfDocument, + pagesOverview, + printContainer, + printResolution, + printAnnotationStoragePromise = null, + }) { + this.pdfDocument = pdfDocument; + this.pagesOverview = pagesOverview; + this.printContainer = printContainer; + this._printResolution = printResolution || 150; + this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "print", + }); + this._printAnnotationStoragePromise = + printAnnotationStoragePromise || Promise.resolve(); + this.currentPage = -1; + this.scratchCanvas = document.createElement("canvas"); + } + layout() { + this.throwIfInactive(); + const body = document.querySelector("body"); + body.setAttribute("data-pdfjsprinting", true); + const { width, height } = this.pagesOverview[0]; + const hasEqualPageSizes = this.pagesOverview.every( + (size) => size.width === width && size.height === height, + ); + if (!hasEqualPageSizes) { + console.warn( + "Not all pages have the same size. The printed result may be incorrect!", + ); + } + this.pageStyleSheet = document.createElement("style"); + this.pageStyleSheet.textContent = `@page { size: ${width}pt ${height}pt;}`; + body.append(this.pageStyleSheet); + } + destroy() { + if (activeService !== this) { + return; + } + this.printContainer.textContent = ""; + const body = document.querySelector("body"); + body.removeAttribute("data-pdfjsprinting"); + if (this.pageStyleSheet) { + this.pageStyleSheet.remove(); + this.pageStyleSheet = null; + } + this.scratchCanvas.width = this.scratchCanvas.height = 0; + this.scratchCanvas = null; + activeService = null; + ensureOverlay().then(function () { + if (overlayManager.active === dialog) { + overlayManager.close(dialog); + } + }); + } + renderPages() { + if (this.pdfDocument.isPureXfa) { + getXfaHtmlForPrinting(this.printContainer, this.pdfDocument); + return Promise.resolve(); + } + const pageCount = this.pagesOverview.length; + const renderNextPage = (resolve, reject) => { + this.throwIfInactive(); + if (++this.currentPage >= pageCount) { + renderProgress(pageCount, pageCount); + resolve(); + return; + } + const index = this.currentPage; + renderProgress(index, pageCount); + renderPage( + this, + this.pdfDocument, + index + 1, + this.pagesOverview[index], + this._printResolution, + this._optionalContentConfigPromise, + this._printAnnotationStoragePromise, + ) + .then(this.useRenderedPage.bind(this)) + .then(function () { + renderNextPage(resolve, reject); + }, reject); + }; + return new Promise(renderNextPage); + } + useRenderedPage() { + this.throwIfInactive(); + const img = document.createElement("img"); + this.scratchCanvas.toBlob((blob) => { + img.src = URL.createObjectURL(blob); + }); + const wrapper = document.createElement("div"); + wrapper.className = "printedPage"; + wrapper.append(img); + this.printContainer.append(wrapper); + const { promise, resolve, reject } = Promise.withResolvers(); + img.onload = resolve; + img.onerror = reject; + promise + .catch(() => {}) + .then(() => { + URL.revokeObjectURL(img.src); + }); + return promise; + } + performPrint() { + this.throwIfInactive(); + return new Promise((resolve) => { + setTimeout(() => { + if (!this.active) { + resolve(); + return; + } + print.call(window); + setTimeout(resolve, 20); + }, 0); + }); + } + get active() { + return this === activeService; + } + throwIfInactive() { + if (!this.active) { + throw new Error("This print request was cancelled or completed."); + } + } +} +const print = window.print; +window.print = function () { + if (activeService) { + console.warn("Ignored window.print() because of a pending print job."); + return; + } + ensureOverlay().then(function () { + if (activeService) { + overlayManager.open(dialog); + } + }); + try { + dispatchEvent("beforeprint"); + } finally { + if (!activeService) { + console.error("Expected print service to be initialized."); + ensureOverlay().then(function () { + if (overlayManager.active === dialog) { + overlayManager.close(dialog); + } + }); + return; + } + const activeServiceOnEntry = activeService; + activeService + .renderPages() + .then(function () { + return activeServiceOnEntry.performPrint(); + }) + .catch(function () {}) + .then(function () { + if (activeServiceOnEntry.active) { + abort(); + } + }); + } +}; +function dispatchEvent(eventType) { + const event = new CustomEvent(eventType, { + bubbles: false, + cancelable: false, + detail: "custom", + }); + window.dispatchEvent(event); +} +function abort() { + if (activeService) { + activeService.destroy(); + dispatchEvent("afterprint"); + } +} +function renderProgress(index, total) { + dialog ||= document.getElementById("printServiceDialog"); + const progress = Math.round((100 * index) / total); + const progressBar = dialog.querySelector("progress"); + const progressPerc = dialog.querySelector(".relative-progress"); + progressBar.value = progress; + progressPerc.setAttribute( + "data-l10n-args", + JSON.stringify({ + progress, + }), + ); +} +window.addEventListener( + "keydown", + function (event) { + if ( + event.keyCode === 80 && + (event.ctrlKey || event.metaKey) && + !event.altKey && + (!event.shiftKey || window.chrome || window.opera) + ) { + window.print(); + event.preventDefault(); + event.stopImmediatePropagation(); + } + }, + true, +); +if ("onbeforeprint" in window) { + const stopPropagationIfNeeded = function (event) { + if (event.detail !== "custom") { + event.stopImmediatePropagation(); + } + }; + window.addEventListener("beforeprint", stopPropagationIfNeeded); + window.addEventListener("afterprint", stopPropagationIfNeeded); +} +let overlayPromise; +function ensureOverlay() { + if (!overlayPromise) { + overlayManager = viewerApp.overlayManager; + if (!overlayManager) { + throw new Error("The overlay manager has not yet been initialized."); + } + dialog ||= document.getElementById("printServiceDialog"); + overlayPromise = overlayManager.register(dialog, true); + document.getElementById("printCancel").onclick = abort; + dialog.addEventListener("close", abort); + } + return overlayPromise; +} +class PDFPrintServiceFactory { + static initGlobals(app) { + viewerApp = app; + } + static get supportsPrinting() { + return shadow(this, "supportsPrinting", true); + } + static createPrintService(params) { + if (activeService) { + throw new Error("The print service is created and active."); + } + return (activeService = new PDFPrintService(params)); + } +} // ./web/pdf_rendering_queue.js + +const CLEANUP_TIMEOUT = 30000; +class PDFRenderingQueue { + constructor() { + this.pdfViewer = null; + this.pdfThumbnailViewer = null; + this.onIdle = null; + this.highestPriorityPage = null; + this.idleTimeout = null; + this.printing = false; + this.isThumbnailViewEnabled = false; + Object.defineProperty(this, "hasViewer", { + value: () => !!this.pdfViewer, + }); + } + setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + } + setThumbnailViewer(pdfThumbnailViewer) { + this.pdfThumbnailViewer = pdfThumbnailViewer; + } + isHighestPriority(view) { + return this.highestPriorityPage === view.renderingId; + } + renderHighestPriority(currentlyVisiblePages) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; + } + if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { + return; + } + if ( + this.isThumbnailViewEnabled && + this.pdfThumbnailViewer?.forceRendering() + ) { + return; + } + if (this.printing) { + return; + } + if (this.onIdle) { + this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); + } + } + getHighestPriority(visible, views, scrolledDown, preRenderExtra = false) { + const visibleViews = visible.views, + numVisible = visibleViews.length; + if (numVisible === 0) { + return null; + } + for (let i = 0; i < numVisible; i++) { + const view = visibleViews[i].view; + if (!this.isViewFinished(view)) { + return view; + } + } + const firstId = visible.first.id, + lastId = visible.last.id; + if (lastId - firstId + 1 > numVisible) { + const visibleIds = visible.ids; + for (let i = 1, ii = lastId - firstId; i < ii; i++) { + const holeId = scrolledDown ? firstId + i : lastId - i; + if (visibleIds.has(holeId)) { + continue; + } + const holeView = views[holeId - 1]; + if (!this.isViewFinished(holeView)) { + return holeView; + } + } + } + let preRenderIndex = scrolledDown ? lastId : firstId - 2; + let preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + if (preRenderExtra) { + preRenderIndex += scrolledDown ? 1 : -1; + preRenderView = views[preRenderIndex]; + if (preRenderView && !this.isViewFinished(preRenderView)) { + return preRenderView; + } + } + return null; + } + isViewFinished(view) { + return view.renderingState === RenderingStates.FINISHED; + } + renderView(view) { + switch (view.renderingState) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + this.highestPriorityPage = view.renderingId; + view.resume(); + break; + case RenderingStates.RUNNING: + this.highestPriorityPage = view.renderingId; + break; + case RenderingStates.INITIAL: + this.highestPriorityPage = view.renderingId; + view + .draw() + .finally(() => { + this.renderHighestPriority(); + }) + .catch((reason) => { + if (reason instanceof RenderingCancelledException) { + return; + } + console.error("renderView:", reason); + }); + break; + } + return true; + } +} // ./web/pdf_scripting_manager.js + +class PDFScriptingManager { + #closeCapability = null; + #destroyCapability = null; + #docProperties = null; + #eventAbortController = null; + #eventBus = null; + #externalServices = null; + #pdfDocument = null; + #pdfViewer = null; + #ready = false; + #scripting = null; + #willPrintCapability = null; + constructor({ eventBus, externalServices = null, docProperties = null }) { + this.#eventBus = eventBus; + this.#externalServices = externalServices; + this.#docProperties = docProperties; + } + setViewer(pdfViewer) { + this.#pdfViewer = pdfViewer; + } + async setDocument(pdfDocument) { + if (this.#pdfDocument) { + await this.#destroyScripting(); + } + this.#pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const [objects, calculationOrder, docActions] = await Promise.all([ + pdfDocument.getFieldObjects(), + pdfDocument.getCalculationOrderIds(), + pdfDocument.getJSActions(), + ]); + if (!objects && !docActions) { + await this.#destroyScripting(); + return; + } + if (pdfDocument !== this.#pdfDocument) { + return; + } + try { + this.#scripting = this.#initScripting(); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + const eventBus = this.#eventBus; + this.#eventAbortController = new AbortController(); + const { signal } = this.#eventAbortController; + eventBus._on( + "updatefromsandbox", + (event) => { + if (event?.source === window) { + this.#updateFromSandbox(event.detail); + } + }, + { + signal, + }, + ); + eventBus._on( + "dispatcheventinsandbox", + (event) => { + this.#scripting?.dispatchEventInSandbox(event.detail); + }, + { + signal, + }, + ); + eventBus._on( + "pagechanging", + ({ pageNumber, previous }) => { + if (pageNumber === previous) { + return; + } + this.#dispatchPageClose(previous); + this.#dispatchPageOpen(pageNumber); + }, + { + signal, + }, + ); + eventBus._on( + "pagerendered", + ({ pageNumber }) => { + if (!this._pageOpenPending.has(pageNumber)) { + return; + } + if (pageNumber !== this.#pdfViewer.currentPageNumber) { + return; + } + this.#dispatchPageOpen(pageNumber); + }, + { + signal, + }, + ); + eventBus._on( + "pagesdestroy", + async () => { + await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber); + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillClose", + }); + this.#closeCapability?.resolve(); + }, + { + signal, + }, + ); + try { + const docProperties = await this.#docProperties(pdfDocument); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting.createSandbox({ + objects, + calculationOrder, + appInfo: { + platform: navigator.platform, + language: navigator.language, + }, + docInfo: { + ...docProperties, + actions: docActions, + }, + }); + eventBus.dispatch("sandboxcreated", { + source: this, + }); + } catch (error) { + console.error("setDocument:", error); + await this.#destroyScripting(); + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "Open", + }); + await this.#dispatchPageOpen(this.#pdfViewer.currentPageNumber, true); + Promise.resolve().then(() => { + if (pdfDocument === this.#pdfDocument) { + this.#ready = true; + } + }); + } + async dispatchWillSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "WillSave", + }); + } + async dispatchDidSave() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidSave", + }); + } + async dispatchWillPrint() { + if (!this.#scripting) { + return; + } + await this.#willPrintCapability?.promise; + this.#willPrintCapability = Promise.withResolvers(); + try { + await this.#scripting.dispatchEventInSandbox({ + id: "doc", + name: "WillPrint", + }); + } catch (ex) { + this.#willPrintCapability.resolve(); + this.#willPrintCapability = null; + throw ex; + } + await this.#willPrintCapability.promise; + } + async dispatchDidPrint() { + return this.#scripting?.dispatchEventInSandbox({ + id: "doc", + name: "DidPrint", + }); + } + get destroyPromise() { + return this.#destroyCapability?.promise || null; + } + get ready() { + return this.#ready; + } + get _pageOpenPending() { + return shadow(this, "_pageOpenPending", new Set()); + } + get _visitedPages() { + return shadow(this, "_visitedPages", new Map()); + } + async #updateFromSandbox(detail) { + const pdfViewer = this.#pdfViewer; + const isInPresentationMode = + pdfViewer.isInPresentationMode || pdfViewer.isChangingPresentationMode; + const { id, siblings, command, value } = detail; + if (!id) { + switch (command) { + case "clear": + console.clear(); + break; + case "error": + console.error(value); + break; + case "layout": + if (!isInPresentationMode) { + const modes = apiPageLayoutToViewerModes(value); + pdfViewer.spreadMode = modes.spreadMode; + } + break; + case "page-num": + pdfViewer.currentPageNumber = value + 1; + break; + case "print": + await pdfViewer.pagesPromise; + this.#eventBus.dispatch("print", { + source: this, + }); + break; + case "println": + console.log(value); + break; + case "zoom": + if (!isInPresentationMode) { + pdfViewer.currentScaleValue = value; + } + break; + case "SaveAs": + this.#eventBus.dispatch("download", { + source: this, + }); + break; + case "FirstPage": + pdfViewer.currentPageNumber = 1; + break; + case "LastPage": + pdfViewer.currentPageNumber = pdfViewer.pagesCount; + break; + case "NextPage": + pdfViewer.nextPage(); + break; + case "PrevPage": + pdfViewer.previousPage(); + break; + case "ZoomViewIn": + if (!isInPresentationMode) { + pdfViewer.increaseScale(); + } + break; + case "ZoomViewOut": + if (!isInPresentationMode) { + pdfViewer.decreaseScale(); + } + break; + case "WillPrintFinished": + this.#willPrintCapability?.resolve(); + this.#willPrintCapability = null; + break; + } + return; + } + if (isInPresentationMode && detail.focus) { + return; + } + delete detail.id; + delete detail.siblings; + const ids = siblings ? [id, ...siblings] : [id]; + for (const elementId of ids) { + const element = document.querySelector( + `[data-element-id="${elementId}"]`, + ); + if (element) { + element.dispatchEvent( + new CustomEvent("updatefromsandbox", { + detail, + }), + ); + } else { + this.#pdfDocument?.annotationStorage.setValue(elementId, detail); + } + } + } + async #dispatchPageOpen(pageNumber, initialize = false) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (initialize) { + this.#closeCapability = Promise.withResolvers(); + } + if (!this.#closeCapability) { + return; + } + const pageView = this.#pdfViewer.getPageView(pageNumber - 1); + if (pageView?.renderingState !== RenderingStates.FINISHED) { + this._pageOpenPending.add(pageNumber); + return; + } + this._pageOpenPending.delete(pageNumber); + const actionsPromise = (async () => { + const actions = await (!visitedPages.has(pageNumber) + ? pageView.pdfPage?.getJSActions() + : null); + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageOpen", + pageNumber, + actions, + }); + })(); + visitedPages.set(pageNumber, actionsPromise); + } + async #dispatchPageClose(pageNumber) { + const pdfDocument = this.#pdfDocument, + visitedPages = this._visitedPages; + if (!this.#closeCapability) { + return; + } + if (this._pageOpenPending.has(pageNumber)) { + return; + } + const actionsPromise = visitedPages.get(pageNumber); + if (!actionsPromise) { + return; + } + visitedPages.set(pageNumber, null); + await actionsPromise; + if (pdfDocument !== this.#pdfDocument) { + return; + } + await this.#scripting?.dispatchEventInSandbox({ + id: "page", + name: "PageClose", + pageNumber, + }); + } + #initScripting() { + this.#destroyCapability = Promise.withResolvers(); + if (this.#scripting) { + throw new Error("#initScripting: Scripting already exists."); + } + return this.#externalServices.createScripting(); + } + async #destroyScripting() { + if (!this.#scripting) { + this.#pdfDocument = null; + this.#destroyCapability?.resolve(); + return; + } + if (this.#closeCapability) { + await Promise.race([ + this.#closeCapability.promise, + new Promise((resolve) => { + setTimeout(resolve, 1000); + }), + ]).catch(() => {}); + this.#closeCapability = null; + } + this.#pdfDocument = null; + try { + await this.#scripting.destroySandbox(); + } catch {} + this.#willPrintCapability?.reject(new Error("Scripting destroyed.")); + this.#willPrintCapability = null; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._pageOpenPending.clear(); + this._visitedPages.clear(); + this.#scripting = null; + this.#ready = false; + this.#destroyCapability?.resolve(); + } +} // ./web/pdf_sidebar.js + +const SIDEBAR_WIDTH_VAR = "--sidebar-width"; +const SIDEBAR_MIN_WIDTH = 200; +const SIDEBAR_RESIZING_CLASS = "sidebarResizing"; +const UI_NOTIFICATION_CLASS = "pdfSidebarNotification"; +class PDFSidebar { + #isRTL = false; + #mouseAC = null; + #outerContainerWidth = null; + #width = null; + constructor({ elements, eventBus, l10n }) { + this.isOpen = false; + this.active = SidebarView.THUMBS; + this.isInitialViewSet = false; + this.isInitialEventDispatched = false; + this.onToggled = null; + this.onUpdateThumbnails = null; + this.outerContainer = elements.outerContainer; + this.sidebarContainer = elements.sidebarContainer; + this.toggleButton = elements.toggleButton; + this.resizer = elements.resizer; + this.thumbnailButton = elements.thumbnailButton; + this.outlineButton = elements.outlineButton; + this.attachmentsButton = elements.attachmentsButton; + this.layersButton = elements.layersButton; + this.thumbnailView = elements.thumbnailView; + this.outlineView = elements.outlineView; + this.attachmentsView = elements.attachmentsView; + this.layersView = elements.layersView; + this._currentOutlineItemButton = elements.currentOutlineItemButton; + this.eventBus = eventBus; + this.#isRTL = l10n.getDirection() === "rtl"; + this.#addEventListeners(); + } + reset() { + this.isInitialViewSet = false; + this.isInitialEventDispatched = false; + this.#hideUINotification(true); + this.switchView(SidebarView.THUMBS); + this.outlineButton.disabled = false; + this.attachmentsButton.disabled = false; + this.layersButton.disabled = false; + this._currentOutlineItemButton.disabled = true; + } + get visibleView() { + return this.isOpen ? this.active : SidebarView.NONE; + } + setInitialView(view = SidebarView.NONE) { + if (this.isInitialViewSet) { + return; + } + this.isInitialViewSet = true; + if (view === SidebarView.NONE || view === SidebarView.UNKNOWN) { + this.#dispatchEvent(); + return; + } + this.switchView(view, true); + if (!this.isInitialEventDispatched) { + this.#dispatchEvent(); + } + } + switchView(view, forceOpen = false) { + const isViewChanged = view !== this.active; + let forceRendering = false; + switch (view) { + case SidebarView.NONE: + if (this.isOpen) { + this.close(); + } + return; + case SidebarView.THUMBS: + if (this.isOpen && isViewChanged) { + forceRendering = true; + } + break; + case SidebarView.OUTLINE: + if (this.outlineButton.disabled) { + return; + } + break; + case SidebarView.ATTACHMENTS: + if (this.attachmentsButton.disabled) { + return; + } + break; + case SidebarView.LAYERS: + if (this.layersButton.disabled) { + return; + } + break; + default: + console.error(`PDFSidebar.switchView: "${view}" is not a valid view.`); + return; + } + this.active = view; + toggleCheckedBtn( + this.thumbnailButton, + view === SidebarView.THUMBS, + this.thumbnailView, + ); + toggleCheckedBtn( + this.outlineButton, + view === SidebarView.OUTLINE, + this.outlineView, + ); + toggleCheckedBtn( + this.attachmentsButton, + view === SidebarView.ATTACHMENTS, + this.attachmentsView, + ); + toggleCheckedBtn( + this.layersButton, + view === SidebarView.LAYERS, + this.layersView, + ); + if (forceOpen && !this.isOpen) { + this.open(); + return; + } + if (forceRendering) { + this.onUpdateThumbnails(); + this.onToggled(); + } + if (isViewChanged) { + this.#dispatchEvent(); + } + } + open() { + if (this.isOpen) { + return; + } + this.isOpen = true; + toggleExpandedBtn(this.toggleButton, true); + this.outerContainer.classList.add("sidebarMoving", "sidebarOpen"); + if (this.active === SidebarView.THUMBS) { + this.onUpdateThumbnails(); + } + this.onToggled(); + this.#dispatchEvent(); + this.#hideUINotification(); + } + close(evt = null) { + if (!this.isOpen) { + return; + } + this.isOpen = false; + toggleExpandedBtn(this.toggleButton, false); + this.outerContainer.classList.add("sidebarMoving"); + this.outerContainer.classList.remove("sidebarOpen"); + this.onToggled(); + this.#dispatchEvent(); + if (evt?.detail > 0) { + this.toggleButton.blur(); + } + } + toggle(evt = null) { + if (this.isOpen) { + this.close(evt); + } else { + this.open(); + } + } + #dispatchEvent() { + if (this.isInitialViewSet) { + this.isInitialEventDispatched ||= true; + } + this.eventBus.dispatch("sidebarviewchanged", { + source: this, + view: this.visibleView, + }); + } + #showUINotification() { + this.toggleButton.setAttribute( + "data-l10n-id", + "pdfjs-toggle-sidebar-notification-button", + ); + if (!this.isOpen) { + this.toggleButton.classList.add(UI_NOTIFICATION_CLASS); + } + } + #hideUINotification(reset = false) { + if (this.isOpen || reset) { + this.toggleButton.classList.remove(UI_NOTIFICATION_CLASS); + } + if (reset) { + this.toggleButton.setAttribute( + "data-l10n-id", + "pdfjs-toggle-sidebar-button", + ); + } + } + #addEventListeners() { + const { eventBus, outerContainer } = this; + this.sidebarContainer.addEventListener("transitionend", (evt) => { + if (evt.target === this.sidebarContainer) { + outerContainer.classList.remove("sidebarMoving"); + eventBus.dispatch("resize", { + source: this, + }); + } + }); + this.toggleButton.addEventListener("click", (evt) => { + this.toggle(evt); + }); + this.thumbnailButton.addEventListener("click", () => { + this.switchView(SidebarView.THUMBS); + }); + this.outlineButton.addEventListener("click", () => { + this.switchView(SidebarView.OUTLINE); + }); + this.outlineButton.addEventListener("dblclick", () => { + eventBus.dispatch("toggleoutlinetree", { + source: this, + }); + }); + this.attachmentsButton.addEventListener("click", () => { + this.switchView(SidebarView.ATTACHMENTS); + }); + this.layersButton.addEventListener("click", () => { + this.switchView(SidebarView.LAYERS); + }); + this.layersButton.addEventListener("dblclick", () => { + eventBus.dispatch("resetlayers", { + source: this, + }); + }); + this._currentOutlineItemButton.addEventListener("click", () => { + eventBus.dispatch("currentoutlineitem", { + source: this, + }); + }); + const onTreeLoaded = (count, button, view) => { + button.disabled = !count; + if (count) { + this.#showUINotification(); + } else if (this.active === view) { + this.switchView(SidebarView.THUMBS); + } + }; + eventBus._on("outlineloaded", (evt) => { + onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE); + evt.currentOutlineItemPromise.then((enabled) => { + if (!this.isInitialViewSet) { + return; + } + this._currentOutlineItemButton.disabled = !enabled; + }); + }); + eventBus._on("attachmentsloaded", (evt) => { + onTreeLoaded( + evt.attachmentsCount, + this.attachmentsButton, + SidebarView.ATTACHMENTS, + ); + }); + eventBus._on("layersloaded", (evt) => { + onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS); + }); + eventBus._on("presentationmodechanged", (evt) => { + if ( + evt.state === PresentationModeState.NORMAL && + this.visibleView === SidebarView.THUMBS + ) { + this.onUpdateThumbnails(); + } + }); + this.resizer.addEventListener("mousedown", (evt) => { + if (evt.button !== 0) { + return; + } + outerContainer.classList.add(SIDEBAR_RESIZING_CLASS); + this.#mouseAC = new AbortController(); + const opts = { + signal: this.#mouseAC.signal, + }; + window.addEventListener("mousemove", this.#mouseMove.bind(this), opts); + window.addEventListener("mouseup", this.#mouseUp.bind(this), opts); + window.addEventListener("blur", this.#mouseUp.bind(this), opts); + }); + eventBus._on("resize", (evt) => { + if (evt.source !== window) { + return; + } + this.#outerContainerWidth = null; + if (!this.#width) { + return; + } + if (!this.isOpen) { + this.#updateWidth(this.#width); + return; + } + outerContainer.classList.add(SIDEBAR_RESIZING_CLASS); + const updated = this.#updateWidth(this.#width); + Promise.resolve().then(() => { + outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS); + if (updated) { + eventBus.dispatch("resize", { + source: this, + }); + } + }); + }); + } + get outerContainerWidth() { + return (this.#outerContainerWidth ||= this.outerContainer.clientWidth); + } + #updateWidth(width = 0) { + const maxWidth = Math.floor(this.outerContainerWidth / 2); + if (width > maxWidth) { + width = maxWidth; + } + if (width < SIDEBAR_MIN_WIDTH) { + width = SIDEBAR_MIN_WIDTH; + } + if (width === this.#width) { + return false; + } + this.#width = width; + docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`); + return true; + } + #mouseMove(evt) { + let width = evt.clientX; + if (this.#isRTL) { + width = this.outerContainerWidth - width; + } + this.#updateWidth(width); + } + #mouseUp(evt) { + this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS); + this.eventBus.dispatch("resize", { + source: this, + }); + this.#mouseAC?.abort(); + this.#mouseAC = null; + } +} // ./web/pdf_thumbnail_view.js + +const DRAW_UPSCALE_FACTOR = 2; +const MAX_NUM_SCALING_STEPS = 3; +const THUMBNAIL_WIDTH = 98; +class TempImageFactory { + static #tempCanvas = null; + static getCanvas(width, height) { + const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas")); + tempCanvas.width = width; + tempCanvas.height = height; + const ctx = tempCanvas.getContext("2d", { + alpha: false, + }); + ctx.save(); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + return [tempCanvas, tempCanvas.getContext("2d")]; + } + static destroyCanvas() { + const tempCanvas = this.#tempCanvas; + if (tempCanvas) { + tempCanvas.width = 0; + tempCanvas.height = 0; + } + this.#tempCanvas = null; + } +} +class PDFThumbnailView { + constructor({ + container, + eventBus, + id, + defaultViewport, + optionalContentConfigPromise, + linkService, + renderingQueue, + pageColors, + enableHWA, + }) { + this.id = id; + this.renderingId = "thumbnail" + id; + this.pageLabel = null; + this.pdfPage = null; + this.rotation = 0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this._optionalContentConfigPromise = optionalContentConfigPromise || null; + this.pageColors = pageColors || null; + this.enableHWA = enableHWA || false; + this.eventBus = eventBus; + this.linkService = linkService; + this.renderingQueue = renderingQueue; + this.renderTask = null; + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + const anchor = document.createElement("a"); + anchor.href = linkService.getAnchorUrl("#page=" + id); + anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title"); + anchor.setAttribute("data-l10n-args", this.#pageL10nArgs); + anchor.onclick = function () { + linkService.goToPage(id); + return false; + }; + this.anchor = anchor; + const div = document.createElement("div"); + div.className = "thumbnail"; + div.setAttribute("data-page-number", this.id); + this.div = div; + this.#updateDims(); + const img = document.createElement("div"); + img.className = "thumbnailImage"; + this._placeholderImg = img; + div.append(img); + anchor.append(div); + container.append(anchor); + } + #updateDims() { + const { width, height } = this.viewport; + const ratio = width / height; + this.canvasWidth = THUMBNAIL_WIDTH; + this.canvasHeight = (this.canvasWidth / ratio) | 0; + this.scale = this.canvasWidth / width; + const { style } = this.div; + style.setProperty("--thumbnail-width", `${this.canvasWidth}px`); + style.setProperty("--thumbnail-height", `${this.canvasHeight}px`); + } + setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport({ + scale: 1, + rotation: totalRotation, + }); + this.reset(); + } + reset() { + this.cancelRendering(); + this.renderingState = RenderingStates.INITIAL; + this.div.removeAttribute("data-loaded"); + this.image?.replaceWith(this._placeholderImg); + this.#updateDims(); + if (this.image) { + this.image.removeAttribute("src"); + delete this.image; + } + } + update({ rotation = null }) { + if (typeof rotation === "number") { + this.rotation = rotation; + } + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: 1, + rotation: totalRotation, + }); + this.reset(); + } + cancelRendering() { + if (this.renderTask) { + this.renderTask.cancel(); + this.renderTask = null; + } + this.resume = null; + } + #getPageDrawContext(upscaleFactor = 1, enableHWA = this.enableHWA) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d", { + alpha: false, + willReadFrequently: !enableHWA, + }); + const outputScale = new OutputScale(); + canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0; + canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0; + const transform = outputScale.scaled + ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] + : null; + return { + ctx, + canvas, + transform, + }; + } + #convertCanvasToImage(canvas) { + if (this.renderingState !== RenderingStates.FINISHED) { + throw new Error("#convertCanvasToImage: Rendering has not finished."); + } + const reducedCanvas = this.#reduceImage(canvas); + const image = document.createElement("img"); + image.className = "thumbnailImage"; + image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas"); + image.setAttribute("data-l10n-args", this.#pageL10nArgs); + image.src = reducedCanvas.toDataURL(); + this.image = image; + this.div.setAttribute("data-loaded", true); + this._placeholderImg.replaceWith(image); + reducedCanvas.width = 0; + reducedCanvas.height = 0; + } + async #finishRenderTask(renderTask, canvas, error = null) { + if (renderTask === this.renderTask) { + this.renderTask = null; + } + if (error instanceof RenderingCancelledException) { + return; + } + this.renderingState = RenderingStates.FINISHED; + this.#convertCanvasToImage(canvas); + if (error) { + throw error; + } + } + async draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error("Must be in new state before drawing"); + return undefined; + } + const { pdfPage } = this; + if (!pdfPage) { + this.renderingState = RenderingStates.FINISHED; + throw new Error("pdfPage is not loaded"); + } + this.renderingState = RenderingStates.RUNNING; + const { ctx, canvas, transform } = + this.#getPageDrawContext(DRAW_UPSCALE_FACTOR); + const drawViewport = this.viewport.clone({ + scale: DRAW_UPSCALE_FACTOR * this.scale, + }); + const renderContinueCallback = (cont) => { + if (!this.renderingQueue.isHighestPriority(this)) { + this.renderingState = RenderingStates.PAUSED; + this.resume = () => { + this.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + const renderContext = { + canvasContext: ctx, + transform, + viewport: drawViewport, + optionalContentConfigPromise: this._optionalContentConfigPromise, + pageColors: this.pageColors, + }; + const renderTask = (this.renderTask = pdfPage.render(renderContext)); + renderTask.onContinue = renderContinueCallback; + const resultPromise = renderTask.promise.then( + () => this.#finishRenderTask(renderTask, canvas), + (error) => this.#finishRenderTask(renderTask, canvas, error), + ); + resultPromise.finally(() => { + canvas.width = 0; + canvas.height = 0; + this.eventBus.dispatch("thumbnailrendered", { + source: this, + pageNumber: this.id, + pdfPage: this.pdfPage, + }); + }); + return resultPromise; + } + setImage(pageView) { + if (this.renderingState !== RenderingStates.INITIAL) { + return; + } + const { thumbnailCanvas: canvas, pdfPage, scale } = pageView; + if (!canvas) { + return; + } + if (!this.pdfPage) { + this.setPdfPage(pdfPage); + } + if (scale < this.scale) { + return; + } + this.renderingState = RenderingStates.FINISHED; + this.#convertCanvasToImage(canvas); + } + #reduceImage(img) { + const { ctx, canvas } = this.#getPageDrawContext(1, true); + if (img.width <= 2 * canvas.width) { + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + 0, + 0, + canvas.width, + canvas.height, + ); + return canvas; + } + let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; + let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; + const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas( + reducedWidth, + reducedHeight, + ); + while (reducedWidth > img.width || reducedHeight > img.height) { + reducedWidth >>= 1; + reducedHeight >>= 1; + } + reducedImageCtx.drawImage( + img, + 0, + 0, + img.width, + img.height, + 0, + 0, + reducedWidth, + reducedHeight, + ); + while (reducedWidth > 2 * canvas.width) { + reducedImageCtx.drawImage( + reducedImage, + 0, + 0, + reducedWidth, + reducedHeight, + 0, + 0, + reducedWidth >> 1, + reducedHeight >> 1, + ); + reducedWidth >>= 1; + reducedHeight >>= 1; + } + ctx.drawImage( + reducedImage, + 0, + 0, + reducedWidth, + reducedHeight, + 0, + 0, + canvas.width, + canvas.height, + ); + return canvas; + } + get #pageL10nArgs() { + return JSON.stringify({ + page: this.pageLabel ?? this.id, + }); + } + setPageLabel(label) { + this.pageLabel = typeof label === "string" ? label : null; + this.anchor.setAttribute("data-l10n-args", this.#pageL10nArgs); + if (this.renderingState !== RenderingStates.FINISHED) { + return; + } + this.image?.setAttribute("data-l10n-args", this.#pageL10nArgs); + } +} // ./web/pdf_thumbnail_viewer.js + +const THUMBNAIL_SCROLL_MARGIN = -19; +const THUMBNAIL_SELECTED_CLASS = "selected"; +class PDFThumbnailViewer { + constructor({ + container, + eventBus, + linkService, + renderingQueue, + pageColors, + abortSignal, + enableHWA, + }) { + this.container = container; + this.eventBus = eventBus; + this.linkService = linkService; + this.renderingQueue = renderingQueue; + this.pageColors = pageColors || null; + this.enableHWA = enableHWA || false; + this.scroll = watchScroll( + this.container, + this.#scrollUpdated.bind(this), + abortSignal, + ); + this.#resetView(); + } + #scrollUpdated() { + this.renderingQueue.renderHighestPriority(); + } + getThumbnail(index) { + return this._thumbnails[index]; + } + #getVisibleThumbs() { + return getVisibleElements({ + scrollEl: this.container, + views: this._thumbnails, + }); + } + scrollThumbnailIntoView(pageNumber) { + if (!this.pdfDocument) { + return; + } + const thumbnailView = this._thumbnails[pageNumber - 1]; + if (!thumbnailView) { + console.error('scrollThumbnailIntoView: Invalid "pageNumber" parameter.'); + return; + } + if (pageNumber !== this._currentPageNumber) { + const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1]; + prevThumbnailView.div.classList.remove(THUMBNAIL_SELECTED_CLASS); + thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS); + } + const { first, last, views } = this.#getVisibleThumbs(); + if (views.length > 0) { + let shouldScroll = false; + if (pageNumber <= first.id || pageNumber >= last.id) { + shouldScroll = true; + } else { + for (const { id, percent } of views) { + if (id !== pageNumber) { + continue; + } + shouldScroll = percent < 100; + break; + } + } + if (shouldScroll) { + scrollIntoView(thumbnailView.div, { + top: THUMBNAIL_SCROLL_MARGIN, + }); + } + } + this._currentPageNumber = pageNumber; + } + get pagesRotation() { + return this._pagesRotation; + } + set pagesRotation(rotation) { + if (!isValidRotation(rotation)) { + throw new Error("Invalid thumbnails rotation angle."); + } + if (!this.pdfDocument) { + return; + } + if (this._pagesRotation === rotation) { + return; + } + this._pagesRotation = rotation; + const updateArgs = { + rotation, + }; + for (const thumbnail of this._thumbnails) { + thumbnail.update(updateArgs); + } + } + cleanup() { + for (const thumbnail of this._thumbnails) { + if (thumbnail.renderingState !== RenderingStates.FINISHED) { + thumbnail.reset(); + } + } + TempImageFactory.destroyCanvas(); + } + #resetView() { + this._thumbnails = []; + this._currentPageNumber = 1; + this._pageLabels = null; + this._pagesRotation = 0; + this.container.textContent = ""; + } + setDocument(pdfDocument) { + if (this.pdfDocument) { + this.#cancelRendering(); + this.#resetView(); + } + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const firstPagePromise = pdfDocument.getPage(1); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display", + }); + firstPagePromise + .then((firstPdfPage) => { + const pagesCount = pdfDocument.numPages; + const viewport = firstPdfPage.getViewport({ + scale: 1, + }); + for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { + const thumbnail = new PDFThumbnailView({ + container: this.container, + eventBus: this.eventBus, + id: pageNum, + defaultViewport: viewport.clone(), + optionalContentConfigPromise, + linkService: this.linkService, + renderingQueue: this.renderingQueue, + pageColors: this.pageColors, + enableHWA: this.enableHWA, + }); + this._thumbnails.push(thumbnail); + } + this._thumbnails[0]?.setPdfPage(firstPdfPage); + const thumbnailView = this._thumbnails[this._currentPageNumber - 1]; + thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS); + }) + .catch((reason) => { + console.error("Unable to initialize thumbnail viewer", reason); + }); + } + #cancelRendering() { + for (const thumbnail of this._thumbnails) { + thumbnail.cancelRendering(); + } + } + setPageLabels(labels) { + if (!this.pdfDocument) { + return; + } + if (!labels) { + this._pageLabels = null; + } else if ( + !(Array.isArray(labels) && this.pdfDocument.numPages === labels.length) + ) { + this._pageLabels = null; + console.error("PDFThumbnailViewer_setPageLabels: Invalid page labels."); + } else { + this._pageLabels = labels; + } + for (let i = 0, ii = this._thumbnails.length; i < ii; i++) { + this._thumbnails[i].setPageLabel(this._pageLabels?.[i] ?? null); + } + } + async #ensurePdfPageLoaded(thumbView) { + if (thumbView.pdfPage) { + return thumbView.pdfPage; + } + try { + const pdfPage = await this.pdfDocument.getPage(thumbView.id); + if (!thumbView.pdfPage) { + thumbView.setPdfPage(pdfPage); + } + return pdfPage; + } catch (reason) { + console.error("Unable to get page for thumb view", reason); + return null; + } + } + #getScrollAhead(visible) { + if (visible.first?.id === 1) { + return true; + } else if (visible.last?.id === this._thumbnails.length) { + return false; + } + return this.scroll.down; + } + forceRendering() { + const visibleThumbs = this.#getVisibleThumbs(); + const scrollAhead = this.#getScrollAhead(visibleThumbs); + const thumbView = this.renderingQueue.getHighestPriority( + visibleThumbs, + this._thumbnails, + scrollAhead, + ); + if (thumbView) { + this.#ensurePdfPageLoaded(thumbView).then(() => { + this.renderingQueue.renderView(thumbView); + }); + return true; + } + return false; + } +} // ./web/annotation_editor_layer_builder.js + +class AnnotationEditorLayerBuilder { + #annotationLayer = null; + #drawLayer = null; + #onAppend = null; + #structTreeLayer = null; + #textLayer = null; + #uiManager; + constructor(options) { + this.pdfPage = options.pdfPage; + this.accessibilityManager = options.accessibilityManager; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.annotationEditorLayer = null; + this.div = null; + this._cancelled = false; + this.#uiManager = options.uiManager; + this.#annotationLayer = options.annotationLayer || null; + this.#textLayer = options.textLayer || null; + this.#drawLayer = options.drawLayer || null; + this.#onAppend = options.onAppend || null; + this.#structTreeLayer = options.structTreeLayer || null; + } + async render(viewport, intent = "display") { + if (intent !== "display") { + return; + } + if (this._cancelled) { + return; + } + const clonedViewport = viewport.clone({ + dontFlip: true, + }); + if (this.div) { + this.annotationEditorLayer.update({ + viewport: clonedViewport, + }); + this.show(); + return; + } + const div = (this.div = document.createElement("div")); + div.className = "annotationEditorLayer"; + div.hidden = true; + div.dir = this.#uiManager.direction; + this.#onAppend?.(div); + this.annotationEditorLayer = new AnnotationEditorLayer({ + uiManager: this.#uiManager, + div, + structTreeLayer: this.#structTreeLayer, + accessibilityManager: this.accessibilityManager, + pageIndex: this.pdfPage.pageNumber - 1, + l10n: this.l10n, + viewport: clonedViewport, + annotationLayer: this.#annotationLayer, + textLayer: this.#textLayer, + drawLayer: this.#drawLayer, + }); + const parameters = { + viewport: clonedViewport, + div, + annotations: null, + intent, + }; + this.annotationEditorLayer.render(parameters); + this.show(); + } + cancel() { + this._cancelled = true; + if (!this.div) { + return; + } + this.annotationEditorLayer.destroy(); + } + hide() { + if (!this.div) { + return; + } + this.annotationEditorLayer.pause(true); + this.div.hidden = true; + } + show() { + if (!this.div || this.annotationEditorLayer.isInvisible) { + return; + } + this.div.hidden = false; + this.annotationEditorLayer.pause(false); + } +} // ./web/annotation_layer_builder.js + +class AnnotationLayerBuilder { + #onAppend = null; + #eventAbortController = null; + constructor({ + pdfPage, + linkService, + downloadManager, + annotationStorage = null, + imageResourcesPath = "", + renderForms = true, + enableScripting = false, + hasJSActionsPromise = null, + fieldObjectsPromise = null, + annotationCanvasMap = null, + accessibilityManager = null, + annotationEditorUIManager = null, + onAppend = null, + }) { + this.pdfPage = pdfPage; + this.linkService = linkService; + this.downloadManager = downloadManager; + this.imageResourcesPath = imageResourcesPath; + this.renderForms = renderForms; + this.annotationStorage = annotationStorage; + this.enableScripting = enableScripting; + this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false); + this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null); + this._annotationCanvasMap = annotationCanvasMap; + this._accessibilityManager = accessibilityManager; + this._annotationEditorUIManager = annotationEditorUIManager; + this.#onAppend = onAppend; + this.annotationLayer = null; + this.div = null; + this._cancelled = false; + this._eventBus = linkService.eventBus; + } + async render(viewport, options, intent = "display") { + if (this.div) { + if (this._cancelled || !this.annotationLayer) { + return; + } + this.annotationLayer.update({ + viewport: viewport.clone({ + dontFlip: true, + }), + }); + return; + } + const [annotations, hasJSActions, fieldObjects] = await Promise.all([ + this.pdfPage.getAnnotations({ + intent, + }), + this._hasJSActionsPromise, + this._fieldObjectsPromise, + ]); + if (this._cancelled) { + return; + } + const div = (this.div = document.createElement("div")); + div.className = "annotationLayer"; + this.#onAppend?.(div); + if (annotations.length === 0) { + this.hide(); + return; + } + this.annotationLayer = new AnnotationLayer({ + div, + accessibilityManager: this._accessibilityManager, + annotationCanvasMap: this._annotationCanvasMap, + annotationEditorUIManager: this._annotationEditorUIManager, + page: this.pdfPage, + viewport: viewport.clone({ + dontFlip: true, + }), + structTreeLayer: options?.structTreeLayer || null, + }); + await this.annotationLayer.render({ + annotations, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.renderForms, + linkService: this.linkService, + downloadManager: this.downloadManager, + annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, + hasJSActions, + fieldObjects, + }); + if (this.linkService.isInPresentationMode) { + this.#updatePresentationModeState(PresentationModeState.FULLSCREEN); + } + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this._eventBus?._on( + "presentationmodechanged", + (evt) => { + this.#updatePresentationModeState(evt.state); + }, + { + signal: this.#eventAbortController.signal, + }, + ); + } + } + cancel() { + this._cancelled = true; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + } + hide() { + if (!this.div) { + return; + } + this.div.hidden = true; + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { + if (!this.div) { + return; + } + let disableFormElements = false; + switch (state) { + case PresentationModeState.FULLSCREEN: + disableFormElements = true; + break; + case PresentationModeState.NORMAL: + break; + default: + return; + } + for (const section of this.div.childNodes) { + if (section.hasAttribute("data-internal-link")) { + continue; + } + section.inert = disableFormElements; + } + } +} // ./web/draw_layer_builder.js + +class DrawLayerBuilder { + #drawLayer = null; + constructor(options) { + this.pageIndex = options.pageIndex; + } + async render(intent = "display") { + if (intent !== "display" || this.#drawLayer || this._cancelled) { + return; + } + this.#drawLayer = new DrawLayer({ + pageIndex: this.pageIndex, + }); + } + cancel() { + this._cancelled = true; + if (!this.#drawLayer) { + return; + } + this.#drawLayer.destroy(); + this.#drawLayer = null; + } + setParent(parent) { + this.#drawLayer?.setParent(parent); + } + getDrawLayer() { + return this.#drawLayer; + } +} // ./web/struct_tree_layer_builder.js + +const PDF_ROLE_TO_HTML_ROLE = { + Document: null, + DocumentFragment: null, + Part: "group", + Sect: "group", + Div: "group", + Aside: "note", + NonStruct: "none", + P: null, + H: "heading", + Title: null, + FENote: "note", + Sub: "group", + Lbl: null, + Span: null, + Em: null, + Strong: null, + Link: "link", + Annot: "note", + Form: "form", + Ruby: null, + RB: null, + RT: null, + RP: null, + Warichu: null, + WT: null, + WP: null, + L: "list", + LI: "listitem", + LBody: null, + Table: "table", + TR: "row", + TH: "columnheader", + TD: "cell", + THead: "columnheader", + TBody: null, + TFoot: null, + Caption: null, + Figure: "figure", + Formula: null, + Artifact: null, +}; +const HEADING_PATTERN = /^H(\d+)$/; +class StructTreeLayerBuilder { + #promise; + #treeDom = null; + #treePromise; + #elementAttributes = new Map(); + #rawDims; + #elementsToAddToTextLayer = null; + constructor(pdfPage, rawDims) { + this.#promise = pdfPage.getStructTree(); + this.#rawDims = rawDims; + } + async render() { + if (this.#treePromise) { + return this.#treePromise; + } + const { promise, resolve, reject } = Promise.withResolvers(); + this.#treePromise = promise; + try { + this.#treeDom = this.#walk(await this.#promise); + } catch (ex) { + reject(ex); + } + this.#promise = null; + this.#treeDom?.classList.add("structTree"); + resolve(this.#treeDom); + return promise; + } + async getAriaAttributes(annotationId) { + try { + await this.render(); + return this.#elementAttributes.get(annotationId); + } catch {} + return null; + } + hide() { + if (this.#treeDom && !this.#treeDom.hidden) { + this.#treeDom.hidden = true; + } + } + show() { + if (this.#treeDom?.hidden) { + this.#treeDom.hidden = false; + } + } + #setAttributes(structElement, htmlElement) { + const { alt, id, lang } = structElement; + if (alt !== undefined) { + let added = false; + const label = removeNullCharacters(alt); + for (const child of structElement.children) { + if (child.type === "annotation") { + let attrs = this.#elementAttributes.get(child.id); + if (!attrs) { + attrs = new Map(); + this.#elementAttributes.set(child.id, attrs); + } + attrs.set("aria-label", label); + added = true; + } + } + if (!added) { + htmlElement.setAttribute("aria-label", label); + } + } + if (id !== undefined) { + htmlElement.setAttribute("aria-owns", id); + } + if (lang !== undefined) { + htmlElement.setAttribute("lang", removeNullCharacters(lang, true)); + } + } + #addImageInTextLayer(node, element) { + const { alt, bbox, children } = node; + const child = children?.[0]; + if (!this.#rawDims || !alt || !bbox || child?.type !== "content") { + return false; + } + const { id } = child; + if (!id) { + return false; + } + element.setAttribute("aria-owns", id); + const img = document.createElement("span"); + (this.#elementsToAddToTextLayer ||= new Map()).set(id, img); + img.setAttribute("role", "img"); + img.setAttribute("aria-label", removeNullCharacters(alt)); + const { pageHeight, pageX, pageY } = this.#rawDims; + const calc = "calc(var(--scale-factor)*"; + const { style } = img; + style.width = `${calc}${bbox[2] - bbox[0]}px)`; + style.height = `${calc}${bbox[3] - bbox[1]}px)`; + style.left = `${calc}${bbox[0] - pageX}px)`; + style.top = `${calc}${pageHeight - bbox[3] + pageY}px)`; + return true; + } + addElementsToTextLayer() { + if (!this.#elementsToAddToTextLayer) { + return; + } + for (const [id, img] of this.#elementsToAddToTextLayer) { + document.getElementById(id)?.append(img); + } + this.#elementsToAddToTextLayer.clear(); + this.#elementsToAddToTextLayer = null; + } + #walk(node) { + if (!node) { + return null; + } + const element = document.createElement("span"); + if ("role" in node) { + const { role } = node; + const match = role.match(HEADING_PATTERN); + if (match) { + element.setAttribute("role", "heading"); + element.setAttribute("aria-level", match[1]); + } else if (PDF_ROLE_TO_HTML_ROLE[role]) { + element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]); + } + if (role === "Figure" && this.#addImageInTextLayer(node, element)) { + return element; + } + } + this.#setAttributes(node, element); + if (node.children) { + if (node.children.length === 1 && "id" in node.children[0]) { + this.#setAttributes(node.children[0], element); + } else { + for (const kid of node.children) { + element.append(this.#walk(kid)); + } + } + } + return element; + } +} // ./web/text_accessibility.js + +class TextAccessibilityManager { + #enabled = false; + #textChildren = null; + #textNodes = new Map(); + #waitingElements = new Map(); + setTextMapping(textDivs) { + this.#textChildren = textDivs; + } + static #compareElementPositions(e1, e2) { + const rect1 = e1.getBoundingClientRect(); + const rect2 = e2.getBoundingClientRect(); + if (rect1.width === 0 && rect1.height === 0) { + return +1; + } + if (rect2.width === 0 && rect2.height === 0) { + return -1; + } + const top1 = rect1.y; + const bot1 = rect1.y + rect1.height; + const mid1 = rect1.y + rect1.height / 2; + const top2 = rect2.y; + const bot2 = rect2.y + rect2.height; + const mid2 = rect2.y + rect2.height / 2; + if (mid1 <= top2 && mid2 >= bot1) { + return -1; + } + if (mid2 <= top1 && mid1 >= bot2) { + return +1; + } + const centerX1 = rect1.x + rect1.width / 2; + const centerX2 = rect2.x + rect2.width / 2; + return centerX1 - centerX2; + } + enable() { + if (this.#enabled) { + throw new Error("TextAccessibilityManager is already enabled."); + } + if (!this.#textChildren) { + throw new Error("Text divs and strings have not been set."); + } + this.#enabled = true; + this.#textChildren = this.#textChildren.slice(); + this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions); + if (this.#textNodes.size > 0) { + const textChildren = this.#textChildren; + for (const [id, nodeIndex] of this.#textNodes) { + const element = document.getElementById(id); + if (!element) { + this.#textNodes.delete(id); + continue; + } + this.#addIdToAriaOwns(id, textChildren[nodeIndex]); + } + } + for (const [element, isRemovable] of this.#waitingElements) { + this.addPointerInTextLayer(element, isRemovable); + } + this.#waitingElements.clear(); + } + disable() { + if (!this.#enabled) { + return; + } + this.#waitingElements.clear(); + this.#textChildren = null; + this.#enabled = false; + } + removePointerInTextLayer(element) { + if (!this.#enabled) { + this.#waitingElements.delete(element); + return; + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return; + } + const { id } = element; + const nodeIndex = this.#textNodes.get(id); + if (nodeIndex === undefined) { + return; + } + const node = children[nodeIndex]; + this.#textNodes.delete(id); + let owns = node.getAttribute("aria-owns"); + if (owns?.includes(id)) { + owns = owns + .split(" ") + .filter((x) => x !== id) + .join(" "); + if (owns) { + node.setAttribute("aria-owns", owns); + } else { + node.removeAttribute("aria-owns"); + node.setAttribute("role", "presentation"); + } + } + } + #addIdToAriaOwns(id, node) { + const owns = node.getAttribute("aria-owns"); + if (!owns?.includes(id)) { + node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id); + } + node.removeAttribute("role"); + } + addPointerInTextLayer(element, isRemovable) { + const { id } = element; + if (!id) { + return null; + } + if (!this.#enabled) { + this.#waitingElements.set(element, isRemovable); + return null; + } + if (isRemovable) { + this.removePointerInTextLayer(element); + } + const children = this.#textChildren; + if (!children || children.length === 0) { + return null; + } + const index = binarySearchFirstItem( + children, + (node) => + TextAccessibilityManager.#compareElementPositions(element, node) < 0, + ); + const nodeIndex = Math.max(0, index - 1); + const child = children[nodeIndex]; + this.#addIdToAriaOwns(id, child); + this.#textNodes.set(id, nodeIndex); + const parent = child.parentNode; + return parent?.classList.contains("markedContent") ? parent.id : null; + } + moveElementInDOM(container, element, contentElement, isRemovable) { + const id = this.addPointerInTextLayer(contentElement, isRemovable); + if (!container.hasChildNodes()) { + container.append(element); + return id; + } + const children = Array.from(container.childNodes).filter( + (node) => node !== element, + ); + if (children.length === 0) { + return id; + } + const elementToCompare = contentElement || element; + const index = binarySearchFirstItem( + children, + (node) => + TextAccessibilityManager.#compareElementPositions( + elementToCompare, + node, + ) < 0, + ); + if (index === 0) { + children[0].before(element); + } else { + children[index - 1].after(element); + } + return id; + } +} // ./web/text_highlighter.js + +class TextHighlighter { + #eventAbortController = null; + constructor({ findController, eventBus, pageIndex }) { + this.findController = findController; + this.matches = []; + this.eventBus = eventBus; + this.pageIdx = pageIndex; + this.textDivs = null; + this.textContentItemsStr = null; + this.enabled = false; + } + setTextMapping(divs, texts) { + this.textDivs = divs; + this.textContentItemsStr = texts; + } + enable() { + if (!this.textDivs || !this.textContentItemsStr) { + throw new Error("Text divs and strings have not been set."); + } + if (this.enabled) { + throw new Error("TextHighlighter is already enabled."); + } + this.enabled = true; + if (!this.#eventAbortController) { + this.#eventAbortController = new AbortController(); + this.eventBus._on( + "updatetextlayermatches", + (evt) => { + if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) { + this._updateMatches(); + } + }, + { + signal: this.#eventAbortController.signal, + }, + ); + } + this._updateMatches(); + } + disable() { + if (!this.enabled) { + return; + } + this.enabled = false; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this._updateMatches(true); + } + _convertMatches(matches, matchesLength) { + if (!matches) { + return []; + } + const { textContentItemsStr } = this; + let i = 0, + iIndex = 0; + const end = textContentItemsStr.length - 1; + const result = []; + for (let m = 0, mm = matches.length; m < mm; m++) { + let matchIdx = matches[m]; + while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + if (i === textContentItemsStr.length) { + console.error("Could not find a matching mapping"); + } + const match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex, + }, + }; + matchIdx += matchesLength[m]; + while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) { + iIndex += textContentItemsStr[i].length; + i++; + } + match.end = { + divIdx: i, + offset: matchIdx - iIndex, + }; + result.push(match); + } + return result; + } + _renderMatches(matches) { + if (matches.length === 0) { + return; + } + const { findController, pageIdx } = this; + const { textContentItemsStr, textDivs } = this; + const isSelectedPage = pageIdx === findController.selected.pageIdx; + const selectedMatchIdx = findController.selected.matchIdx; + const highlightAll = findController.state.highlightAll; + let prevEnd = null; + const infinity = { + divIdx: -1, + offset: undefined, + }; + function beginText(begin, className) { + const divIdx = begin.divIdx; + textDivs[divIdx].textContent = ""; + return appendTextToDiv(divIdx, 0, begin.offset, className); + } + function appendTextToDiv(divIdx, fromOffset, toOffset, className) { + let div = textDivs[divIdx]; + if (div.nodeType === Node.TEXT_NODE) { + const span = document.createElement("span"); + div.before(span); + span.append(div); + textDivs[divIdx] = span; + div = span; + } + const content = textContentItemsStr[divIdx].substring( + fromOffset, + toOffset, + ); + const node = document.createTextNode(content); + if (className) { + const span = document.createElement("span"); + span.className = `${className} appended`; + span.append(node); + div.append(span); + if (className.includes("selected")) { + const { left } = span.getClientRects()[0]; + const parentLeft = div.getBoundingClientRect().left; + return left - parentLeft; + } + return 0; + } + div.append(node); + return 0; + } + let i0 = selectedMatchIdx, + i1 = i0 + 1; + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + return; + } + let lastDivIdx = -1; + let lastOffset = -1; + for (let i = i0; i < i1; i++) { + const match = matches[i]; + const begin = match.begin; + if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) { + continue; + } + lastDivIdx = begin.divIdx; + lastOffset = begin.offset; + const end = match.end; + const isSelected = isSelectedPage && i === selectedMatchIdx; + const highlightSuffix = isSelected ? " selected" : ""; + let selectedLeft = 0; + if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { + if (prevEnd !== null) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + beginText(begin); + } else { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); + } + if (begin.divIdx === end.divIdx) { + selectedLeft = appendTextToDiv( + begin.divIdx, + begin.offset, + end.offset, + "highlight" + highlightSuffix, + ); + } else { + selectedLeft = appendTextToDiv( + begin.divIdx, + begin.offset, + infinity.offset, + "highlight begin" + highlightSuffix, + ); + for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { + textDivs[n0].className = "highlight middle" + highlightSuffix; + } + beginText(end, "highlight end" + highlightSuffix); + } + prevEnd = end; + if (isSelected) { + findController.scrollMatchIntoView({ + element: textDivs[begin.divIdx], + selectedLeft, + pageIndex: pageIdx, + matchIndex: selectedMatchIdx, + }); + } + } + if (prevEnd) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + } + _updateMatches(reset = false) { + if (!this.enabled && !reset) { + return; + } + const { findController, matches, pageIdx } = this; + const { textContentItemsStr, textDivs } = this; + let clearedUntilDivIdx = -1; + for (const match of matches) { + const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (let n = begin, end = match.end.divIdx; n <= end; n++) { + const div = textDivs[n]; + div.textContent = textContentItemsStr[n]; + div.className = ""; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } + if (!findController?.highlightMatches || reset) { + return; + } + const pageMatches = findController.pageMatches[pageIdx] || null; + const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null; + this.matches = this._convertMatches(pageMatches, pageMatchesLength); + this._renderMatches(this.matches); + } +} // ./web/text_layer_builder.js + +class TextLayerBuilder { + #enablePermissions = false; + #onAppend = null; + #renderingDone = false; + #textLayer = null; + static #textLayers = new Map(); + static #selectionChangeAbortController = null; + constructor({ + pdfPage, + highlighter = null, + accessibilityManager = null, + enablePermissions = false, + onAppend = null, + }) { + this.pdfPage = pdfPage; + this.highlighter = highlighter; + this.accessibilityManager = accessibilityManager; + this.#enablePermissions = enablePermissions === true; + this.#onAppend = onAppend; + this.div = document.createElement("div"); + this.div.tabIndex = 0; + this.div.className = "textLayer"; + } + async render(viewport, textContentParams = null) { + if (this.#renderingDone && this.#textLayer) { + this.#textLayer.update({ + viewport, + onBefore: this.hide.bind(this), + }); + this.show(); + return; + } + this.cancel(); + this.#textLayer = new TextLayer({ + textContentSource: this.pdfPage.streamTextContent( + textContentParams || { + includeMarkedContent: true, + disableNormalization: true, + }, + ), + container: this.div, + viewport, + }); + const { textDivs, textContentItemsStr } = this.#textLayer; + this.highlighter?.setTextMapping(textDivs, textContentItemsStr); + this.accessibilityManager?.setTextMapping(textDivs); + await this.#textLayer.render(); + this.#renderingDone = true; + const endOfContent = document.createElement("div"); + endOfContent.className = "endOfContent"; + this.div.append(endOfContent); + this.#bindMouse(endOfContent); + this.#onAppend?.(this.div); + this.highlighter?.enable(); + this.accessibilityManager?.enable(); + } + hide() { + if (!this.div.hidden && this.#renderingDone) { + this.highlighter?.disable(); + this.div.hidden = true; + } + } + show() { + if (this.div.hidden && this.#renderingDone) { + this.div.hidden = false; + this.highlighter?.enable(); + } + } + cancel() { + this.#textLayer?.cancel(); + this.#textLayer = null; + this.highlighter?.disable(); + this.accessibilityManager?.disable(); + TextLayerBuilder.#removeGlobalSelectionListener(this.div); + } + #bindMouse(end) { + const { div } = this; + div.addEventListener("mousedown", () => { + div.classList.add("selecting"); + }); + div.addEventListener("copy", (event) => { + if (!this.#enablePermissions) { + const selection = document.getSelection(); + event.clipboardData.setData( + "text/plain", + removeNullCharacters(normalizeUnicode(selection.toString())), + ); + } + stopEvent(event); + }); + TextLayerBuilder.#textLayers.set(div, end); + TextLayerBuilder.#enableGlobalSelectionListener(); + } + static #removeGlobalSelectionListener(textLayerDiv) { + this.#textLayers.delete(textLayerDiv); + if (this.#textLayers.size === 0) { + this.#selectionChangeAbortController?.abort(); + this.#selectionChangeAbortController = null; + } + } + static #enableGlobalSelectionListener() { + if (this.#selectionChangeAbortController) { + return; + } + this.#selectionChangeAbortController = new AbortController(); + const { signal } = this.#selectionChangeAbortController; + const reset = (end, textLayer) => { + textLayer.append(end); + end.style.width = ""; + end.style.height = ""; + textLayer.classList.remove("selecting"); + }; + let isPointerDown = false; + document.addEventListener( + "pointerdown", + () => { + isPointerDown = true; + }, + { + signal, + }, + ); + document.addEventListener( + "pointerup", + () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, + { + signal, + }, + ); + window.addEventListener( + "blur", + () => { + isPointerDown = false; + this.#textLayers.forEach(reset); + }, + { + signal, + }, + ); + document.addEventListener( + "keyup", + () => { + if (!isPointerDown) { + this.#textLayers.forEach(reset); + } + }, + { + signal, + }, + ); + var isFirefox, prevRange; + document.addEventListener( + "selectionchange", + () => { + const selection = document.getSelection(); + if (selection.rangeCount === 0) { + this.#textLayers.forEach(reset); + return; + } + const activeTextLayers = new Set(); + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + for (const textLayerDiv of this.#textLayers.keys()) { + if ( + !activeTextLayers.has(textLayerDiv) && + range.intersectsNode(textLayerDiv) + ) { + activeTextLayers.add(textLayerDiv); + } + } + } + for (const [textLayerDiv, endDiv] of this.#textLayers) { + if (activeTextLayers.has(textLayerDiv)) { + textLayerDiv.classList.add("selecting"); + } else { + reset(endDiv, textLayerDiv); + } + } + isFirefox ??= + getComputedStyle( + this.#textLayers.values().next().value, + ).getPropertyValue("-moz-user-select") === "none"; + if (isFirefox) { + return; + } + const range = selection.getRangeAt(0); + const modifyStart = + prevRange && + (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || + range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0); + let anchor = modifyStart ? range.startContainer : range.endContainer; + if (anchor.nodeType === Node.TEXT_NODE) { + anchor = anchor.parentNode; + } + const parentTextLayer = anchor.parentElement?.closest(".textLayer"); + const endDiv = this.#textLayers.get(parentTextLayer); + if (endDiv) { + endDiv.style.width = parentTextLayer.style.width; + endDiv.style.height = parentTextLayer.style.height; + anchor.parentElement.insertBefore( + endDiv, + modifyStart ? anchor : anchor.nextSibling, + ); + } + prevRange = range.cloneRange(); + }, + { + signal, + }, + ); + } +} // ./web/pdf_page_view.js + +const DEFAULT_LAYER_PROPERTIES = null; +const LAYERS_ORDER = new Map([ + ["canvasWrapper", 0], + ["textLayer", 1], + ["annotationLayer", 2], + ["annotationEditorLayer", 3], + ["xfaLayer", 3], +]); +class PDFPageView { + #annotationMode = AnnotationMode.ENABLE_FORMS; + #canvasWrapper = null; + #enableHWA = false; + #hasRestrictedScaling = false; + #isEditing = false; + #layerProperties = null; + #loadingId = null; + #originalViewport = null; + #previousRotation = null; + #scaleRoundX = 1; + #scaleRoundY = 1; + #renderError = null; + #renderingState = RenderingStates.INITIAL; + #textLayerMode = TextLayerMode.ENABLE; + #useThumbnailCanvas = { + directDrawing: true, + initialOptionalContent: true, + regularAnnotations: true, + }; + #layers = [null, null, null, null]; + constructor(options) { + const container = options.container; + const defaultViewport = options.defaultViewport; + this.id = options.id; + this.renderingId = "page" + this.id; + this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES; + this.pdfPage = null; + this.pageLabel = null; + this.rotation = 0; + this.scale = options.scale || DEFAULT_SCALE; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this._optionalContentConfigPromise = + options.optionalContentConfigPromise || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = + options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.maxCanvasPixels = + options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); + this.pageColors = options.pageColors || null; + this.#enableHWA = options.enableHWA || false; + this.eventBus = options.eventBus; + this.renderingQueue = options.renderingQueue; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.renderTask = null; + this.resume = null; + this._isStandalone = !this.renderingQueue?.hasViewer(); + this._container = container; + this._annotationCanvasMap = null; + this.annotationLayer = null; + this.annotationEditorLayer = null; + this.textLayer = null; + this.xfaLayer = null; + this.structTreeLayer = null; + this.drawLayer = null; + const div = document.createElement("div"); + div.className = "page"; + div.setAttribute("data-page-number", this.id); + div.setAttribute("role", "region"); + div.setAttribute("data-l10n-id", "pdfjs-page-landmark"); + div.setAttribute( + "data-l10n-args", + JSON.stringify({ + page: this.id, + }), + ); + this.div = div; + this.#setDimensions(); + container?.append(div); + if (this._isStandalone) { + container?.style.setProperty( + "--scale-factor", + this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + ); + if (this.pageColors?.background) { + container?.style.setProperty( + "--page-bg-color", + this.pageColors.background, + ); + } + const { optionalContentConfigPromise } = options; + if (optionalContentConfigPromise) { + optionalContentConfigPromise.then((optionalContentConfig) => { + if ( + optionalContentConfigPromise !== this._optionalContentConfigPromise + ) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = + optionalContentConfig.hasInitialVisibility; + }); + } + if (!options.l10n) { + this.l10n.translate(this.div); + } + } + } + #addLayer(div, name) { + const pos = LAYERS_ORDER.get(name); + const oldDiv = this.#layers[pos]; + this.#layers[pos] = div; + if (oldDiv) { + oldDiv.replaceWith(div); + return; + } + for (let i = pos - 1; i >= 0; i--) { + const layer = this.#layers[i]; + if (layer) { + layer.after(div); + return; + } + } + this.div.prepend(div); + } + get renderingState() { + return this.#renderingState; + } + set renderingState(state) { + if (state === this.#renderingState) { + return; + } + this.#renderingState = state; + if (this.#loadingId) { + clearTimeout(this.#loadingId); + this.#loadingId = null; + } + switch (state) { + case RenderingStates.PAUSED: + this.div.classList.remove("loading"); + break; + case RenderingStates.RUNNING: + this.div.classList.add("loadingIcon"); + this.#loadingId = setTimeout(() => { + this.div.classList.add("loading"); + this.#loadingId = null; + }, 0); + break; + case RenderingStates.INITIAL: + case RenderingStates.FINISHED: + this.div.classList.remove("loadingIcon", "loading"); + break; + } + } + #setDimensions() { + const { viewport } = this; + if (this.pdfPage) { + if (this.#previousRotation === viewport.rotation) { + return; + } + this.#previousRotation = viewport.rotation; + } + setLayerDimensions(this.div, viewport, true, false); + } + setPdfPage(pdfPage) { + if ( + this._isStandalone && + (this.pageColors?.foreground === "CanvasText" || + this.pageColors?.background === "Canvas") + ) { + this._container?.style.setProperty( + "--hcm-highlight-filter", + pdfPage.filterFactory.addHighlightHCMFilter( + "highlight", + "CanvasText", + "Canvas", + "HighlightText", + "Highlight", + ), + ); + this._container?.style.setProperty( + "--hcm-highlight-selected-filter", + pdfPage.filterFactory.addHighlightHCMFilter( + "highlight_selected", + "CanvasText", + "Canvas", + "HighlightText", + "Highlight", + ), + ); + } + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation, + }); + this.#setDimensions(); + this.reset(); + } + destroy() { + this.reset(); + this.pdfPage?.cleanup(); + } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + get _textHighlighter() { + return shadow( + this, + "_textHighlighter", + new TextHighlighter({ + pageIndex: this.id - 1, + eventBus: this.eventBus, + findController: this.#layerProperties.findController, + }), + ); + } + #dispatchLayerRendered(name, error) { + this.eventBus.dispatch(name, { + source: this, + pageNumber: this.id, + error, + }); + } + async #renderAnnotationLayer() { + let error = null; + try { + await this.annotationLayer.render( + this.viewport, + { + structTreeLayer: this.structTreeLayer, + }, + "display", + ); + } catch (ex) { + console.error("#renderAnnotationLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationlayerrendered", error); + } + } + async #renderAnnotationEditorLayer() { + let error = null; + try { + await this.annotationEditorLayer.render(this.viewport, "display"); + } catch (ex) { + console.error("#renderAnnotationEditorLayer:", ex); + error = ex; + } finally { + this.#dispatchLayerRendered("annotationeditorlayerrendered", error); + } + } + async #renderDrawLayer() { + try { + await this.drawLayer.render("display"); + } catch (ex) { + console.error("#renderDrawLayer:", ex); + } + } + async #renderXfaLayer() { + let error = null; + try { + const result = await this.xfaLayer.render(this.viewport, "display"); + if (result?.textDivs && this._textHighlighter) { + this.#buildXfaTextContentItems(result.textDivs); + } + } catch (ex) { + console.error("#renderXfaLayer:", ex); + error = ex; + } finally { + if (this.xfaLayer?.div) { + this.l10n.pause(); + this.#addLayer(this.xfaLayer.div, "xfaLayer"); + this.l10n.resume(); + } + this.#dispatchLayerRendered("xfalayerrendered", error); + } + } + async #renderTextLayer() { + if (!this.textLayer) { + return; + } + let error = null; + try { + await this.textLayer.render(this.viewport); + } catch (ex) { + if (ex instanceof AbortException) { + return; + } + console.error("#renderTextLayer:", ex); + error = ex; + } + this.#dispatchLayerRendered("textlayerrendered", error); + this.#renderStructTreeLayer(); + } + async #renderStructTreeLayer() { + if (!this.textLayer) { + return; + } + const treeDom = await this.structTreeLayer?.render(); + if (treeDom) { + this.l10n.pause(); + this.structTreeLayer?.addElementsToTextLayer(); + if (this.canvas && treeDom.parentNode !== this.canvas) { + this.canvas.append(treeDom); + } + this.l10n.resume(); + } + this.structTreeLayer?.show(); + } + async #buildXfaTextContentItems(textDivs) { + const text = await this.pdfPage.getTextContent(); + const items = []; + for (const item of text.items) { + items.push(item.str); + } + this._textHighlighter.setTextMapping(textDivs, items); + this._textHighlighter.enable(); + } + #resetCanvas() { + const { canvas } = this; + if (!canvas) { + return; + } + canvas.remove(); + canvas.width = canvas.height = 0; + this.canvas = null; + this.#originalViewport = null; + } + reset({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + keepCanvasWrapper = false, + } = {}) { + this.cancelRendering({ + keepAnnotationLayer, + keepAnnotationEditorLayer, + keepXfaLayer, + keepTextLayer, + }); + this.renderingState = RenderingStates.INITIAL; + const div = this.div; + const childNodes = div.childNodes, + annotationLayerNode = + (keepAnnotationLayer && this.annotationLayer?.div) || null, + annotationEditorLayerNode = + (keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null, + xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null, + textLayerNode = (keepTextLayer && this.textLayer?.div) || null, + canvasWrapperNode = (keepCanvasWrapper && this.#canvasWrapper) || null; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + switch (node) { + case annotationLayerNode: + case annotationEditorLayerNode: + case xfaLayerNode: + case textLayerNode: + case canvasWrapperNode: + continue; + } + node.remove(); + const layerIndex = this.#layers.indexOf(node); + if (layerIndex >= 0) { + this.#layers[layerIndex] = null; + } + } + div.removeAttribute("data-loaded"); + if (annotationLayerNode) { + this.annotationLayer.hide(); + } + if (annotationEditorLayerNode) { + this.annotationEditorLayer.hide(); + } + if (xfaLayerNode) { + this.xfaLayer.hide(); + } + if (textLayerNode) { + this.textLayer.hide(); + } + this.structTreeLayer?.hide(); + if (!keepCanvasWrapper && this.#canvasWrapper) { + this.#canvasWrapper = null; + this.#resetCanvas(); + } + } + toggleEditingMode(isEditing) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#isEditing = isEditing; + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true, + }); + } + update({ + scale = 0, + rotation = null, + optionalContentConfigPromise = null, + drawingDelay = -1, + }) { + this.scale = scale || this.scale; + if (typeof rotation === "number") { + this.rotation = rotation; + } + if (optionalContentConfigPromise instanceof Promise) { + this._optionalContentConfigPromise = optionalContentConfigPromise; + optionalContentConfigPromise.then((optionalContentConfig) => { + if ( + optionalContentConfigPromise !== this._optionalContentConfigPromise + ) { + return; + } + this.#useThumbnailCanvas.initialOptionalContent = + optionalContentConfig.hasInitialVisibility; + }); + } + this.#useThumbnailCanvas.directDrawing = true; + const totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: totalRotation, + }); + this.#setDimensions(); + if (this._isStandalone) { + this._container?.style.setProperty("--scale-factor", this.viewport.scale); + } + if (this.canvas) { + let onlyCssZoom = false; + if (this.#hasRestrictedScaling) { + if (this.maxCanvasPixels === 0) { + onlyCssZoom = true; + } else if (this.maxCanvasPixels > 0) { + const { width, height } = this.viewport; + const { sx, sy } = this.outputScale; + onlyCssZoom = + ((Math.floor(width) * sx) | 0) * ((Math.floor(height) * sy) | 0) > + this.maxCanvasPixels; + } + } + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + if (postponeDrawing || onlyCssZoom) { + if ( + postponeDrawing && + !onlyCssZoom && + this.renderingState !== RenderingStates.FINISHED + ) { + this.cancelRendering({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + cancelExtraDelay: drawingDelay, + }); + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.directDrawing = false; + } + this.cssTransform({ + redrawAnnotationLayer: true, + redrawAnnotationEditorLayer: true, + redrawXfaLayer: true, + redrawTextLayer: !postponeDrawing, + hideTextLayer: postponeDrawing, + }); + if (postponeDrawing) { + return; + } + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: true, + timestamp: performance.now(), + error: this.#renderError, + }); + return; + } + } + this.cssTransform({}); + this.reset({ + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + keepCanvasWrapper: true, + }); + } + cancelRendering({ + keepAnnotationLayer = false, + keepAnnotationEditorLayer = false, + keepXfaLayer = false, + keepTextLayer = false, + cancelExtraDelay = 0, + } = {}) { + if (this.renderTask) { + this.renderTask.cancel(cancelExtraDelay); + this.renderTask = null; + } + this.resume = null; + if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) { + this.textLayer.cancel(); + this.textLayer = null; + } + if ( + this.annotationLayer && + (!keepAnnotationLayer || !this.annotationLayer.div) + ) { + this.annotationLayer.cancel(); + this.annotationLayer = null; + this._annotationCanvasMap = null; + } + if (this.structTreeLayer && !this.textLayer) { + this.structTreeLayer = null; + } + if ( + this.annotationEditorLayer && + (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div) + ) { + if (this.drawLayer) { + this.drawLayer.cancel(); + this.drawLayer = null; + } + this.annotationEditorLayer.cancel(); + this.annotationEditorLayer = null; + } + if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) { + this.xfaLayer.cancel(); + this.xfaLayer = null; + this._textHighlighter?.disable(); + } + } + cssTransform({ + redrawAnnotationLayer = false, + redrawAnnotationEditorLayer = false, + redrawXfaLayer = false, + redrawTextLayer = false, + hideTextLayer = false, + }) { + const { canvas } = this; + if (!canvas) { + return; + } + const originalViewport = this.#originalViewport; + if (this.viewport !== originalViewport) { + const relativeRotation = + (360 + this.viewport.rotation - originalViewport.rotation) % 360; + if (relativeRotation === 90 || relativeRotation === 270) { + const { width, height } = this.viewport; + const scaleX = height / width; + const scaleY = width / height; + canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`; + } else { + canvas.style.transform = + relativeRotation === 0 ? "" : `rotate(${relativeRotation}deg)`; + } + } + if (redrawAnnotationLayer && this.annotationLayer) { + this.#renderAnnotationLayer(); + } + if (redrawAnnotationEditorLayer && this.annotationEditorLayer) { + if (this.drawLayer) { + this.#renderDrawLayer(); + } + this.#renderAnnotationEditorLayer(); + } + if (redrawXfaLayer && this.xfaLayer) { + this.#renderXfaLayer(); + } + if (this.textLayer) { + if (hideTextLayer) { + this.textLayer.hide(); + this.structTreeLayer?.hide(); + } else if (redrawTextLayer) { + this.#renderTextLayer(); + } + } + } + get width() { + return this.viewport.width; + } + get height() { + return this.viewport.height; + } + getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + } + async #finishRenderTask(renderTask, error = null) { + if (renderTask === this.renderTask) { + this.renderTask = null; + } + if (error instanceof RenderingCancelledException) { + this.#renderError = null; + return; + } + this.#renderError = error; + this.renderingState = RenderingStates.FINISHED; + this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots; + this.eventBus.dispatch("pagerendered", { + source: this, + pageNumber: this.id, + cssTransform: false, + timestamp: performance.now(), + error: this.#renderError, + }); + if (error) { + throw error; + } + } + async draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error("Must be in new state before drawing"); + this.reset(); + } + const { div, l10n, pageColors, pdfPage, viewport } = this; + if (!pdfPage) { + this.renderingState = RenderingStates.FINISHED; + throw new Error("pdfPage is not loaded"); + } + this.renderingState = RenderingStates.RUNNING; + let canvasWrapper = this.#canvasWrapper; + if (!canvasWrapper) { + canvasWrapper = this.#canvasWrapper = document.createElement("div"); + canvasWrapper.classList.add("canvasWrapper"); + this.#addLayer(canvasWrapper, "canvasWrapper"); + } + if ( + !this.textLayer && + this.#textLayerMode !== TextLayerMode.DISABLE && + !pdfPage.isPureXfa + ) { + this._accessibilityManager ||= new TextAccessibilityManager(); + this.textLayer = new TextLayerBuilder({ + pdfPage, + highlighter: this._textHighlighter, + accessibilityManager: this._accessibilityManager, + enablePermissions: + this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS, + onAppend: (textLayerDiv) => { + this.l10n.pause(); + this.#addLayer(textLayerDiv, "textLayer"); + this.l10n.resume(); + }, + }); + } + if ( + !this.annotationLayer && + this.#annotationMode !== AnnotationMode.DISABLE + ) { + const { + annotationStorage, + annotationEditorUIManager, + downloadManager, + enableScripting, + fieldObjectsPromise, + hasJSActionsPromise, + linkService, + } = this.#layerProperties; + this._annotationCanvasMap ||= new Map(); + this.annotationLayer = new AnnotationLayerBuilder({ + pdfPage, + annotationStorage, + imageResourcesPath: this.imageResourcesPath, + renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS, + linkService, + downloadManager, + enableScripting, + hasJSActionsPromise, + fieldObjectsPromise, + annotationCanvasMap: this._annotationCanvasMap, + accessibilityManager: this._accessibilityManager, + annotationEditorUIManager, + onAppend: (annotationLayerDiv) => { + this.#addLayer(annotationLayerDiv, "annotationLayer"); + }, + }); + } + const renderContinueCallback = (cont) => { + showCanvas?.(false); + if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) { + this.renderingState = RenderingStates.PAUSED; + this.resume = () => { + this.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + const { width, height } = viewport; + const canvas = document.createElement("canvas"); + canvas.setAttribute("role", "presentation"); + const hasHCM = !!(pageColors?.background && pageColors?.foreground); + const prevCanvas = this.canvas; + const updateOnFirstShow = !prevCanvas && !hasHCM; + this.canvas = canvas; + this.#originalViewport = viewport; + let showCanvas = (isLastShow) => { + if (updateOnFirstShow) { + canvasWrapper.prepend(canvas); + showCanvas = null; + return; + } + if (!isLastShow) { + return; + } + if (prevCanvas) { + prevCanvas.replaceWith(canvas); + prevCanvas.width = prevCanvas.height = 0; + } else { + canvasWrapper.prepend(canvas); + } + showCanvas = null; + }; + const ctx = canvas.getContext("2d", { + alpha: false, + willReadFrequently: !this.#enableHWA, + }); + const outputScale = (this.outputScale = new OutputScale()); + if (this.maxCanvasPixels === 0) { + const invScale = 1 / this.scale; + outputScale.sx *= invScale; + outputScale.sy *= invScale; + this.#hasRestrictedScaling = true; + } else if (this.maxCanvasPixels > 0) { + const pixelsInViewport = width * height; + const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + this.#hasRestrictedScaling = true; + } else { + this.#hasRestrictedScaling = false; + } + } + const sfx = approximateFraction(outputScale.sx); + const sfy = approximateFraction(outputScale.sy); + const canvasWidth = (canvas.width = floorToDivide( + calcRound(width * outputScale.sx), + sfx[0], + )); + const canvasHeight = (canvas.height = floorToDivide( + calcRound(height * outputScale.sy), + sfy[0], + )); + const pageWidth = floorToDivide(calcRound(width), sfx[1]); + const pageHeight = floorToDivide(calcRound(height), sfy[1]); + outputScale.sx = canvasWidth / pageWidth; + outputScale.sy = canvasHeight / pageHeight; + if (this.#scaleRoundX !== sfx[1]) { + div.style.setProperty("--scale-round-x", `${sfx[1]}px`); + this.#scaleRoundX = sfx[1]; + } + if (this.#scaleRoundY !== sfy[1]) { + div.style.setProperty("--scale-round-y", `${sfy[1]}px`); + this.#scaleRoundY = sfy[1]; + } + const transform = outputScale.scaled + ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] + : null; + const renderContext = { + canvasContext: ctx, + transform, + viewport, + annotationMode: this.#annotationMode, + optionalContentConfigPromise: this._optionalContentConfigPromise, + annotationCanvasMap: this._annotationCanvasMap, + pageColors, + isEditing: this.#isEditing, + }; + const renderTask = (this.renderTask = pdfPage.render(renderContext)); + renderTask.onContinue = renderContinueCallback; + const resultPromise = renderTask.promise.then( + async () => { + showCanvas?.(true); + await this.#finishRenderTask(renderTask); + this.structTreeLayer ||= new StructTreeLayerBuilder( + pdfPage, + viewport.rawDims, + ); + this.#renderTextLayer(); + if (this.annotationLayer) { + await this.#renderAnnotationLayer(); + } + const { annotationEditorUIManager } = this.#layerProperties; + if (!annotationEditorUIManager) { + return; + } + this.drawLayer ||= new DrawLayerBuilder({ + pageIndex: this.id, + }); + await this.#renderDrawLayer(); + this.drawLayer.setParent(canvasWrapper); + this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({ + uiManager: annotationEditorUIManager, + pdfPage, + l10n, + structTreeLayer: this.structTreeLayer, + accessibilityManager: this._accessibilityManager, + annotationLayer: this.annotationLayer?.annotationLayer, + textLayer: this.textLayer, + drawLayer: this.drawLayer.getDrawLayer(), + onAppend: (annotationEditorLayerDiv) => { + this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer"); + }, + }); + this.#renderAnnotationEditorLayer(); + }, + (error) => { + if (!(error instanceof RenderingCancelledException)) { + showCanvas?.(true); + } else { + prevCanvas?.remove(); + this.#resetCanvas(); + } + return this.#finishRenderTask(renderTask, error); + }, + ); + if (pdfPage.isPureXfa) { + if (!this.xfaLayer) { + const { annotationStorage, linkService } = this.#layerProperties; + this.xfaLayer = new XfaLayerBuilder({ + pdfPage, + annotationStorage, + linkService, + }); + } + this.#renderXfaLayer(); + } + div.setAttribute("data-loaded", true); + this.eventBus.dispatch("pagerender", { + source: this, + pageNumber: this.id, + }); + return resultPromise; + } + setPageLabel(label) { + this.pageLabel = typeof label === "string" ? label : null; + this.div.setAttribute( + "data-l10n-args", + JSON.stringify({ + page: this.pageLabel ?? this.id, + }), + ); + if (this.pageLabel !== null) { + this.div.setAttribute("data-page-label", this.pageLabel); + } else { + this.div.removeAttribute("data-page-label"); + } + } + get thumbnailCanvas() { + const { directDrawing, initialOptionalContent, regularAnnotations } = + this.#useThumbnailCanvas; + return directDrawing && initialOptionalContent && regularAnnotations + ? this.canvas + : null; + } +} // ./web/pdf_viewer.js + +const DEFAULT_CACHE_SIZE = 10; +const PagesCountLimit = { + FORCE_SCROLL_MODE_PAGE: 10000, + FORCE_LAZY_PAGE_INIT: 5000, + PAUSE_EAGER_PAGE_INIT: 250, +}; +function isValidAnnotationEditorMode(mode) { + return ( + Object.values(AnnotationEditorType).includes(mode) && + mode !== AnnotationEditorType.DISABLE + ); +} +class PDFPageViewBuffer { + #buf = new Set(); + #size = 0; + constructor(size) { + this.#size = size; + } + push(view) { + const buf = this.#buf; + if (buf.has(view)) { + buf.delete(view); + } + buf.add(view); + if (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + resize(newSize, idsToKeep = null) { + this.#size = newSize; + const buf = this.#buf; + if (idsToKeep) { + const ii = buf.size; + let i = 1; + for (const view of buf) { + if (idsToKeep.has(view.id)) { + buf.delete(view); + buf.add(view); + } + if (++i > ii) { + break; + } + } + } + while (buf.size > this.#size) { + this.#destroyFirstView(); + } + } + has(view) { + return this.#buf.has(view); + } + [Symbol.iterator]() { + return this.#buf.keys(); + } + #destroyFirstView() { + const firstView = this.#buf.keys().next().value; + firstView?.destroy(); + this.#buf.delete(firstView); + } +} +class PDFViewer { + #buffer = null; + #altTextManager = null; + #annotationEditorHighlightColors = null; + #annotationEditorMode = AnnotationEditorType.NONE; + #annotationEditorUIManager = null; + #annotationMode = AnnotationMode.ENABLE_FORMS; + #containerTopLeft = null; + #editorUndoBar = null; + #enableHWA = false; + #enableHighlightFloatingButton = false; + #enablePermissions = false; + #enableUpdatedAddImage = false; + #enableNewAltTextWhenAddingImage = false; + #eventAbortController = null; + #mlManager = null; + #switchAnnotationEditorModeAC = null; + #switchAnnotationEditorModeTimeoutId = null; + #getAllTextInProgress = false; + #hiddenCopyElement = null; + #interruptCopyCondition = false; + #previousContainerHeight = 0; + #resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this)); + #scrollModePageState = null; + #scaleTimeoutId = null; + #supportsPinchToZoom = true; + #textLayerMode = TextLayerMode.ENABLE; + constructor(options) { + const viewerVersion = "4.10.38"; + if (version !== viewerVersion) { + throw new Error( + `The API version "${version}" does not match the Viewer version "${viewerVersion}".`, + ); + } + this.container = options.container; + this.viewer = options.viewer || options.container.firstElementChild; + if (this.container?.tagName !== "DIV" || this.viewer?.tagName !== "DIV") { + throw new Error("Invalid `container` and/or `viewer` option."); + } + if ( + this.container.offsetParent && + getComputedStyle(this.container).position !== "absolute" + ) { + throw new Error("The `container` must be absolutely positioned."); + } + this.#resizeObserver.observe(this.container); + this.eventBus = options.eventBus; + this.linkService = options.linkService || new SimpleLinkService(); + this.downloadManager = options.downloadManager || null; + this.findController = options.findController || null; + this.#altTextManager = options.altTextManager || null; + this.#editorUndoBar = options.editorUndoBar || null; + if (this.findController) { + this.findController.onIsPageVisible = (pageNumber) => + this._getVisiblePages().ids.has(pageNumber); + } + this._scriptingManager = options.scriptingManager || null; + this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; + this.#annotationMode = + options.annotationMode ?? AnnotationMode.ENABLE_FORMS; + this.#annotationEditorMode = + options.annotationEditorMode ?? AnnotationEditorType.NONE; + this.#annotationEditorHighlightColors = + options.annotationEditorHighlightColors || null; + this.#enableHighlightFloatingButton = + options.enableHighlightFloatingButton === true; + this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true; + this.#enableNewAltTextWhenAddingImage = + options.enableNewAltTextWhenAddingImage === true; + this.imageResourcesPath = options.imageResourcesPath || ""; + this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; + this.removePageBorders = options.removePageBorders || false; + this.maxCanvasPixels = options.maxCanvasPixels; + this.l10n = options.l10n; + this.l10n ||= new genericl10n_GenericL10n(); + this.#enablePermissions = options.enablePermissions || false; + this.pageColors = options.pageColors || null; + this.#mlManager = options.mlManager || null; + this.#enableHWA = options.enableHWA || false; + this.#supportsPinchToZoom = options.supportsPinchToZoom !== false; + this.defaultRenderingQueue = !options.renderingQueue; + if (this.defaultRenderingQueue) { + this.renderingQueue = new PDFRenderingQueue(); + this.renderingQueue.setViewer(this); + } else { + this.renderingQueue = options.renderingQueue; + } + const { abortSignal } = options; + abortSignal?.addEventListener( + "abort", + () => { + this.#resizeObserver.disconnect(); + this.#resizeObserver = null; + }, + { + once: true, + }, + ); + this.scroll = watchScroll( + this.container, + this._scrollUpdate.bind(this), + abortSignal, + ); + this.presentationModeState = PresentationModeState.UNKNOWN; + this._resetView(); + if (this.removePageBorders) { + this.viewer.classList.add("removePageBorders"); + } + this.#updateContainerHeightCss(); + this.eventBus._on("thumbnailrendered", ({ pageNumber, pdfPage }) => { + const pageView = this._pages[pageNumber - 1]; + if (!this.#buffer.has(pageView)) { + pdfPage?.cleanup(); + } + }); + if (!options.l10n) { + this.l10n.translate(this.container); + } + } + get pagesCount() { + return this._pages.length; + } + getPageView(index) { + return this._pages[index]; + } + getCachedPageViews() { + return new Set(this.#buffer); + } + get pageViewsReady() { + return this._pages.every((pageView) => pageView?.pdfPage); + } + get renderForms() { + return this.#annotationMode === AnnotationMode.ENABLE_FORMS; + } + get enableScripting() { + return !!this._scriptingManager; + } + get currentPageNumber() { + return this._currentPageNumber; + } + set currentPageNumber(val) { + if (!Number.isInteger(val)) { + throw new Error("Invalid page number."); + } + if (!this.pdfDocument) { + return; + } + if (!this._setCurrentPageNumber(val, true)) { + console.error(`currentPageNumber: "${val}" is not a valid page.`); + } + } + _setCurrentPageNumber(val, resetCurrentPageView = false) { + if (this._currentPageNumber === val) { + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + if (!(0 < val && val <= this.pagesCount)) { + return false; + } + const previous = this._currentPageNumber; + this._currentPageNumber = val; + this.eventBus.dispatch("pagechanging", { + source: this, + pageNumber: val, + pageLabel: this._pageLabels?.[val - 1] ?? null, + previous, + }); + if (resetCurrentPageView) { + this.#resetCurrentPageView(); + } + return true; + } + get currentPageLabel() { + return this._pageLabels?.[this._currentPageNumber - 1] ?? null; + } + set currentPageLabel(val) { + if (!this.pdfDocument) { + return; + } + let page = val | 0; + if (this._pageLabels) { + const i = this._pageLabels.indexOf(val); + if (i >= 0) { + page = i + 1; + } + } + if (!this._setCurrentPageNumber(page, true)) { + console.error(`currentPageLabel: "${val}" is not a valid page.`); + } + } + get currentScale() { + return this._currentScale !== UNKNOWN_SCALE + ? this._currentScale + : DEFAULT_SCALE; + } + set currentScale(val) { + if (isNaN(val)) { + throw new Error("Invalid numeric scale."); + } + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false, + }); + } + get currentScaleValue() { + return this._currentScaleValue; + } + set currentScaleValue(val) { + if (!this.pdfDocument) { + return; + } + this.#setScale(val, { + noScroll: false, + }); + } + get pagesRotation() { + return this._pagesRotation; + } + set pagesRotation(rotation) { + if (!isValidRotation(rotation)) { + throw new Error("Invalid pages rotation angle."); + } + if (!this.pdfDocument) { + return; + } + rotation %= 360; + if (rotation < 0) { + rotation += 360; + } + if (this._pagesRotation === rotation) { + return; + } + this._pagesRotation = rotation; + const pageNumber = this._currentPageNumber; + this.refresh(true, { + rotation, + }); + if (this._currentScaleValue) { + this.#setScale(this._currentScaleValue, { + noScroll: true, + }); + } + this.eventBus.dispatch("rotationchanging", { + source: this, + pagesRotation: rotation, + pageNumber, + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get firstPagePromise() { + return this.pdfDocument ? this._firstPageCapability.promise : null; + } + get onePageRendered() { + return this.pdfDocument ? this._onePageRenderedCapability.promise : null; + } + get pagesPromise() { + return this.pdfDocument ? this._pagesCapability.promise : null; + } + get _layerProperties() { + const self = this; + return shadow(this, "_layerProperties", { + get annotationEditorUIManager() { + return self.#annotationEditorUIManager; + }, + get annotationStorage() { + return self.pdfDocument?.annotationStorage; + }, + get downloadManager() { + return self.downloadManager; + }, + get enableScripting() { + return !!self._scriptingManager; + }, + get fieldObjectsPromise() { + return self.pdfDocument?.getFieldObjects(); + }, + get findController() { + return self.findController; + }, + get hasJSActionsPromise() { + return self.pdfDocument?.hasJSActions(); + }, + get linkService() { + return self.linkService; + }, + }); + } + #initializePermissions(permissions) { + const params = { + annotationEditorMode: this.#annotationEditorMode, + annotationMode: this.#annotationMode, + textLayerMode: this.#textLayerMode, + }; + if (!permissions) { + return params; + } + if ( + !permissions.includes(PermissionFlag.COPY) && + this.#textLayerMode === TextLayerMode.ENABLE + ) { + params.textLayerMode = TextLayerMode.ENABLE_PERMISSIONS; + } + if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { + params.annotationEditorMode = AnnotationEditorType.DISABLE; + } + if ( + !permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && + !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && + this.#annotationMode === AnnotationMode.ENABLE_FORMS + ) { + params.annotationMode = AnnotationMode.ENABLE; + } + return params; + } + async #onePageRenderedOrForceFetch(signal) { + if ( + document.visibilityState === "hidden" || + !this.container.offsetParent || + this._getVisiblePages().views.length === 0 + ) { + return; + } + const hiddenCapability = Promise.withResolvers(), + ac = new AbortController(); + document.addEventListener( + "visibilitychange", + () => { + if (document.visibilityState === "hidden") { + hiddenCapability.resolve(); + } + }, + { + signal: + typeof AbortSignal.any === "function" + ? AbortSignal.any([signal, ac.signal]) + : signal, + }, + ); + await Promise.race([ + this._onePageRenderedCapability.promise, + hiddenCapability.promise, + ]); + ac.abort(); + } + async getAllText() { + const texts = []; + const buffer = []; + for ( + let pageNum = 1, pagesCount = this.pdfDocument.numPages; + pageNum <= pagesCount; + ++pageNum + ) { + if (this.#interruptCopyCondition) { + return null; + } + buffer.length = 0; + const page = await this.pdfDocument.getPage(pageNum); + const { items } = await page.getTextContent(); + for (const item of items) { + if (item.str) { + buffer.push(item.str); + } + if (item.hasEOL) { + buffer.push("\n"); + } + } + texts.push(removeNullCharacters(buffer.join(""))); + } + return texts.join("\n"); + } + #copyCallback(textLayerMode, event) { + const selection = document.getSelection(); + const { focusNode, anchorNode } = selection; + if ( + anchorNode && + focusNode && + selection.containsNode(this.#hiddenCopyElement) + ) { + if ( + this.#getAllTextInProgress || + textLayerMode === TextLayerMode.ENABLE_PERMISSIONS + ) { + stopEvent(event); + return; + } + this.#getAllTextInProgress = true; + const { classList } = this.viewer; + classList.add("copyAll"); + const ac = new AbortController(); + window.addEventListener( + "keydown", + (ev) => (this.#interruptCopyCondition = ev.key === "Escape"), + { + signal: ac.signal, + }, + ); + this.getAllText() + .then(async (text) => { + if (text !== null) { + await navigator.clipboard.writeText(text); + } + }) + .catch((reason) => { + console.warn( + `Something goes wrong when extracting the text: ${reason.message}`, + ); + }) + .finally(() => { + this.#getAllTextInProgress = false; + this.#interruptCopyCondition = false; + ac.abort(); + classList.remove("copyAll"); + }); + stopEvent(event); + } + } + setDocument(pdfDocument) { + if (this.pdfDocument) { + this.eventBus.dispatch("pagesdestroy", { + source: this, + }); + this._cancelRendering(); + this._resetView(); + this.findController?.setDocument(null); + this._scriptingManager?.setDocument(null); + this.#annotationEditorUIManager?.destroy(); + this.#annotationEditorUIManager = null; + } + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + const pagesCount = pdfDocument.numPages; + const firstPagePromise = pdfDocument.getPage(1); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display", + }); + const permissionsPromise = this.#enablePermissions + ? pdfDocument.getPermissions() + : Promise.resolve(); + const { eventBus, pageColors, viewer } = this; + this.#eventAbortController = new AbortController(); + const { signal } = this.#eventAbortController; + if (pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + console.warn( + "Forcing PAGE-scrolling for performance reasons, given the length of the document.", + ); + const mode = (this._scrollMode = ScrollMode.PAGE); + eventBus.dispatch("scrollmodechanged", { + source: this, + mode, + }); + } + this._pagesCapability.promise.then( + () => { + eventBus.dispatch("pagesloaded", { + source: this, + pagesCount, + }); + }, + () => {}, + ); + const onBeforeDraw = (evt) => { + const pageView = this._pages[evt.pageNumber - 1]; + if (!pageView) { + return; + } + this.#buffer.push(pageView); + }; + eventBus._on("pagerender", onBeforeDraw, { + signal, + }); + const onAfterDraw = (evt) => { + if (evt.cssTransform) { + return; + } + this._onePageRenderedCapability.resolve({ + timestamp: evt.timestamp, + }); + eventBus._off("pagerendered", onAfterDraw); + }; + eventBus._on("pagerendered", onAfterDraw, { + signal, + }); + Promise.all([firstPagePromise, permissionsPromise]) + .then(([firstPdfPage, permissions]) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this._firstPageCapability.resolve(firstPdfPage); + this._optionalContentConfigPromise = optionalContentConfigPromise; + const { annotationEditorMode, annotationMode, textLayerMode } = + this.#initializePermissions(permissions); + if (textLayerMode !== TextLayerMode.DISABLE) { + const element = (this.#hiddenCopyElement = + document.createElement("div")); + element.id = "hiddenCopyElement"; + viewer.before(element); + } + if ( + typeof AbortSignal.any === "function" && + annotationEditorMode !== AnnotationEditorType.DISABLE + ) { + const mode = annotationEditorMode; + if (pdfDocument.isPureXfa) { + console.warn("Warning: XFA-editing is not implemented."); + } else if (isValidAnnotationEditorMode(mode)) { + this.#annotationEditorUIManager = new AnnotationEditorUIManager( + this.container, + viewer, + this.#altTextManager, + eventBus, + pdfDocument, + pageColors, + this.#annotationEditorHighlightColors, + this.#enableHighlightFloatingButton, + this.#enableUpdatedAddImage, + this.#enableNewAltTextWhenAddingImage, + this.#mlManager, + this.#editorUndoBar, + this.#supportsPinchToZoom, + ); + eventBus.dispatch("annotationeditoruimanager", { + source: this, + uiManager: this.#annotationEditorUIManager, + }); + if (mode !== AnnotationEditorType.NONE) { + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + this.#annotationEditorUIManager.updateMode(mode); + } + } else { + console.error(`Invalid AnnotationEditor mode: ${mode}`); + } + } + const viewerElement = + this._scrollMode === ScrollMode.PAGE ? null : viewer; + const scale = this.currentScale; + const viewport = firstPdfPage.getViewport({ + scale: scale * PixelsPerInch.PDF_TO_CSS_UNITS, + }); + viewer.style.setProperty("--scale-factor", viewport.scale); + if (pageColors?.background) { + viewer.style.setProperty("--page-bg-color", pageColors.background); + } + if ( + pageColors?.foreground === "CanvasText" || + pageColors?.background === "Canvas" + ) { + viewer.style.setProperty( + "--hcm-highlight-filter", + pdfDocument.filterFactory.addHighlightHCMFilter( + "highlight", + "CanvasText", + "Canvas", + "HighlightText", + "Highlight", + ), + ); + viewer.style.setProperty( + "--hcm-highlight-selected-filter", + pdfDocument.filterFactory.addHighlightHCMFilter( + "highlight_selected", + "CanvasText", + "Canvas", + "HighlightText", + "ButtonText", + ), + ); + } + for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { + const pageView = new PDFPageView({ + container: viewerElement, + eventBus, + id: pageNum, + scale, + defaultViewport: viewport.clone(), + optionalContentConfigPromise, + renderingQueue: this.renderingQueue, + textLayerMode, + annotationMode, + imageResourcesPath: this.imageResourcesPath, + maxCanvasPixels: this.maxCanvasPixels, + pageColors, + l10n: this.l10n, + layerProperties: this._layerProperties, + enableHWA: this.#enableHWA, + }); + this._pages.push(pageView); + } + this._pages[0]?.setPdfPage(firstPdfPage); + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._spreadMode !== SpreadMode.NONE) { + this._updateSpreadMode(); + } + this.#onePageRenderedOrForceFetch(signal).then(async () => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.findController?.setDocument(pdfDocument); + this._scriptingManager?.setDocument(pdfDocument); + if (this.#hiddenCopyElement) { + document.addEventListener( + "copy", + this.#copyCallback.bind(this, textLayerMode), + { + signal, + }, + ); + } + if (this.#annotationEditorUIManager) { + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode: this.#annotationEditorMode, + }); + } + if ( + pdfDocument.loadingParams.disableAutoFetch || + pagesCount > PagesCountLimit.FORCE_LAZY_PAGE_INIT + ) { + this._pagesCapability.resolve(); + return; + } + let getPagesLeft = pagesCount - 1; + if (getPagesLeft <= 0) { + this._pagesCapability.resolve(); + return; + } + for (let pageNum = 2; pageNum <= pagesCount; ++pageNum) { + const promise = pdfDocument.getPage(pageNum).then( + (pdfPage) => { + const pageView = this._pages[pageNum - 1]; + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }, + (reason) => { + console.error( + `Unable to get page ${pageNum} to initialize viewer`, + reason, + ); + if (--getPagesLeft === 0) { + this._pagesCapability.resolve(); + } + }, + ); + if (pageNum % PagesCountLimit.PAUSE_EAGER_PAGE_INIT === 0) { + await promise; + } + } + }); + eventBus.dispatch("pagesinit", { + source: this, + }); + pdfDocument.getMetadata().then(({ info }) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + if (info.Language) { + viewer.lang = info.Language; + } + }); + if (this.defaultRenderingQueue) { + this.update(); + } + }) + .catch((reason) => { + console.error("Unable to initialize viewer", reason); + this._pagesCapability.reject(reason); + }); + } + setPageLabels(labels) { + if (!this.pdfDocument) { + return; + } + if (!labels) { + this._pageLabels = null; + } else if ( + !(Array.isArray(labels) && this.pdfDocument.numPages === labels.length) + ) { + this._pageLabels = null; + console.error(`setPageLabels: Invalid page labels.`); + } else { + this._pageLabels = labels; + } + for (let i = 0, ii = this._pages.length; i < ii; i++) { + this._pages[i].setPageLabel(this._pageLabels?.[i] ?? null); + } + } + _resetView() { + this._pages = []; + this._currentPageNumber = 1; + this._currentScale = UNKNOWN_SCALE; + this._currentScaleValue = null; + this._pageLabels = null; + this.#buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); + this._location = null; + this._pagesRotation = 0; + this._optionalContentConfigPromise = null; + this._firstPageCapability = Promise.withResolvers(); + this._onePageRenderedCapability = Promise.withResolvers(); + this._pagesCapability = Promise.withResolvers(); + this._scrollMode = ScrollMode.VERTICAL; + this._previousScrollMode = ScrollMode.UNKNOWN; + this._spreadMode = SpreadMode.NONE; + this.#scrollModePageState = { + previousPageNumber: 1, + scrollDown: true, + pages: [], + }; + this.#eventAbortController?.abort(); + this.#eventAbortController = null; + this.viewer.textContent = ""; + this._updateScrollMode(); + this.viewer.removeAttribute("lang"); + this.#hiddenCopyElement?.remove(); + this.#hiddenCopyElement = null; + this.#cleanupSwitchAnnotationEditorMode(); + } + #ensurePageViewVisible() { + if (this._scrollMode !== ScrollMode.PAGE) { + throw new Error("#ensurePageViewVisible: Invalid scrollMode value."); + } + const pageNumber = this._currentPageNumber, + state = this.#scrollModePageState, + viewer = this.viewer; + viewer.textContent = ""; + state.pages.length = 0; + if (this._spreadMode === SpreadMode.NONE && !this.isInPresentationMode) { + const pageView = this._pages[pageNumber - 1]; + viewer.append(pageView.div); + state.pages.push(pageView); + } else { + const pageIndexSet = new Set(), + parity = this._spreadMode - 1; + if (parity === -1) { + pageIndexSet.add(pageNumber - 1); + } else if (pageNumber % 2 !== parity) { + pageIndexSet.add(pageNumber - 1); + pageIndexSet.add(pageNumber); + } else { + pageIndexSet.add(pageNumber - 2); + pageIndexSet.add(pageNumber - 1); + } + const spread = document.createElement("div"); + spread.className = "spread"; + if (this.isInPresentationMode) { + const dummyPage = document.createElement("div"); + dummyPage.className = "dummyPage"; + spread.append(dummyPage); + } + for (const i of pageIndexSet) { + const pageView = this._pages[i]; + if (!pageView) { + continue; + } + spread.append(pageView.div); + state.pages.push(pageView); + } + viewer.append(spread); + } + state.scrollDown = pageNumber >= state.previousPageNumber; + state.previousPageNumber = pageNumber; + } + _scrollUpdate() { + if (this.pagesCount === 0) { + return; + } + this.update(); + } + #scrollIntoView(pageView, pageSpot = null) { + const { div, id } = pageView; + if (this._currentPageNumber !== id) { + this._setCurrentPageNumber(id); + } + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + this.update(); + } + if (!pageSpot && !this.isInPresentationMode) { + const left = div.offsetLeft + div.clientLeft, + right = left + div.clientWidth; + const { scrollLeft, clientWidth } = this.container; + if ( + this._scrollMode === ScrollMode.HORIZONTAL || + left < scrollLeft || + right > scrollLeft + clientWidth + ) { + pageSpot = { + left: 0, + top: 0, + }; + } + } + scrollIntoView(div, pageSpot); + if (!this._currentScaleValue && this._location) { + this._location = null; + } + } + #isSameScale(newScale) { + return ( + newScale === this._currentScale || + Math.abs(newScale - this._currentScale) < 1e-15 + ); + } + #setScaleUpdatePages( + newScale, + newValue, + { noScroll = false, preset = false, drawingDelay = -1, origin = null }, + ) { + this._currentScaleValue = newValue.toString(); + if (this.#isSameScale(newScale)) { + if (preset) { + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: newValue, + }); + } + return; + } + this.viewer.style.setProperty( + "--scale-factor", + newScale * PixelsPerInch.PDF_TO_CSS_UNITS, + ); + const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; + this.refresh(true, { + scale: newScale, + drawingDelay: postponeDrawing ? drawingDelay : -1, + }); + if (postponeDrawing) { + this.#scaleTimeoutId = setTimeout(() => { + this.#scaleTimeoutId = null; + this.refresh(); + }, drawingDelay); + } + const previousScale = this._currentScale; + this._currentScale = newScale; + if (!noScroll) { + let page = this._currentPageNumber, + dest; + if ( + this._location && + !(this.isInPresentationMode || this.isChangingPresentationMode) + ) { + page = this._location.pageNumber; + dest = [ + null, + { + name: "XYZ", + }, + this._location.left, + this._location.top, + null, + ]; + } + this.scrollPageIntoView({ + pageNumber: page, + destArray: dest, + allowNegativeOffset: true, + }); + if (Array.isArray(origin)) { + const scaleDiff = newScale / previousScale - 1; + const [top, left] = this.containerTopLeft; + this.container.scrollLeft += (origin[0] - left) * scaleDiff; + this.container.scrollTop += (origin[1] - top) * scaleDiff; + } + } + this.eventBus.dispatch("scalechanging", { + source: this, + scale: newScale, + presetValue: preset ? newValue : undefined, + }); + if (this.defaultRenderingQueue) { + this.update(); + } + } + get #pageWidthScaleFactor() { + if ( + this._spreadMode !== SpreadMode.NONE && + this._scrollMode !== ScrollMode.HORIZONTAL + ) { + return 2; + } + return 1; + } + #setScale(value, options) { + let scale = parseFloat(value); + if (scale > 0) { + options.preset = false; + this.#setScaleUpdatePages(scale, value, options); + } else { + const currentPage = this._pages[this._currentPageNumber - 1]; + if (!currentPage) { + return; + } + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.isInPresentationMode) { + hPadding = vPadding = 4; + if (this._spreadMode !== SpreadMode.NONE) { + hPadding *= 2; + } + } else if (this.removePageBorders) { + hPadding = vPadding = 0; + } else if (this._scrollMode === ScrollMode.HORIZONTAL) { + [hPadding, vPadding] = [vPadding, hPadding]; + } + const pageWidthScale = + (((this.container.clientWidth - hPadding) / currentPage.width) * + currentPage.scale) / + this.#pageWidthScaleFactor; + const pageHeightScale = + ((this.container.clientHeight - vPadding) / currentPage.height) * + currentPage.scale; + switch (value) { + case "page-actual": + scale = 1; + break; + case "page-width": + scale = pageWidthScale; + break; + case "page-height": + scale = pageHeightScale; + break; + case "page-fit": + scale = Math.min(pageWidthScale, pageHeightScale); + break; + case "auto": + const horizontalScale = isPortraitOrientation(currentPage) + ? pageWidthScale + : Math.min(pageHeightScale, pageWidthScale); + scale = Math.min(MAX_AUTO_SCALE, horizontalScale); + break; + default: + console.error(`#setScale: "${value}" is an unknown zoom value.`); + return; + } + options.preset = true; + this.#setScaleUpdatePages(scale, value, options); + } + } + #resetCurrentPageView() { + const pageView = this._pages[this._currentPageNumber - 1]; + if (this.isInPresentationMode) { + this.#setScale(this._currentScaleValue, { + noScroll: true, + }); + } + this.#scrollIntoView(pageView); + } + pageLabelToPageNumber(label) { + if (!this._pageLabels) { + return null; + } + const i = this._pageLabels.indexOf(label); + if (i < 0) { + return null; + } + return i + 1; + } + scrollPageIntoView({ + pageNumber, + destArray = null, + allowNegativeOffset = false, + ignoreDestinationZoom = false, + }) { + if (!this.pdfDocument) { + return; + } + const pageView = + Number.isInteger(pageNumber) && this._pages[pageNumber - 1]; + if (!pageView) { + console.error( + `scrollPageIntoView: "${pageNumber}" is not a valid pageNumber parameter.`, + ); + return; + } + if (this.isInPresentationMode || !destArray) { + this._setCurrentPageNumber(pageNumber, true); + return; + } + let x = 0, + y = 0; + let width = 0, + height = 0, + widthScale, + heightScale; + const changeOrientation = pageView.rotation % 180 !== 0; + const pageWidth = + (changeOrientation ? pageView.height : pageView.width) / + pageView.scale / + PixelsPerInch.PDF_TO_CSS_UNITS; + const pageHeight = + (changeOrientation ? pageView.width : pageView.height) / + pageView.scale / + PixelsPerInch.PDF_TO_CSS_UNITS; + let scale = 0; + switch (destArray[1].name) { + case "XYZ": + x = destArray[2]; + y = destArray[3]; + scale = destArray[4]; + x = x !== null ? x : 0; + y = y !== null ? y : pageHeight; + break; + case "Fit": + case "FitB": + scale = "page-fit"; + break; + case "FitH": + case "FitBH": + y = destArray[2]; + scale = "page-width"; + if (y === null && this._location) { + x = this._location.left; + y = this._location.top; + } else if (typeof y !== "number" || y < 0) { + y = pageHeight; + } + break; + case "FitV": + case "FitBV": + x = destArray[2]; + width = pageWidth; + height = pageHeight; + scale = "page-height"; + break; + case "FitR": + x = destArray[2]; + y = destArray[3]; + width = destArray[4] - x; + height = destArray[5] - y; + let hPadding = SCROLLBAR_PADDING, + vPadding = VERTICAL_PADDING; + if (this.removePageBorders) { + hPadding = vPadding = 0; + } + widthScale = + (this.container.clientWidth - hPadding) / + width / + PixelsPerInch.PDF_TO_CSS_UNITS; + heightScale = + (this.container.clientHeight - vPadding) / + height / + PixelsPerInch.PDF_TO_CSS_UNITS; + scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); + break; + default: + console.error( + `scrollPageIntoView: "${destArray[1].name}" is not a valid destination type.`, + ); + return; + } + if (!ignoreDestinationZoom) { + if (scale && scale !== this._currentScale) { + this.currentScaleValue = scale; + } else if (this._currentScale === UNKNOWN_SCALE) { + this.currentScaleValue = DEFAULT_SCALE_VALUE; + } + } + if (scale === "page-fit" && !destArray[4]) { + this.#scrollIntoView(pageView); + return; + } + const boundingRect = [ + pageView.viewport.convertToViewportPoint(x, y), + pageView.viewport.convertToViewportPoint(x + width, y + height), + ]; + let left = Math.min(boundingRect[0][0], boundingRect[1][0]); + let top = Math.min(boundingRect[0][1], boundingRect[1][1]); + if (!allowNegativeOffset) { + left = Math.max(left, 0); + top = Math.max(top, 0); + } + this.#scrollIntoView(pageView, { + left, + top, + }); + } + _updateLocation(firstPage) { + const currentScale = this._currentScale; + const currentScaleValue = this._currentScaleValue; + const normalizedScaleValue = + parseFloat(currentScaleValue) === currentScale + ? Math.round(currentScale * 10000) / 100 + : currentScaleValue; + const pageNumber = firstPage.id; + const currentPageView = this._pages[pageNumber - 1]; + const container = this.container; + const topLeft = currentPageView.getPagePoint( + container.scrollLeft - firstPage.x, + container.scrollTop - firstPage.y, + ); + const intLeft = Math.round(topLeft[0]); + const intTop = Math.round(topLeft[1]); + let pdfOpenParams = `#page=${pageNumber}`; + if (!this.isInPresentationMode) { + pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`; + } + this._location = { + pageNumber, + scale: normalizedScaleValue, + top: intTop, + left: intLeft, + rotation: this._pagesRotation, + pdfOpenParams, + }; + } + update() { + const visible = this._getVisiblePages(); + const visiblePages = visible.views, + numVisiblePages = visiblePages.length; + if (numVisiblePages === 0) { + return; + } + const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1); + this.#buffer.resize(newCacheSize, visible.ids); + this.renderingQueue.renderHighestPriority(visible); + const isSimpleLayout = + this._spreadMode === SpreadMode.NONE && + (this._scrollMode === ScrollMode.PAGE || + this._scrollMode === ScrollMode.VERTICAL); + const currentId = this._currentPageNumber; + let stillFullyVisible = false; + for (const page of visiblePages) { + if (page.percent < 100) { + break; + } + if (page.id === currentId && isSimpleLayout) { + stillFullyVisible = true; + break; + } + } + this._setCurrentPageNumber( + stillFullyVisible ? currentId : visiblePages[0].id, + ); + this._updateLocation(visible.first); + this.eventBus.dispatch("updateviewarea", { + source: this, + location: this._location, + }); + } + #switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { ids, views } = visible; + for (const page of views) { + const { view } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + } + if (pagesToRefresh.length === 0) { + return null; + } + this.renderingQueue.renderHighestPriority({ + first: pagesToRefresh[0], + last: pagesToRefresh.at(-1), + views: pagesToRefresh, + ids, + }); + return ids; + } + containsElement(element) { + return this.container.contains(element); + } + focus() { + this.container.focus(); + } + get _isContainerRtl() { + return getComputedStyle(this.container).direction === "rtl"; + } + get isInPresentationMode() { + return this.presentationModeState === PresentationModeState.FULLSCREEN; + } + get isChangingPresentationMode() { + return this.presentationModeState === PresentationModeState.CHANGING; + } + get isHorizontalScrollbarEnabled() { + return this.isInPresentationMode + ? false + : this.container.scrollWidth > this.container.clientWidth; + } + get isVerticalScrollbarEnabled() { + return this.isInPresentationMode + ? false + : this.container.scrollHeight > this.container.clientHeight; + } + _getVisiblePages() { + const views = + this._scrollMode === ScrollMode.PAGE + ? this.#scrollModePageState.pages + : this._pages, + horizontal = this._scrollMode === ScrollMode.HORIZONTAL, + rtl = horizontal && this._isContainerRtl; + return getVisibleElements({ + scrollEl: this.container, + views, + sortByVisibility: true, + horizontal, + rtl, + }); + } + cleanup() { + for (const pageView of this._pages) { + if (pageView.renderingState !== RenderingStates.FINISHED) { + pageView.reset(); + } + } + } + _cancelRendering() { + for (const pageView of this._pages) { + pageView.cancelRendering(); + } + } + async #ensurePdfPageLoaded(pageView) { + if (pageView.pdfPage) { + return pageView.pdfPage; + } + try { + const pdfPage = await this.pdfDocument.getPage(pageView.id); + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + return pdfPage; + } catch (reason) { + console.error("Unable to get page for page view", reason); + return null; + } + } + #getScrollAhead(visible) { + if (visible.first?.id === 1) { + return true; + } else if (visible.last?.id === this.pagesCount) { + return false; + } + switch (this._scrollMode) { + case ScrollMode.PAGE: + return this.#scrollModePageState.scrollDown; + case ScrollMode.HORIZONTAL: + return this.scroll.right; + } + return this.scroll.down; + } + forceRendering(currentlyVisiblePages) { + const visiblePages = currentlyVisiblePages || this._getVisiblePages(); + const scrollAhead = this.#getScrollAhead(visiblePages); + const preRenderExtra = + this._spreadMode !== SpreadMode.NONE && + this._scrollMode !== ScrollMode.HORIZONTAL; + const pageView = this.renderingQueue.getHighestPriority( + visiblePages, + this._pages, + scrollAhead, + preRenderExtra, + ); + if (pageView) { + this.#ensurePdfPageLoaded(pageView).then(() => { + this.renderingQueue.renderView(pageView); + }); + return true; + } + return false; + } + get hasEqualPageSizes() { + const firstPageView = this._pages[0]; + for (let i = 1, ii = this._pages.length; i < ii; ++i) { + const pageView = this._pages[i]; + if ( + pageView.width !== firstPageView.width || + pageView.height !== firstPageView.height + ) { + return false; + } + } + return true; + } + getPagesOverview() { + let initialOrientation; + return this._pages.map((pageView) => { + const viewport = pageView.pdfPage.getViewport({ + scale: 1, + }); + const orientation = isPortraitOrientation(viewport); + if (initialOrientation === undefined) { + initialOrientation = orientation; + } else if ( + this.enablePrintAutoRotate && + orientation !== initialOrientation + ) { + return { + width: viewport.height, + height: viewport.width, + rotation: (viewport.rotation - 90) % 360, + }; + } + return { + width: viewport.width, + height: viewport.height, + rotation: viewport.rotation, + }; + }); + } + get optionalContentConfigPromise() { + if (!this.pdfDocument) { + return Promise.resolve(null); + } + if (!this._optionalContentConfigPromise) { + console.error("optionalContentConfigPromise: Not initialized yet."); + return this.pdfDocument.getOptionalContentConfig({ + intent: "display", + }); + } + return this._optionalContentConfigPromise; + } + set optionalContentConfigPromise(promise) { + if (!(promise instanceof Promise)) { + throw new Error(`Invalid optionalContentConfigPromise: ${promise}`); + } + if (!this.pdfDocument) { + return; + } + if (!this._optionalContentConfigPromise) { + return; + } + this._optionalContentConfigPromise = promise; + this.refresh(false, { + optionalContentConfigPromise: promise, + }); + this.eventBus.dispatch("optionalcontentconfigchanged", { + source: this, + promise, + }); + } + get scrollMode() { + return this._scrollMode; + } + set scrollMode(mode) { + if (this._scrollMode === mode) { + return; + } + if (!isValidScrollMode(mode)) { + throw new Error(`Invalid scroll mode: ${mode}`); + } + if (this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) { + return; + } + this._previousScrollMode = this._scrollMode; + this._scrollMode = mode; + this.eventBus.dispatch("scrollmodechanged", { + source: this, + mode, + }); + this._updateScrollMode(this._currentPageNumber); + } + _updateScrollMode(pageNumber = null) { + const scrollMode = this._scrollMode, + viewer = this.viewer; + viewer.classList.toggle( + "scrollHorizontal", + scrollMode === ScrollMode.HORIZONTAL, + ); + viewer.classList.toggle("scrollWrapped", scrollMode === ScrollMode.WRAPPED); + if (!this.pdfDocument || !pageNumber) { + return; + } + if (scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else if (this._previousScrollMode === ScrollMode.PAGE) { + this._updateSpreadMode(); + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true, + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + get spreadMode() { + return this._spreadMode; + } + set spreadMode(mode) { + if (this._spreadMode === mode) { + return; + } + if (!isValidSpreadMode(mode)) { + throw new Error(`Invalid spread mode: ${mode}`); + } + this._spreadMode = mode; + this.eventBus.dispatch("spreadmodechanged", { + source: this, + mode, + }); + this._updateSpreadMode(this._currentPageNumber); + } + _updateSpreadMode(pageNumber = null) { + if (!this.pdfDocument) { + return; + } + const viewer = this.viewer, + pages = this._pages; + if (this._scrollMode === ScrollMode.PAGE) { + this.#ensurePageViewVisible(); + } else { + viewer.textContent = ""; + if (this._spreadMode === SpreadMode.NONE) { + for (const pageView of this._pages) { + viewer.append(pageView.div); + } + } else { + const parity = this._spreadMode - 1; + let spread = null; + for (let i = 0, ii = pages.length; i < ii; ++i) { + if (spread === null) { + spread = document.createElement("div"); + spread.className = "spread"; + viewer.append(spread); + } else if (i % 2 === parity) { + spread = spread.cloneNode(false); + viewer.append(spread); + } + spread.append(pages[i].div); + } + } + } + if (!pageNumber) { + return; + } + if (this._currentScaleValue && isNaN(this._currentScaleValue)) { + this.#setScale(this._currentScaleValue, { + noScroll: true, + }); + } + this._setCurrentPageNumber(pageNumber, true); + this.update(); + } + _getPageAdvance(currentPageNumber, previous = false) { + switch (this._scrollMode) { + case ScrollMode.WRAPPED: { + const { views } = this._getVisiblePages(), + pageLayout = new Map(); + for (const { id, y, percent, widthPercent } of views) { + if (percent === 0 || widthPercent < 100) { + continue; + } + let yArray = pageLayout.get(y); + if (!yArray) { + pageLayout.set(y, (yArray ||= [])); + } + yArray.push(id); + } + for (const yArray of pageLayout.values()) { + const currentIndex = yArray.indexOf(currentPageNumber); + if (currentIndex === -1) { + continue; + } + const numPages = yArray.length; + if (numPages === 1) { + break; + } + if (previous) { + for (let i = currentIndex - 1, ii = 0; i >= ii; i--) { + const currentId = yArray[i], + expectedId = yArray[i + 1] - 1; + if (currentId < expectedId) { + return currentPageNumber - expectedId; + } + } + } else { + for (let i = currentIndex + 1, ii = numPages; i < ii; i++) { + const currentId = yArray[i], + expectedId = yArray[i - 1] + 1; + if (currentId > expectedId) { + return expectedId - currentPageNumber; + } + } + } + if (previous) { + const firstId = yArray[0]; + if (firstId < currentPageNumber) { + return currentPageNumber - firstId + 1; + } + } else { + const lastId = yArray[numPages - 1]; + if (lastId > currentPageNumber) { + return lastId - currentPageNumber + 1; + } + } + break; + } + break; + } + case ScrollMode.HORIZONTAL: { + break; + } + case ScrollMode.PAGE: + case ScrollMode.VERTICAL: { + if (this._spreadMode === SpreadMode.NONE) { + break; + } + const parity = this._spreadMode - 1; + if (previous && currentPageNumber % 2 !== parity) { + break; + } else if (!previous && currentPageNumber % 2 === parity) { + break; + } + const { views } = this._getVisiblePages(), + expectedId = previous ? currentPageNumber - 1 : currentPageNumber + 1; + for (const { id, percent, widthPercent } of views) { + if (id !== expectedId) { + continue; + } + if (percent > 0 && widthPercent === 100) { + return 2; + } + break; + } + break; + } + } + return 1; + } + nextPage() { + const currentPageNumber = this._currentPageNumber, + pagesCount = this.pagesCount; + if (currentPageNumber >= pagesCount) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, false) || 1; + this.currentPageNumber = Math.min(currentPageNumber + advance, pagesCount); + return true; + } + previousPage() { + const currentPageNumber = this._currentPageNumber; + if (currentPageNumber <= 1) { + return false; + } + const advance = this._getPageAdvance(currentPageNumber, true) || 1; + this.currentPageNumber = Math.max(currentPageNumber - advance, 1); + return true; + } + updateScale({ drawingDelay, scaleFactor = null, steps = null, origin }) { + if (steps === null && scaleFactor === null) { + throw new Error( + "Invalid updateScale options: either `steps` or `scaleFactor` must be provided.", + ); + } + if (!this.pdfDocument) { + return; + } + let newScale = this._currentScale; + if (scaleFactor > 0 && scaleFactor !== 1) { + newScale = Math.round(newScale * scaleFactor * 100) / 100; + } else if (steps) { + const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA; + const round = steps > 0 ? Math.ceil : Math.floor; + steps = Math.abs(steps); + do { + newScale = round((newScale * delta).toFixed(2) * 10) / 10; + } while (--steps > 0); + } + newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale)); + this.#setScale(newScale, { + noScroll: false, + drawingDelay, + origin, + }); + } + increaseScale(options = {}) { + this.updateScale({ + ...options, + steps: options.steps ?? 1, + }); + } + decreaseScale(options = {}) { + this.updateScale({ + ...options, + steps: -(options.steps ?? 1), + }); + } + #updateContainerHeightCss(height = this.container.clientHeight) { + if (height !== this.#previousContainerHeight) { + this.#previousContainerHeight = height; + docStyle.setProperty("--viewer-container-height", `${height}px`); + } + } + #resizeObserverCallback(entries) { + for (const entry of entries) { + if (entry.target === this.container) { + this.#updateContainerHeightCss( + Math.floor(entry.borderBoxSize[0].blockSize), + ); + this.#containerTopLeft = null; + break; + } + } + } + get containerTopLeft() { + return (this.#containerTopLeft ||= [ + this.container.offsetTop, + this.container.offsetLeft, + ]); + } + #cleanupSwitchAnnotationEditorMode() { + this.#switchAnnotationEditorModeAC?.abort(); + this.#switchAnnotationEditorModeAC = null; + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + } + get annotationEditorMode() { + return this.#annotationEditorUIManager + ? this.#annotationEditorMode + : AnnotationEditorType.DISABLE; + } + set annotationEditorMode({ mode, editId = null, isFromKeyboard = false }) { + if (!this.#annotationEditorUIManager) { + throw new Error(`The AnnotationEditor is not enabled.`); + } + if (this.#annotationEditorMode === mode) { + return; + } + if (!isValidAnnotationEditorMode(mode)) { + throw new Error(`Invalid AnnotationEditor mode: ${mode}`); + } + if (!this.pdfDocument) { + return; + } + if (mode === AnnotationEditorType.STAMP) { + this.#mlManager?.loadModel("altText"); + } + const { eventBus } = this; + const updater = () => { + this.#cleanupSwitchAnnotationEditorMode(); + this.#annotationEditorMode = mode; + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode, + }); + }; + if ( + mode === AnnotationEditorType.NONE || + this.#annotationEditorMode === AnnotationEditorType.NONE + ) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.annotationStorage.resetModifiedIds(); + } + for (const pageView of this._pages) { + pageView.toggleEditingMode(isEditing); + } + const idsToRefresh = this.#switchToEditAnnotationMode(); + if (isEditing && idsToRefresh) { + this.#cleanupSwitchAnnotationEditorMode(); + this.#switchAnnotationEditorModeAC = new AbortController(); + const signal = AbortSignal.any([ + this.#eventAbortController.signal, + this.#switchAnnotationEditorModeAC.signal, + ]); + eventBus._on( + "pagerendered", + ({ pageNumber }) => { + idsToRefresh.delete(pageNumber); + if (idsToRefresh.size === 0) { + this.#switchAnnotationEditorModeTimeoutId = setTimeout( + updater, + 0, + ); + } + }, + { + signal, + }, + ); + return; + } + } + updater(); + } + refresh(noUpdate = false, updateArgs = Object.create(null)) { + if (!this.pdfDocument) { + return; + } + for (const pageView of this._pages) { + pageView.update(updateArgs); + } + if (this.#scaleTimeoutId !== null) { + clearTimeout(this.#scaleTimeoutId); + this.#scaleTimeoutId = null; + } + if (!noUpdate) { + this.update(); + } + } +} // ./web/secondary_toolbar.js + +class SecondaryToolbar { + #opts; + constructor(options, eventBus) { + this.#opts = options; + const buttons = [ + { + element: options.presentationModeButton, + eventName: "presentationmode", + close: true, + }, + { + element: options.printButton, + eventName: "print", + close: true, + }, + { + element: options.downloadButton, + eventName: "download", + close: true, + }, + { + element: options.viewBookmarkButton, + eventName: null, + close: true, + }, + { + element: options.firstPageButton, + eventName: "firstpage", + close: true, + }, + { + element: options.lastPageButton, + eventName: "lastpage", + close: true, + }, + { + element: options.pageRotateCwButton, + eventName: "rotatecw", + close: false, + }, + { + element: options.pageRotateCcwButton, + eventName: "rotateccw", + close: false, + }, + { + element: options.cursorSelectToolButton, + eventName: "switchcursortool", + eventDetails: { + tool: CursorTool.SELECT, + }, + close: true, + }, + { + element: options.cursorHandToolButton, + eventName: "switchcursortool", + eventDetails: { + tool: CursorTool.HAND, + }, + close: true, + }, + { + element: options.scrollPageButton, + eventName: "switchscrollmode", + eventDetails: { + mode: ScrollMode.PAGE, + }, + close: true, + }, + { + element: options.scrollVerticalButton, + eventName: "switchscrollmode", + eventDetails: { + mode: ScrollMode.VERTICAL, + }, + close: true, + }, + { + element: options.scrollHorizontalButton, + eventName: "switchscrollmode", + eventDetails: { + mode: ScrollMode.HORIZONTAL, + }, + close: true, + }, + { + element: options.scrollWrappedButton, + eventName: "switchscrollmode", + eventDetails: { + mode: ScrollMode.WRAPPED, + }, + close: true, + }, + { + element: options.spreadNoneButton, + eventName: "switchspreadmode", + eventDetails: { + mode: SpreadMode.NONE, + }, + close: true, + }, + { + element: options.spreadOddButton, + eventName: "switchspreadmode", + eventDetails: { + mode: SpreadMode.ODD, + }, + close: true, + }, + { + element: options.spreadEvenButton, + eventName: "switchspreadmode", + eventDetails: { + mode: SpreadMode.EVEN, + }, + close: true, + }, + { + element: options.imageAltTextSettingsButton, + eventName: "imagealttextsettings", + close: true, + }, + { + element: options.documentPropertiesButton, + eventName: "documentproperties", + close: true, + }, + ]; + buttons.push({ + element: options.openFileButton, + eventName: "openfile", + close: true, + }); + this.eventBus = eventBus; + this.opened = false; + this.#bindListeners(buttons); + this.reset(); + } + get isOpen() { + return this.opened; + } + setPageNumber(pageNumber) { + this.pageNumber = pageNumber; + this.#updateUIState(); + } + setPagesCount(pagesCount) { + this.pagesCount = pagesCount; + this.#updateUIState(); + } + reset() { + this.pageNumber = 0; + this.pagesCount = 0; + this.#updateUIState(); + this.eventBus.dispatch("switchcursortool", { + source: this, + reset: true, + }); + this.#scrollModeChanged({ + mode: ScrollMode.VERTICAL, + }); + this.#spreadModeChanged({ + mode: SpreadMode.NONE, + }); + } + #updateUIState() { + const { + firstPageButton, + lastPageButton, + pageRotateCwButton, + pageRotateCcwButton, + } = this.#opts; + firstPageButton.disabled = this.pageNumber <= 1; + lastPageButton.disabled = this.pageNumber >= this.pagesCount; + pageRotateCwButton.disabled = this.pagesCount === 0; + pageRotateCcwButton.disabled = this.pagesCount === 0; + } + #bindListeners(buttons) { + const { eventBus } = this; + const { toggleButton } = this.#opts; + toggleButton.addEventListener("click", this.toggle.bind(this)); + for (const { element, eventName, close, eventDetails } of buttons) { + element.addEventListener("click", (evt) => { + if (eventName !== null) { + eventBus.dispatch(eventName, { + source: this, + ...eventDetails, + }); + } + if (close) { + this.close(); + } + eventBus.dispatch("reporttelemetry", { + source: this, + details: { + type: "buttons", + data: { + id: element.id, + }, + }, + }); + }); + } + eventBus._on("cursortoolchanged", this.#cursorToolChanged.bind(this)); + eventBus._on("scrollmodechanged", this.#scrollModeChanged.bind(this)); + eventBus._on("spreadmodechanged", this.#spreadModeChanged.bind(this)); + } + #cursorToolChanged({ tool, disabled }) { + const { cursorSelectToolButton, cursorHandToolButton } = this.#opts; + toggleCheckedBtn(cursorSelectToolButton, tool === CursorTool.SELECT); + toggleCheckedBtn(cursorHandToolButton, tool === CursorTool.HAND); + cursorSelectToolButton.disabled = disabled; + cursorHandToolButton.disabled = disabled; + } + #scrollModeChanged({ mode }) { + const { + scrollPageButton, + scrollVerticalButton, + scrollHorizontalButton, + scrollWrappedButton, + spreadNoneButton, + spreadOddButton, + spreadEvenButton, + } = this.#opts; + toggleCheckedBtn(scrollPageButton, mode === ScrollMode.PAGE); + toggleCheckedBtn(scrollVerticalButton, mode === ScrollMode.VERTICAL); + toggleCheckedBtn(scrollHorizontalButton, mode === ScrollMode.HORIZONTAL); + toggleCheckedBtn(scrollWrappedButton, mode === ScrollMode.WRAPPED); + const forceScrollModePage = + this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE; + scrollPageButton.disabled = forceScrollModePage; + scrollVerticalButton.disabled = forceScrollModePage; + scrollHorizontalButton.disabled = forceScrollModePage; + scrollWrappedButton.disabled = forceScrollModePage; + const isHorizontal = mode === ScrollMode.HORIZONTAL; + spreadNoneButton.disabled = isHorizontal; + spreadOddButton.disabled = isHorizontal; + spreadEvenButton.disabled = isHorizontal; + } + #spreadModeChanged({ mode }) { + const { spreadNoneButton, spreadOddButton, spreadEvenButton } = this.#opts; + toggleCheckedBtn(spreadNoneButton, mode === SpreadMode.NONE); + toggleCheckedBtn(spreadOddButton, mode === SpreadMode.ODD); + toggleCheckedBtn(spreadEvenButton, mode === SpreadMode.EVEN); + } + open() { + if (this.opened) { + return; + } + this.opened = true; + const { toggleButton, toolbar } = this.#opts; + toggleExpandedBtn(toggleButton, true, toolbar); + } + close() { + if (!this.opened) { + return; + } + this.opened = false; + const { toggleButton, toolbar } = this.#opts; + toggleExpandedBtn(toggleButton, false, toolbar); + } + toggle() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } +} // ./web/toolbar.js + +class Toolbar { + #opts; + constructor(options, eventBus, toolbarDensity = 0) { + this.#opts = options; + this.eventBus = eventBus; + const buttons = [ + { + element: options.previous, + eventName: "previouspage", + }, + { + element: options.next, + eventName: "nextpage", + }, + { + element: options.zoomIn, + eventName: "zoomin", + }, + { + element: options.zoomOut, + eventName: "zoomout", + }, + { + element: options.print, + eventName: "print", + }, + { + element: options.download, + eventName: "download", + }, + { + element: options.editorFreeTextButton, + eventName: "switchannotationeditormode", + eventDetails: { + get mode() { + const { classList } = options.editorFreeTextButton; + return classList.contains("toggled") + ? AnnotationEditorType.NONE + : AnnotationEditorType.FREETEXT; + }, + }, + }, + { + element: options.editorHighlightButton, + eventName: "switchannotationeditormode", + eventDetails: { + get mode() { + const { classList } = options.editorHighlightButton; + return classList.contains("toggled") + ? AnnotationEditorType.NONE + : AnnotationEditorType.HIGHLIGHT; + }, + }, + }, + { + element: options.editorInkButton, + eventName: "switchannotationeditormode", + eventDetails: { + get mode() { + const { classList } = options.editorInkButton; + return classList.contains("toggled") + ? AnnotationEditorType.NONE + : AnnotationEditorType.INK; + }, + }, + }, + { + element: options.editorStampButton, + eventName: "switchannotationeditormode", + eventDetails: { + get mode() { + const { classList } = options.editorStampButton; + return classList.contains("toggled") + ? AnnotationEditorType.NONE + : AnnotationEditorType.STAMP; + }, + }, + telemetry: { + type: "editing", + data: { + action: "pdfjs.image.icon_click", + }, + }, + }, + ]; + this.#bindListeners(buttons); + this.#updateToolbarDensity({ + value: toolbarDensity, + }); + this.reset(); + } + #updateToolbarDensity({ value }) { + let name = "normal"; + switch (value) { + case 1: + name = "compact"; + break; + case 2: + name = "touch"; + break; + } + document.documentElement.setAttribute("data-toolbar-density", name); + } + #setAnnotationEditorUIManager(uiManager, parentContainer) { + const colorPicker = new ColorPicker({ + uiManager, + }); + uiManager.setMainHighlightColorPicker(colorPicker); + parentContainer.append(colorPicker.renderMainDropdown()); + } + setPageNumber(pageNumber, pageLabel) { + this.pageNumber = pageNumber; + this.pageLabel = pageLabel; + this.#updateUIState(false); + } + setPagesCount(pagesCount, hasPageLabels) { + this.pagesCount = pagesCount; + this.hasPageLabels = hasPageLabels; + this.#updateUIState(true); + } + setPageScale(pageScaleValue, pageScale) { + this.pageScaleValue = (pageScaleValue || pageScale).toString(); + this.pageScale = pageScale; + this.#updateUIState(false); + } + reset() { + this.pageNumber = 0; + this.pageLabel = null; + this.hasPageLabels = false; + this.pagesCount = 0; + this.pageScaleValue = DEFAULT_SCALE_VALUE; + this.pageScale = DEFAULT_SCALE; + this.#updateUIState(true); + this.updateLoadingIndicatorState(); + this.#editorModeChanged({ + mode: AnnotationEditorType.DISABLE, + }); + } + #bindListeners(buttons) { + const { eventBus } = this; + const { + editorHighlightColorPicker, + editorHighlightButton, + pageNumber, + scaleSelect, + } = this.#opts; + const self = this; + for (const { element, eventName, eventDetails, telemetry } of buttons) { + element.addEventListener("click", (evt) => { + if (eventName !== null) { + eventBus.dispatch(eventName, { + source: this, + ...eventDetails, + isFromKeyboard: evt.detail === 0, + }); + } + if (telemetry) { + eventBus.dispatch("reporttelemetry", { + source: this, + details: telemetry, + }); + } + }); + } + pageNumber.addEventListener("click", function () { + this.select(); + }); + pageNumber.addEventListener("change", function () { + eventBus.dispatch("pagenumberchanged", { + source: self, + value: this.value, + }); + }); + scaleSelect.addEventListener("change", function () { + if (this.value === "custom") { + return; + } + eventBus.dispatch("scalechanged", { + source: self, + value: this.value, + }); + }); + scaleSelect.addEventListener("click", function ({ target }) { + if ( + this.value === self.pageScaleValue && + target.tagName.toUpperCase() === "OPTION" + ) { + this.blur(); + } + }); + scaleSelect.oncontextmenu = noContextMenu; + eventBus._on( + "annotationeditormodechanged", + this.#editorModeChanged.bind(this), + ); + eventBus._on("showannotationeditorui", ({ mode }) => { + switch (mode) { + case AnnotationEditorType.HIGHLIGHT: + editorHighlightButton.click(); + break; + } + }); + eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this)); + if (editorHighlightColorPicker) { + eventBus._on( + "annotationeditoruimanager", + ({ uiManager }) => { + this.#setAnnotationEditorUIManager( + uiManager, + editorHighlightColorPicker, + ); + }, + { + once: true, + }, + ); + } + } + #editorModeChanged({ mode }) { + const { + editorFreeTextButton, + editorFreeTextParamsToolbar, + editorHighlightButton, + editorHighlightParamsToolbar, + editorInkButton, + editorInkParamsToolbar, + editorStampButton, + editorStampParamsToolbar, + } = this.#opts; + toggleExpandedBtn( + editorFreeTextButton, + mode === AnnotationEditorType.FREETEXT, + editorFreeTextParamsToolbar, + ); + toggleExpandedBtn( + editorHighlightButton, + mode === AnnotationEditorType.HIGHLIGHT, + editorHighlightParamsToolbar, + ); + toggleExpandedBtn( + editorInkButton, + mode === AnnotationEditorType.INK, + editorInkParamsToolbar, + ); + toggleExpandedBtn( + editorStampButton, + mode === AnnotationEditorType.STAMP, + editorStampParamsToolbar, + ); + const isDisable = mode === AnnotationEditorType.DISABLE; + editorFreeTextButton.disabled = isDisable; + editorHighlightButton.disabled = isDisable; + editorInkButton.disabled = isDisable; + editorStampButton.disabled = isDisable; + } + #updateUIState(resetNumPages = false) { + const { pageNumber, pagesCount, pageScaleValue, pageScale } = this; + const opts = this.#opts; + if (resetNumPages) { + if (this.hasPageLabels) { + opts.pageNumber.type = "text"; + opts.numPages.setAttribute("data-l10n-id", "pdfjs-page-of-pages"); + } else { + opts.pageNumber.type = "number"; + opts.numPages.setAttribute("data-l10n-id", "pdfjs-of-pages"); + opts.numPages.setAttribute( + "data-l10n-args", + JSON.stringify({ + pagesCount, + }), + ); + } + opts.pageNumber.max = pagesCount; + } + if (this.hasPageLabels) { + opts.pageNumber.value = this.pageLabel; + opts.numPages.setAttribute( + "data-l10n-args", + JSON.stringify({ + pageNumber, + pagesCount, + }), + ); + } else { + opts.pageNumber.value = pageNumber; + } + opts.previous.disabled = pageNumber <= 1; + opts.next.disabled = pageNumber >= pagesCount; + opts.zoomOut.disabled = pageScale <= MIN_SCALE; + opts.zoomIn.disabled = pageScale >= MAX_SCALE; + let predefinedValueFound = false; + for (const option of opts.scaleSelect.options) { + if (option.value !== pageScaleValue) { + option.selected = false; + continue; + } + option.selected = true; + predefinedValueFound = true; + } + if (!predefinedValueFound) { + opts.customScaleOption.selected = true; + opts.customScaleOption.setAttribute( + "data-l10n-args", + JSON.stringify({ + scale: Math.round(pageScale * 10000) / 100, + }), + ); + } + } + updateLoadingIndicatorState(loading = false) { + const { pageNumber } = this.#opts; + pageNumber.classList.toggle("loading", loading); + } +} // ./web/view_history.js + +const DEFAULT_VIEW_HISTORY_CACHE_SIZE = 20; +class ViewHistory { + constructor(fingerprint, cacheSize = DEFAULT_VIEW_HISTORY_CACHE_SIZE) { + this.fingerprint = fingerprint; + this.cacheSize = cacheSize; + this._initializedPromise = this._readFromStorage().then((databaseStr) => { + const database = JSON.parse(databaseStr || "{}"); + let index = -1; + if (!Array.isArray(database.files)) { + database.files = []; + } else { + while (database.files.length >= this.cacheSize) { + database.files.shift(); + } + for (let i = 0, ii = database.files.length; i < ii; i++) { + const branch = database.files[i]; + if (branch.fingerprint === this.fingerprint) { + index = i; + break; + } + } + } + if (index === -1) { + index = + database.files.push({ + fingerprint: this.fingerprint, + }) - 1; + } + this.file = database.files[index]; + this.database = database; + }); + } + async _writeToStorage() { + const databaseStr = JSON.stringify(this.database); + localStorage.setItem("pdfjs.history", databaseStr); + } + async _readFromStorage() { + return localStorage.getItem("pdfjs.history"); + } + async set(name, val) { + await this._initializedPromise; + this.file[name] = val; + return this._writeToStorage(); + } + async setMultiple(properties) { + await this._initializedPromise; + for (const name in properties) { + this.file[name] = properties[name]; + } + return this._writeToStorage(); + } + async get(name, defaultValue) { + await this._initializedPromise; + const val = this.file[name]; + return val !== undefined ? val : defaultValue; + } + async getMultiple(properties) { + await this._initializedPromise; + const values = Object.create(null); + for (const name in properties) { + const val = this.file[name]; + values[name] = val !== undefined ? val : properties[name]; + } + return values; + } +} // ./web/app.js + +const FORCE_PAGES_LOADED_TIMEOUT = 10000; +const ViewOnLoad = { + UNKNOWN: -1, + PREVIOUS: 0, + INITIAL: 1, +}; +const PDFViewerApplication = { + initialBookmark: document.location.hash.substring(1), + _initializedCapability: { + ...Promise.withResolvers(), + settled: false, + }, + appConfig: null, + pdfDocument: null, + pdfLoadingTask: null, + printService: null, + pdfViewer: null, + pdfThumbnailViewer: null, + pdfRenderingQueue: null, + pdfPresentationMode: null, + pdfDocumentProperties: null, + pdfLinkService: null, + pdfHistory: null, + pdfSidebar: null, + pdfOutlineViewer: null, + pdfAttachmentViewer: null, + pdfLayerViewer: null, + pdfCursorTools: null, + pdfScriptingManager: null, + store: null, + downloadManager: null, + overlayManager: null, + preferences: new Preferences(), + toolbar: null, + secondaryToolbar: null, + eventBus: null, + l10n: null, + annotationEditorParams: null, + imageAltTextSettings: null, + isInitialViewSet: false, + isViewerEmbedded: window.parent !== window, + url: "", + baseUrl: "", + mlManager: null, + _downloadUrl: "", + _eventBusAbortController: null, + _windowAbortController: null, + _globalAbortController: new AbortController(), + documentInfo: null, + metadata: null, + _contentDispositionFilename: null, + _contentLength: null, + _saveInProgress: false, + _wheelUnusedTicks: 0, + _wheelUnusedFactor: 1, + _touchManager: null, + _touchUnusedTicks: 0, + _touchUnusedFactor: 1, + _PDFBug: null, + _hasAnnotationEditors: false, + _title: document.title, + _printAnnotationStoragePromise: null, + _isCtrlKeyDown: false, + _caretBrowsing: null, + _isScrolling: false, + editorUndoBar: null, + async initialize(appConfig) { + this.appConfig = appConfig; + try { + await this.preferences.initializedPromise; + } catch (ex) { + console.error("initialize:", ex); + } + if (AppOptions.get("pdfBugEnabled")) { + await this._parseHashParams(); + } + let mode; + switch (AppOptions.get("viewerCssTheme")) { + case 1: + mode = "is-light"; + break; + case 2: + mode = "is-dark"; + break; + } + if (mode) { + document.documentElement.classList.add(mode); + } + this.l10n = await this.externalServices.createL10n(); + document.getElementsByTagName("html")[0].dir = this.l10n.getDirection(); + this.l10n.translate(appConfig.appContainer || document.documentElement); + if ( + this.isViewerEmbedded && + AppOptions.get("externalLinkTarget") === LinkTarget.NONE + ) { + AppOptions.set("externalLinkTarget", LinkTarget.TOP); + } + await this._initializeViewerComponents(); + this.bindEvents(); + this.bindWindowEvents(); + this._initializedCapability.settled = true; + this._initializedCapability.resolve(); + }, + async _parseHashParams() { + const hash = document.location.hash.substring(1); + if (!hash) { + return; + } + const { mainContainer, viewerContainer } = this.appConfig, + params = parseQueryString(hash); + const loadPDFBug = async () => { + if (this._PDFBug) { + return; + } + const { PDFBug } = await import( + /*webpackIgnore: true*/ AppOptions.get("debuggerSrc") + ); + this._PDFBug = PDFBug; + }; + if (params.get("disableworker") === "true") { + try { + GlobalWorkerOptions.workerSrc ||= AppOptions.get("workerSrc"); + await import(/*webpackIgnore: true*/ PDFWorker.workerSrc); + } catch (ex) { + console.error("_parseHashParams:", ex); + } + } + if (params.has("textlayer")) { + switch (params.get("textlayer")) { + case "off": + AppOptions.set("textLayerMode", TextLayerMode.DISABLE); + break; + case "visible": + case "shadow": + case "hover": + viewerContainer.classList.add(`textLayer-${params.get("textlayer")}`); + try { + await loadPDFBug(); + this._PDFBug.loadCSS(); + } catch (ex) { + console.error("_parseHashParams:", ex); + } + break; + } + } + if (params.has("pdfbug")) { + AppOptions.setAll({ + pdfBug: true, + fontExtraProperties: true, + }); + const enabled = params.get("pdfbug").split(","); + try { + await loadPDFBug(); + this._PDFBug.init(mainContainer, enabled); + } catch (ex) { + console.error("_parseHashParams:", ex); + } + } + if (params.has("locale")) { + AppOptions.set("localeProperties", { + lang: params.get("locale"), + }); + } + const opts = { + disableAutoFetch: (x) => x === "true", + disableFontFace: (x) => x === "true", + disableHistory: (x) => x === "true", + disableRange: (x) => x === "true", + disableStream: (x) => x === "true", + verbosity: (x) => x | 0, + }; + for (const name in opts) { + const check = opts[name], + key = name.toLowerCase(); + if (params.has(key)) { + AppOptions.set(name, check(params.get(key))); + } + } + }, + async _initializeViewerComponents() { + const { appConfig, externalServices, l10n } = this; + const eventBus = new EventBus(); + this.eventBus = AppOptions.eventBus = eventBus; + this.mlManager?.setEventBus(eventBus, this._globalAbortController.signal); + this.overlayManager = new OverlayManager(); + const pdfRenderingQueue = new PDFRenderingQueue(); + pdfRenderingQueue.onIdle = this._cleanup.bind(this); + this.pdfRenderingQueue = pdfRenderingQueue; + const pdfLinkService = new PDFLinkService({ + eventBus, + externalLinkTarget: AppOptions.get("externalLinkTarget"), + externalLinkRel: AppOptions.get("externalLinkRel"), + ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"), + }); + this.pdfLinkService = pdfLinkService; + const downloadManager = (this.downloadManager = new DownloadManager()); + const findController = new PDFFindController({ + linkService: pdfLinkService, + eventBus, + updateMatchesCountOnProgress: true, + }); + this.findController = findController; + const pdfScriptingManager = new PDFScriptingManager({ + eventBus, + externalServices, + docProperties: this._scriptingDocProperties.bind(this), + }); + this.pdfScriptingManager = pdfScriptingManager; + const container = appConfig.mainContainer, + viewer = appConfig.viewerContainer; + const annotationEditorMode = AppOptions.get("annotationEditorMode"); + const pageColors = + AppOptions.get("forcePageColors") || + window.matchMedia("(forced-colors: active)").matches + ? { + background: AppOptions.get("pageColorsBackground"), + foreground: AppOptions.get("pageColorsForeground"), + } + : null; + let altTextManager; + if (AppOptions.get("enableUpdatedAddImage")) { + altTextManager = appConfig.newAltTextDialog + ? new NewAltTextManager( + appConfig.newAltTextDialog, + this.overlayManager, + eventBus, + ) + : null; + } else { + altTextManager = appConfig.altTextDialog + ? new AltTextManager( + appConfig.altTextDialog, + container, + this.overlayManager, + eventBus, + ) + : null; + } + if (appConfig.editorUndoBar) { + this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus); + } + const enableHWA = AppOptions.get("enableHWA"); + const pdfViewer = new PDFViewer({ + container, + viewer, + eventBus, + renderingQueue: pdfRenderingQueue, + linkService: pdfLinkService, + downloadManager, + altTextManager, + editorUndoBar: this.editorUndoBar, + findController, + scriptingManager: + AppOptions.get("enableScripting") && pdfScriptingManager, + l10n, + textLayerMode: AppOptions.get("textLayerMode"), + annotationMode: AppOptions.get("annotationMode"), + annotationEditorMode, + annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"), + enableHighlightFloatingButton: AppOptions.get( + "enableHighlightFloatingButton", + ), + enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"), + enableNewAltTextWhenAddingImage: AppOptions.get( + "enableNewAltTextWhenAddingImage", + ), + imageResourcesPath: AppOptions.get("imageResourcesPath"), + enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), + maxCanvasPixels: AppOptions.get("maxCanvasPixels"), + enablePermissions: AppOptions.get("enablePermissions"), + pageColors, + mlManager: this.mlManager, + abortSignal: this._globalAbortController.signal, + enableHWA, + supportsPinchToZoom: this.supportsPinchToZoom, + }); + this.pdfViewer = pdfViewer; + pdfRenderingQueue.setViewer(pdfViewer); + pdfLinkService.setViewer(pdfViewer); + pdfScriptingManager.setViewer(pdfViewer); + if (appConfig.sidebar?.thumbnailView) { + this.pdfThumbnailViewer = new PDFThumbnailViewer({ + container: appConfig.sidebar.thumbnailView, + eventBus, + renderingQueue: pdfRenderingQueue, + linkService: pdfLinkService, + pageColors, + abortSignal: this._globalAbortController.signal, + enableHWA, + }); + pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer); + } + if (!this.isViewerEmbedded && !AppOptions.get("disableHistory")) { + this.pdfHistory = new PDFHistory({ + linkService: pdfLinkService, + eventBus, + }); + pdfLinkService.setHistory(this.pdfHistory); + } + if (!this.supportsIntegratedFind && appConfig.findBar) { + this.findBar = new PDFFindBar( + appConfig.findBar, + appConfig.principalContainer, + eventBus, + ); + } + if (appConfig.annotationEditorParams) { + if ( + typeof AbortSignal.any === "function" && + annotationEditorMode !== AnnotationEditorType.DISABLE + ) { + this.annotationEditorParams = new AnnotationEditorParams( + appConfig.annotationEditorParams, + eventBus, + ); + } else { + for (const id of ["editorModeButtons", "editorModeSeparator"]) { + document.getElementById(id)?.classList.add("hidden"); + } + } + } + if ( + this.mlManager && + appConfig.secondaryToolbar?.imageAltTextSettingsButton + ) { + this.imageAltTextSettings = new ImageAltTextSettings( + appConfig.altTextSettingsDialog, + this.overlayManager, + eventBus, + this.mlManager, + ); + } + if (appConfig.documentProperties) { + this.pdfDocumentProperties = new PDFDocumentProperties( + appConfig.documentProperties, + this.overlayManager, + eventBus, + l10n, + () => this._docFilename, + ); + } + if (appConfig.secondaryToolbar?.cursorHandToolButton) { + this.pdfCursorTools = new PDFCursorTools({ + container, + eventBus, + cursorToolOnLoad: AppOptions.get("cursorToolOnLoad"), + }); + } + if (appConfig.toolbar) { + this.toolbar = new Toolbar( + appConfig.toolbar, + eventBus, + AppOptions.get("toolbarDensity"), + ); + } + if (appConfig.secondaryToolbar) { + if (AppOptions.get("enableAltText")) { + appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove( + "hidden", + ); + appConfig.secondaryToolbar.imageAltTextSettingsSeparator?.classList.remove( + "hidden", + ); + } + this.secondaryToolbar = new SecondaryToolbar( + appConfig.secondaryToolbar, + eventBus, + ); + } + if ( + this.supportsFullscreen && + appConfig.secondaryToolbar?.presentationModeButton + ) { + this.pdfPresentationMode = new PDFPresentationMode({ + container, + pdfViewer, + eventBus, + }); + } + if (appConfig.passwordOverlay) { + this.passwordPrompt = new PasswordPrompt( + appConfig.passwordOverlay, + this.overlayManager, + this.isViewerEmbedded, + ); + } + if (appConfig.sidebar?.outlineView) { + this.pdfOutlineViewer = new PDFOutlineViewer({ + container: appConfig.sidebar.outlineView, + eventBus, + l10n, + linkService: pdfLinkService, + downloadManager, + }); + } + if (appConfig.sidebar?.attachmentsView) { + this.pdfAttachmentViewer = new PDFAttachmentViewer({ + container: appConfig.sidebar.attachmentsView, + eventBus, + l10n, + downloadManager, + }); + } + if (appConfig.sidebar?.layersView) { + this.pdfLayerViewer = new PDFLayerViewer({ + container: appConfig.sidebar.layersView, + eventBus, + l10n, + }); + } + if (appConfig.sidebar) { + this.pdfSidebar = new PDFSidebar({ + elements: appConfig.sidebar, + eventBus, + l10n, + }); + this.pdfSidebar.onToggled = this.forceRendering.bind(this); + this.pdfSidebar.onUpdateThumbnails = () => { + for (const pageView of pdfViewer.getCachedPageViews()) { + if (pageView.renderingState === RenderingStates.FINISHED) { + this.pdfThumbnailViewer + .getThumbnail(pageView.id - 1) + ?.setImage(pageView); + } + } + this.pdfThumbnailViewer.scrollThumbnailIntoView( + pdfViewer.currentPageNumber, + ); + }; + } + }, + async run(config) { + await this.initialize(config); + const { appConfig, eventBus } = this; + let file; + const queryString = document.location.search.substring(1); + const params = parseQueryString(queryString); + file = params.get("file") ?? AppOptions.get("defaultUrl"); + validateFileURL(file); + const fileInput = (this._openFileInput = document.createElement("input")); + fileInput.id = "fileInput"; + fileInput.hidden = true; + fileInput.type = "file"; + fileInput.value = null; + document.body.append(fileInput); + fileInput.addEventListener("change", function (evt) { + const { files } = evt.target; + if (!files || files.length === 0) { + return; + } + eventBus.dispatch("fileinputchange", { + source: this, + fileInput: evt.target, + }); + }); + appConfig.mainContainer.addEventListener("dragover", function (evt) { + for (const item of evt.dataTransfer.items) { + if (item.type === "application/pdf") { + evt.dataTransfer.dropEffect = + evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move"; + stopEvent(evt); + return; + } + } + }); + appConfig.mainContainer.addEventListener("drop", function (evt) { + if (evt.dataTransfer.files?.[0].type !== "application/pdf") { + return; + } + stopEvent(evt); + eventBus.dispatch("fileinputchange", { + source: this, + fileInput: evt.dataTransfer, + }); + }); + if (!AppOptions.get("supportsDocumentFonts")) { + AppOptions.set("disableFontFace", true); + this.l10n.get("pdfjs-web-fonts-disabled").then((msg) => { + console.warn(msg); + }); + } + if (!this.supportsPrinting) { + appConfig.toolbar?.print?.classList.add("hidden"); + appConfig.secondaryToolbar?.printButton.classList.add("hidden"); + } + if (!this.supportsFullscreen) { + appConfig.secondaryToolbar?.presentationModeButton.classList.add( + "hidden", + ); + } + if (this.supportsIntegratedFind) { + appConfig.findBar?.toggleButton?.classList.add("hidden"); + } + if (file) { + this.open({ + url: file, + }); + } else { + this._hideViewBookmark(); + } + }, + get externalServices() { + return shadow(this, "externalServices", new ExternalServices()); + }, + get initialized() { + return this._initializedCapability.settled; + }, + get initializedPromise() { + return this._initializedCapability.promise; + }, + updateZoom(steps, scaleFactor, origin) { + if (this.pdfViewer.isInPresentationMode) { + return; + } + this.pdfViewer.updateScale({ + drawingDelay: AppOptions.get("defaultZoomDelay"), + steps, + scaleFactor, + origin, + }); + }, + zoomIn() { + this.updateZoom(1); + }, + zoomOut() { + this.updateZoom(-1); + }, + zoomReset() { + if (this.pdfViewer.isInPresentationMode) { + return; + } + this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE; + }, + touchPinchCallback(origin, prevDistance, distance) { + if (this.supportsPinchToZoom) { + const newScaleFactor = this._accumulateFactor( + this.pdfViewer.currentScale, + distance / prevDistance, + "_touchUnusedFactor", + ); + this.updateZoom(null, newScaleFactor, origin); + } else { + const PIXELS_PER_LINE_SCALE = 30; + const ticks = this._accumulateTicks( + (distance - prevDistance) / PIXELS_PER_LINE_SCALE, + "_touchUnusedTicks", + ); + this.updateZoom(ticks, null, origin); + } + }, + touchPinchEndCallback() { + this._touchUnusedTicks = 0; + this._touchUnusedFactor = 1; + }, + get pagesCount() { + return this.pdfDocument ? this.pdfDocument.numPages : 0; + }, + get page() { + return this.pdfViewer.currentPageNumber; + }, + set page(val) { + this.pdfViewer.currentPageNumber = val; + }, + get supportsPrinting() { + return PDFPrintServiceFactory.supportsPrinting; + }, + get supportsFullscreen() { + return shadow(this, "supportsFullscreen", document.fullscreenEnabled); + }, + get supportsPinchToZoom() { + return shadow( + this, + "supportsPinchToZoom", + AppOptions.get("supportsPinchToZoom"), + ); + }, + get supportsIntegratedFind() { + return shadow( + this, + "supportsIntegratedFind", + AppOptions.get("supportsIntegratedFind"), + ); + }, + get loadingBar() { + const barElement = document.getElementById("loadingBar"); + const bar = barElement ? new ProgressBar(barElement) : null; + return shadow(this, "loadingBar", bar); + }, + get supportsMouseWheelZoomCtrlKey() { + return shadow( + this, + "supportsMouseWheelZoomCtrlKey", + AppOptions.get("supportsMouseWheelZoomCtrlKey"), + ); + }, + get supportsMouseWheelZoomMetaKey() { + return shadow( + this, + "supportsMouseWheelZoomMetaKey", + AppOptions.get("supportsMouseWheelZoomMetaKey"), + ); + }, + get supportsCaretBrowsingMode() { + return AppOptions.get("supportsCaretBrowsingMode"); + }, + moveCaret(isUp, select) { + this._caretBrowsing ||= new CaretBrowsingMode( + this._globalAbortController.signal, + this.appConfig.mainContainer, + this.appConfig.viewerContainer, + this.appConfig.toolbar?.container, + ); + this._caretBrowsing.moveCaret(isUp, select); + }, + setTitleUsingUrl(url = "", downloadUrl = null) { + this.url = url; + this.baseUrl = url.split("#", 1)[0]; + if (downloadUrl) { + this._downloadUrl = + downloadUrl === url ? this.baseUrl : downloadUrl.split("#", 1)[0]; + } + if (isDataScheme(url)) { + this._hideViewBookmark(); + } + let title = pdfjs_getPdfFilenameFromUrl(url, ""); + if (!title) { + try { + title = decodeURIComponent(getFilenameFromUrl(url)); + } catch {} + } + this.setTitle(title || url); + }, + setTitle(title = this._title) { + this._title = title; + if (this.isViewerEmbedded) { + return; + } + const editorIndicator = + this._hasAnnotationEditors && !this.pdfRenderingQueue.printing; + document.title = `${editorIndicator ? "* " : ""}${title}`; + }, + get _docFilename() { + return ( + this._contentDispositionFilename || pdfjs_getPdfFilenameFromUrl(this.url) + ); + }, + _hideViewBookmark() { + const { secondaryToolbar } = this.appConfig; + secondaryToolbar?.viewBookmarkButton.classList.add("hidden"); + if (secondaryToolbar?.presentationModeButton.classList.contains("hidden")) { + document.getElementById("viewBookmarkSeparator")?.classList.add("hidden"); + } + }, + async close() { + this._unblockDocumentLoadEvent(); + this._hideViewBookmark(); + if (!this.pdfLoadingTask) { + return; + } + if ( + this.pdfDocument?.annotationStorage.size > 0 && + this._annotationStorageModified + ) { + try { + await this.save(); + } catch {} + } + const promises = []; + promises.push(this.pdfLoadingTask.destroy()); + this.pdfLoadingTask = null; + if (this.pdfDocument) { + this.pdfDocument = null; + this.pdfThumbnailViewer?.setDocument(null); + this.pdfViewer.setDocument(null); + this.pdfLinkService.setDocument(null); + this.pdfDocumentProperties?.setDocument(null); + } + this.pdfLinkService.externalLinkEnabled = true; + this.store = null; + this.isInitialViewSet = false; + this.url = ""; + this.baseUrl = ""; + this._downloadUrl = ""; + this.documentInfo = null; + this.metadata = null; + this._contentDispositionFilename = null; + this._contentLength = null; + this._saveInProgress = false; + this._hasAnnotationEditors = false; + promises.push( + this.pdfScriptingManager.destroyPromise, + this.passwordPrompt.close(), + ); + this.setTitle(); + this.pdfSidebar?.reset(); + this.pdfOutlineViewer?.reset(); + this.pdfAttachmentViewer?.reset(); + this.pdfLayerViewer?.reset(); + this.pdfHistory?.reset(); + this.findBar?.reset(); + this.toolbar?.reset(); + this.secondaryToolbar?.reset(); + this._PDFBug?.cleanup(); + await Promise.all(promises); + }, + async open(args) { + if (this.pdfLoadingTask) { + await this.close(); + } + const workerParams = AppOptions.getAll(OptionKind.WORKER); + Object.assign(GlobalWorkerOptions, workerParams); + if (args.url) { + this.setTitleUsingUrl(args.originalUrl || args.url, args.url); + } + const apiParams = AppOptions.getAll(OptionKind.API); + const loadingTask = getDocument({ + ...apiParams, + ...args, + }); + this.pdfLoadingTask = loadingTask; + loadingTask.onPassword = (updateCallback, reason) => { + if (this.isViewerEmbedded) { + this._unblockDocumentLoadEvent(); + } + this.pdfLinkService.externalLinkEnabled = false; + this.passwordPrompt.setUpdateCallback(updateCallback, reason); + this.passwordPrompt.open(); + }; + loadingTask.onProgress = ({ loaded, total }) => { + this.progress(loaded / total); + }; + return loadingTask.promise.then( + (pdfDocument) => { + this.load(pdfDocument); + }, + (reason) => { + if (loadingTask !== this.pdfLoadingTask) { + return undefined; + } + let key = "pdfjs-loading-error"; + if (reason instanceof InvalidPDFException) { + key = "pdfjs-invalid-file-error"; + } else if (reason instanceof MissingPDFException) { + key = "pdfjs-missing-file-error"; + } else if (reason instanceof UnexpectedResponseException) { + key = "pdfjs-unexpected-response-error"; + } + return this._documentError(key, { + message: reason.message, + }).then(() => { + throw reason; + }); + }, + ); + }, + async download() { + let data; + try { + data = await this.pdfDocument.getData(); + } catch {} + this.downloadManager.download(data, this._downloadUrl, this._docFilename); + }, + async save() { + if (this._saveInProgress) { + return; + } + this._saveInProgress = true; + await this.pdfScriptingManager.dispatchWillSave(); + try { + const data = await this.pdfDocument.saveDocument(); + this.downloadManager.download(data, this._downloadUrl, this._docFilename); + } catch (reason) { + console.error(`Error when saving the document:`, reason); + await this.download(); + } finally { + await this.pdfScriptingManager.dispatchDidSave(); + this._saveInProgress = false; + } + if (this._hasAnnotationEditors) { + this.externalServices.reportTelemetry({ + type: "editing", + data: { + type: "save", + stats: this.pdfDocument?.annotationStorage.editorStats, + }, + }); + } + }, + async downloadOrSave() { + const { classList } = this.appConfig.appContainer; + classList.add("wait"); + await (this.pdfDocument?.annotationStorage.size > 0 + ? this.save() + : this.download()); + classList.remove("wait"); + }, + async _documentError(key, moreInfo = null) { + this._unblockDocumentLoadEvent(); + const message = await this._otherError( + key || "pdfjs-loading-error", + moreInfo, + ); + this.eventBus.dispatch("documenterror", { + source: this, + message, + reason: moreInfo?.message ?? null, + }); + }, + async _otherError(key, moreInfo = null) { + const message = await this.l10n.get(key); + const moreInfoText = [`PDF.js v${version || "?"} (build: ${build || "?"})`]; + if (moreInfo) { + moreInfoText.push(`Message: ${moreInfo.message}`); + if (moreInfo.stack) { + moreInfoText.push(`Stack: ${moreInfo.stack}`); + } else { + if (moreInfo.filename) { + moreInfoText.push(`File: ${moreInfo.filename}`); + } + if (moreInfo.lineNumber) { + moreInfoText.push(`Line: ${moreInfo.lineNumber}`); + } + } + } + console.error(`${message}\n\n${moreInfoText.join("\n")}`); + return message; + }, + progress(level) { + const percent = Math.round(level * 100); + if (!this.loadingBar || percent <= this.loadingBar.percent) { + return; + } + this.loadingBar.percent = percent; + if ( + this.pdfDocument?.loadingParams.disableAutoFetch ?? + AppOptions.get("disableAutoFetch") + ) { + this.loadingBar.setDisableAutoFetch(); + } + }, + load(pdfDocument) { + this.pdfDocument = pdfDocument; + pdfDocument.getDownloadInfo().then(({ length }) => { + this._contentLength = length; + this.loadingBar?.hide(); + firstPagePromise.then(() => { + this.eventBus.dispatch("documentloaded", { + source: this, + }); + }); + }); + const pageLayoutPromise = pdfDocument.getPageLayout().catch(() => {}); + const pageModePromise = pdfDocument.getPageMode().catch(() => {}); + const openActionPromise = pdfDocument.getOpenAction().catch(() => {}); + this.toolbar?.setPagesCount(pdfDocument.numPages, false); + this.secondaryToolbar?.setPagesCount(pdfDocument.numPages); + this.pdfLinkService.setDocument(pdfDocument); + this.pdfDocumentProperties?.setDocument(pdfDocument); + const pdfViewer = this.pdfViewer; + pdfViewer.setDocument(pdfDocument); + const { firstPagePromise, onePageRendered, pagesPromise } = pdfViewer; + this.pdfThumbnailViewer?.setDocument(pdfDocument); + const storedPromise = (this.store = new ViewHistory( + pdfDocument.fingerprints[0], + )) + .getMultiple({ + page: null, + zoom: DEFAULT_SCALE_VALUE, + scrollLeft: "0", + scrollTop: "0", + rotation: null, + sidebarView: SidebarView.UNKNOWN, + scrollMode: ScrollMode.UNKNOWN, + spreadMode: SpreadMode.UNKNOWN, + }) + .catch(() => {}); + firstPagePromise.then((pdfPage) => { + this.loadingBar?.setWidth(this.appConfig.viewerContainer); + this._initializeAnnotationStorageCallbacks(pdfDocument); + Promise.all([ + animationStarted, + storedPromise, + pageLayoutPromise, + pageModePromise, + openActionPromise, + ]) + .then(async ([timeStamp, stored, pageLayout, pageMode, openAction]) => { + const viewOnLoad = AppOptions.get("viewOnLoad"); + this._initializePdfHistory({ + fingerprint: pdfDocument.fingerprints[0], + viewOnLoad, + initialDest: openAction?.dest, + }); + const initialBookmark = this.initialBookmark; + const zoom = AppOptions.get("defaultZoomValue"); + let hash = zoom ? `zoom=${zoom}` : null; + let rotation = null; + let sidebarView = AppOptions.get("sidebarViewOnLoad"); + let scrollMode = AppOptions.get("scrollModeOnLoad"); + let spreadMode = AppOptions.get("spreadModeOnLoad"); + if (stored?.page && viewOnLoad !== ViewOnLoad.INITIAL) { + hash = + `page=${stored.page}&zoom=${zoom || stored.zoom},` + + `${stored.scrollLeft},${stored.scrollTop}`; + rotation = parseInt(stored.rotation, 10); + if (sidebarView === SidebarView.UNKNOWN) { + sidebarView = stored.sidebarView | 0; + } + if (scrollMode === ScrollMode.UNKNOWN) { + scrollMode = stored.scrollMode | 0; + } + if (spreadMode === SpreadMode.UNKNOWN) { + spreadMode = stored.spreadMode | 0; + } + } + if (pageMode && sidebarView === SidebarView.UNKNOWN) { + sidebarView = apiPageModeToSidebarView(pageMode); + } + if ( + pageLayout && + scrollMode === ScrollMode.UNKNOWN && + spreadMode === SpreadMode.UNKNOWN + ) { + const modes = apiPageLayoutToViewerModes(pageLayout); + spreadMode = modes.spreadMode; + } + this.setInitialView(hash, { + rotation, + sidebarView, + scrollMode, + spreadMode, + }); + this.eventBus.dispatch("documentinit", { + source: this, + }); + if (!this.isViewerEmbedded) { + pdfViewer.focus(); + } + await Promise.race([ + pagesPromise, + new Promise((resolve) => { + setTimeout(resolve, FORCE_PAGES_LOADED_TIMEOUT); + }), + ]); + if (!initialBookmark && !hash) { + return; + } + if (pdfViewer.hasEqualPageSizes) { + return; + } + this.initialBookmark = initialBookmark; + pdfViewer.currentScaleValue = pdfViewer.currentScaleValue; + this.setInitialView(hash); + }) + .catch(() => { + this.setInitialView(); + }) + .then(function () { + pdfViewer.update(); + }); + }); + pagesPromise.then( + () => { + this._unblockDocumentLoadEvent(); + this._initializeAutoPrint(pdfDocument, openActionPromise); + }, + (reason) => { + this._documentError("pdfjs-loading-error", { + message: reason.message, + }); + }, + ); + onePageRendered.then((data) => { + this.externalServices.reportTelemetry({ + type: "pageInfo", + timestamp: data.timestamp, + }); + if (this.pdfOutlineViewer) { + pdfDocument.getOutline().then((outline) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.pdfOutlineViewer.render({ + outline, + pdfDocument, + }); + }); + } + if (this.pdfAttachmentViewer) { + pdfDocument.getAttachments().then((attachments) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.pdfAttachmentViewer.render({ + attachments, + }); + }); + } + if (this.pdfLayerViewer) { + pdfViewer.optionalContentConfigPromise.then((optionalContentConfig) => { + if (pdfDocument !== this.pdfDocument) { + return; + } + this.pdfLayerViewer.render({ + optionalContentConfig, + pdfDocument, + }); + }); + } + }); + this._initializePageLabels(pdfDocument); + this._initializeMetadata(pdfDocument); + }, + async _scriptingDocProperties(pdfDocument) { + if (!this.documentInfo) { + await new Promise((resolve) => { + this.eventBus._on("metadataloaded", resolve, { + once: true, + }); + }); + if (pdfDocument !== this.pdfDocument) { + return null; + } + } + if (!this._contentLength) { + await new Promise((resolve) => { + this.eventBus._on("documentloaded", resolve, { + once: true, + }); + }); + if (pdfDocument !== this.pdfDocument) { + return null; + } + } + return { + ...this.documentInfo, + baseURL: this.baseUrl, + filesize: this._contentLength, + filename: this._docFilename, + metadata: this.metadata?.getRaw(), + authors: this.metadata?.get("dc:creator"), + numPages: this.pagesCount, + URL: this.url, + }; + }, + async _initializeAutoPrint(pdfDocument, openActionPromise) { + const [openAction, jsActions] = await Promise.all([ + openActionPromise, + this.pdfViewer.enableScripting ? null : pdfDocument.getJSActions(), + ]); + if (pdfDocument !== this.pdfDocument) { + return; + } + let triggerAutoPrint = openAction?.action === "Print"; + if (jsActions) { + console.warn("Warning: JavaScript support is not enabled"); + for (const name in jsActions) { + if (triggerAutoPrint) { + break; + } + switch (name) { + case "WillClose": + case "WillSave": + case "DidSave": + case "WillPrint": + case "DidPrint": + continue; + } + triggerAutoPrint = jsActions[name].some((js) => + AutoPrintRegExp.test(js), + ); + } + } + if (triggerAutoPrint) { + this.triggerPrinting(); + } + }, + async _initializeMetadata(pdfDocument) { + const { info, metadata, contentDispositionFilename, contentLength } = + await pdfDocument.getMetadata(); + if (pdfDocument !== this.pdfDocument) { + return; + } + this.documentInfo = info; + this.metadata = metadata; + this._contentDispositionFilename ??= contentDispositionFilename; + this._contentLength ??= contentLength; + console.log( + `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` + + `${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` + + `(PDF.js: ${version || "?"} [${build || "?"}])`, + ); + let pdfTitle = info.Title; + const metadataTitle = metadata?.get("dc:title"); + if (metadataTitle) { + if ( + metadataTitle !== "Untitled" && + !/[\uFFF0-\uFFFF]/g.test(metadataTitle) + ) { + pdfTitle = metadataTitle; + } + } + if (pdfTitle) { + this.setTitle( + `${pdfTitle} - ${this._contentDispositionFilename || this._title}`, + ); + } else if (this._contentDispositionFilename) { + this.setTitle(this._contentDispositionFilename); + } + if ( + info.IsXFAPresent && + !info.IsAcroFormPresent && + !pdfDocument.isPureXfa + ) { + if (pdfDocument.loadingParams.enableXfa) { + console.warn("Warning: XFA Foreground documents are not supported"); + } else { + console.warn("Warning: XFA support is not enabled"); + } + } else if ( + (info.IsAcroFormPresent || info.IsXFAPresent) && + !this.pdfViewer.renderForms + ) { + console.warn("Warning: Interactive form support is not enabled"); + } + if (info.IsSignaturesPresent) { + console.warn("Warning: Digital signatures validation is not supported"); + } + this.eventBus.dispatch("metadataloaded", { + source: this, + }); + }, + async _initializePageLabels(pdfDocument) { + const labels = await pdfDocument.getPageLabels(); + if (pdfDocument !== this.pdfDocument) { + return; + } + if (!labels || AppOptions.get("disablePageLabels")) { + return; + } + const numLabels = labels.length; + let standardLabels = 0, + emptyLabels = 0; + for (let i = 0; i < numLabels; i++) { + const label = labels[i]; + if (label === (i + 1).toString()) { + standardLabels++; + } else if (label === "") { + emptyLabels++; + } else { + break; + } + } + if (standardLabels >= numLabels || emptyLabels >= numLabels) { + return; + } + const { pdfViewer, pdfThumbnailViewer, toolbar } = this; + pdfViewer.setPageLabels(labels); + pdfThumbnailViewer?.setPageLabels(labels); + toolbar?.setPagesCount(numLabels, true); + toolbar?.setPageNumber( + pdfViewer.currentPageNumber, + pdfViewer.currentPageLabel, + ); + }, + _initializePdfHistory({ fingerprint, viewOnLoad, initialDest = null }) { + if (!this.pdfHistory) { + return; + } + this.pdfHistory.initialize({ + fingerprint, + resetHistory: viewOnLoad === ViewOnLoad.INITIAL, + updateUrl: AppOptions.get("historyUpdateUrl"), + }); + if (this.pdfHistory.initialBookmark) { + this.initialBookmark = this.pdfHistory.initialBookmark; + this.initialRotation = this.pdfHistory.initialRotation; + } + if ( + initialDest && + !this.initialBookmark && + viewOnLoad === ViewOnLoad.UNKNOWN + ) { + this.initialBookmark = JSON.stringify(initialDest); + this.pdfHistory.push({ + explicitDest: initialDest, + pageNumber: null, + }); + } + }, + _initializeAnnotationStorageCallbacks(pdfDocument) { + if (pdfDocument !== this.pdfDocument) { + return; + } + const { annotationStorage } = pdfDocument; + annotationStorage.onSetModified = () => { + window.addEventListener("beforeunload", beforeUnload); + this._annotationStorageModified = true; + }; + annotationStorage.onResetModified = () => { + window.removeEventListener("beforeunload", beforeUnload); + delete this._annotationStorageModified; + }; + annotationStorage.onAnnotationEditor = (typeStr) => { + this._hasAnnotationEditors = !!typeStr; + this.setTitle(); + }; + }, + setInitialView( + storedHash, + { rotation, sidebarView, scrollMode, spreadMode } = {}, + ) { + const setRotation = (angle) => { + if (isValidRotation(angle)) { + this.pdfViewer.pagesRotation = angle; + } + }; + const setViewerModes = (scroll, spread) => { + if (isValidScrollMode(scroll)) { + this.pdfViewer.scrollMode = scroll; + } + if (isValidSpreadMode(spread)) { + this.pdfViewer.spreadMode = spread; + } + }; + this.isInitialViewSet = true; + this.pdfSidebar?.setInitialView(sidebarView); + setViewerModes(scrollMode, spreadMode); + if (this.initialBookmark) { + setRotation(this.initialRotation); + delete this.initialRotation; + this.pdfLinkService.setHash(this.initialBookmark); + this.initialBookmark = null; + } else if (storedHash) { + setRotation(rotation); + this.pdfLinkService.setHash(storedHash); + } + this.toolbar?.setPageNumber( + this.pdfViewer.currentPageNumber, + this.pdfViewer.currentPageLabel, + ); + this.secondaryToolbar?.setPageNumber(this.pdfViewer.currentPageNumber); + if (!this.pdfViewer.currentScaleValue) { + this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE; + } + }, + _cleanup() { + if (!this.pdfDocument) { + return; + } + this.pdfViewer.cleanup(); + this.pdfThumbnailViewer?.cleanup(); + this.pdfDocument.cleanup(AppOptions.get("fontExtraProperties")); + }, + forceRendering() { + this.pdfRenderingQueue.printing = !!this.printService; + this.pdfRenderingQueue.isThumbnailViewEnabled = + this.pdfSidebar?.visibleView === SidebarView.THUMBS; + this.pdfRenderingQueue.renderHighestPriority(); + }, + beforePrint() { + this._printAnnotationStoragePromise = this.pdfScriptingManager + .dispatchWillPrint() + .catch(() => {}) + .then(() => this.pdfDocument?.annotationStorage.print); + if (this.printService) { + return; + } + if (!this.supportsPrinting) { + this._otherError("pdfjs-printing-not-supported"); + return; + } + if (!this.pdfViewer.pageViewsReady) { + this.l10n.get("pdfjs-printing-not-ready").then((msg) => { + window.alert(msg); + }); + return; + } + this.printService = PDFPrintServiceFactory.createPrintService({ + pdfDocument: this.pdfDocument, + pagesOverview: this.pdfViewer.getPagesOverview(), + printContainer: this.appConfig.printContainer, + printResolution: AppOptions.get("printResolution"), + printAnnotationStoragePromise: this._printAnnotationStoragePromise, + }); + this.forceRendering(); + this.setTitle(); + this.printService.layout(); + if (this._hasAnnotationEditors) { + this.externalServices.reportTelemetry({ + type: "editing", + data: { + type: "print", + stats: this.pdfDocument?.annotationStorage.editorStats, + }, + }); + } + }, + afterPrint() { + if (this._printAnnotationStoragePromise) { + this._printAnnotationStoragePromise.then(() => { + this.pdfScriptingManager.dispatchDidPrint(); + }); + this._printAnnotationStoragePromise = null; + } + if (this.printService) { + this.printService.destroy(); + this.printService = null; + this.pdfDocument?.annotationStorage.resetModified(); + } + this.forceRendering(); + this.setTitle(); + }, + rotatePages(delta) { + this.pdfViewer.pagesRotation += delta; + }, + requestPresentationMode() { + this.pdfPresentationMode?.request(); + }, + triggerPrinting() { + if (this.supportsPrinting) { + window.print(); + } + }, + bindEvents() { + if (this._eventBusAbortController) { + return; + } + const ac = (this._eventBusAbortController = new AbortController()); + const opts = { + signal: ac.signal, + }; + const { + eventBus, + externalServices, + pdfDocumentProperties, + pdfViewer, + preferences, + } = this; + eventBus._on("resize", onResize.bind(this), opts); + eventBus._on("hashchange", onHashchange.bind(this), opts); + eventBus._on("beforeprint", this.beforePrint.bind(this), opts); + eventBus._on("afterprint", this.afterPrint.bind(this), opts); + eventBus._on("pagerender", onPageRender.bind(this), opts); + eventBus._on("pagerendered", onPageRendered.bind(this), opts); + eventBus._on("updateviewarea", onUpdateViewarea.bind(this), opts); + eventBus._on("pagechanging", onPageChanging.bind(this), opts); + eventBus._on("scalechanging", onScaleChanging.bind(this), opts); + eventBus._on("rotationchanging", onRotationChanging.bind(this), opts); + eventBus._on("sidebarviewchanged", onSidebarViewChanged.bind(this), opts); + eventBus._on("pagemode", onPageMode.bind(this), opts); + eventBus._on("namedaction", onNamedAction.bind(this), opts); + eventBus._on( + "presentationmodechanged", + (evt) => (pdfViewer.presentationModeState = evt.state), + opts, + ); + eventBus._on( + "presentationmode", + this.requestPresentationMode.bind(this), + opts, + ); + eventBus._on( + "switchannotationeditormode", + (evt) => (pdfViewer.annotationEditorMode = evt), + opts, + ); + eventBus._on("print", this.triggerPrinting.bind(this), opts); + eventBus._on("download", this.downloadOrSave.bind(this), opts); + eventBus._on("firstpage", () => (this.page = 1), opts); + eventBus._on("lastpage", () => (this.page = this.pagesCount), opts); + eventBus._on("nextpage", () => pdfViewer.nextPage(), opts); + eventBus._on("previouspage", () => pdfViewer.previousPage(), opts); + eventBus._on("zoomin", this.zoomIn.bind(this), opts); + eventBus._on("zoomout", this.zoomOut.bind(this), opts); + eventBus._on("zoomreset", this.zoomReset.bind(this), opts); + eventBus._on("pagenumberchanged", onPageNumberChanged.bind(this), opts); + eventBus._on( + "scalechanged", + (evt) => (pdfViewer.currentScaleValue = evt.value), + opts, + ); + eventBus._on("rotatecw", this.rotatePages.bind(this, 90), opts); + eventBus._on("rotateccw", this.rotatePages.bind(this, -90), opts); + eventBus._on( + "optionalcontentconfig", + (evt) => (pdfViewer.optionalContentConfigPromise = evt.promise), + opts, + ); + eventBus._on( + "switchscrollmode", + (evt) => (pdfViewer.scrollMode = evt.mode), + opts, + ); + eventBus._on( + "scrollmodechanged", + onViewerModesChanged.bind(this, "scrollMode"), + opts, + ); + eventBus._on( + "switchspreadmode", + (evt) => (pdfViewer.spreadMode = evt.mode), + opts, + ); + eventBus._on( + "spreadmodechanged", + onViewerModesChanged.bind(this, "spreadMode"), + opts, + ); + eventBus._on( + "imagealttextsettings", + onImageAltTextSettings.bind(this), + opts, + ); + eventBus._on( + "documentproperties", + () => pdfDocumentProperties?.open(), + opts, + ); + eventBus._on("findfromurlhash", onFindFromUrlHash.bind(this), opts); + eventBus._on( + "updatefindmatchescount", + onUpdateFindMatchesCount.bind(this), + opts, + ); + eventBus._on( + "updatefindcontrolstate", + onUpdateFindControlState.bind(this), + opts, + ); + eventBus._on("fileinputchange", onFileInputChange.bind(this), opts); + eventBus._on("openfile", onOpenFile.bind(this), opts); + }, + bindWindowEvents() { + if (this._windowAbortController) { + return; + } + this._windowAbortController = new AbortController(); + const { + eventBus, + appConfig: { mainContainer }, + pdfViewer, + _windowAbortController: { signal }, + } = this; + if (typeof AbortSignal.any === "function") { + this._touchManager = new TouchManager({ + container: window, + isPinchingDisabled: () => pdfViewer.isInPresentationMode, + isPinchingStopped: () => this.overlayManager?.active, + onPinching: this.touchPinchCallback.bind(this), + onPinchEnd: this.touchPinchEndCallback.bind(this), + signal, + }); + } + function addWindowResolutionChange(evt = null) { + if (evt) { + pdfViewer.refresh(); + } + const mediaQueryList = window.matchMedia( + `(resolution: ${window.devicePixelRatio || 1}dppx)`, + ); + mediaQueryList.addEventListener("change", addWindowResolutionChange, { + once: true, + signal, + }); + } + addWindowResolutionChange(); + window.addEventListener("wheel", onWheel.bind(this), { + passive: false, + signal, + }); + window.addEventListener("click", onClick.bind(this), { + signal, + }); + window.addEventListener("keydown", onKeyDown.bind(this), { + signal, + }); + window.addEventListener("keyup", onKeyUp.bind(this), { + signal, + }); + window.addEventListener( + "resize", + () => + eventBus.dispatch("resize", { + source: window, + }), + { + signal, + }, + ); + window.addEventListener( + "hashchange", + () => { + eventBus.dispatch("hashchange", { + source: window, + hash: document.location.hash.substring(1), + }); + }, + { + signal, + }, + ); + window.addEventListener( + "beforeprint", + () => + eventBus.dispatch("beforeprint", { + source: window, + }), + { + signal, + }, + ); + window.addEventListener( + "afterprint", + () => + eventBus.dispatch("afterprint", { + source: window, + }), + { + signal, + }, + ); + window.addEventListener( + "updatefromsandbox", + (evt) => { + eventBus.dispatch("updatefromsandbox", { + source: window, + detail: evt.detail, + }); + }, + { + signal, + }, + ); + if (!("onscrollend" in document.documentElement)) { + return; + } + ({ scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft } = + mainContainer); + const scrollend = () => { + ({ scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft } = + mainContainer); + this._isScrolling = false; + mainContainer.addEventListener("scroll", scroll, { + passive: true, + signal, + }); + mainContainer.removeEventListener("scrollend", scrollend); + mainContainer.removeEventListener("blur", scrollend); + }; + const scroll = () => { + if (this._isCtrlKeyDown) { + return; + } + if ( + this._lastScrollTop === mainContainer.scrollTop && + this._lastScrollLeft === mainContainer.scrollLeft + ) { + return; + } + mainContainer.removeEventListener("scroll", scroll); + this._isScrolling = true; + mainContainer.addEventListener("scrollend", scrollend, { + signal, + }); + mainContainer.addEventListener("blur", scrollend, { + signal, + }); + }; + mainContainer.addEventListener("scroll", scroll, { + passive: true, + signal, + }); + }, + unbindEvents() { + this._eventBusAbortController?.abort(); + this._eventBusAbortController = null; + }, + unbindWindowEvents() { + this._windowAbortController?.abort(); + this._windowAbortController = null; + this._touchManager = null; + }, + async testingClose() { + this.unbindEvents(); + this.unbindWindowEvents(); + this._globalAbortController?.abort(); + this._globalAbortController = null; + this.findBar?.close(); + await Promise.all([this.l10n?.destroy(), this.close()]); + }, + _accumulateTicks(ticks, prop) { + if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) { + this[prop] = 0; + } + this[prop] += ticks; + const wholeTicks = Math.trunc(this[prop]); + this[prop] -= wholeTicks; + return wholeTicks; + }, + _accumulateFactor(previousScale, factor, prop) { + if (factor === 1) { + return 1; + } + if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) { + this[prop] = 1; + } + const newFactor = + Math.floor(previousScale * factor * this[prop] * 100) / + (100 * previousScale); + this[prop] = factor / newFactor; + return newFactor; + }, + _unblockDocumentLoadEvent() { + document.blockUnblockOnload?.(false); + this._unblockDocumentLoadEvent = () => {}; + }, + get scriptingReady() { + return this.pdfScriptingManager.ready; + }, +}; +initCom(PDFViewerApplication); +{ + PDFPrintServiceFactory.initGlobals(PDFViewerApplication); +} +{ + const HOSTED_VIEWER_ORIGINS = [ + "null", + "http://mozilla.github.io", + "https://mozilla.github.io", + ]; + var validateFileURL = function (file) { + if (!file) { + return; + } + try { + } catch (ex) { + PDFViewerApplication._documentError("pdfjs-loading-error", { + message: ex.message, + }); + throw ex; + } + }; + var onFileInputChange = function (evt) { + if (this.pdfViewer?.isInPresentationMode) { + return; + } + const file = evt.fileInput.files[0]; + this.open({ + url: URL.createObjectURL(file), + originalUrl: file.name, + }); + }; + var onOpenFile = function (evt) { + this._openFileInput?.click(); + }; +} +function onPageRender({ pageNumber }) { + if (pageNumber === this.page) { + this.toolbar?.updateLoadingIndicatorState(true); + } +} +function onPageRendered({ pageNumber, error }) { + if (pageNumber === this.page) { + this.toolbar?.updateLoadingIndicatorState(false); + } + if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) { + const pageView = this.pdfViewer.getPageView(pageNumber - 1); + const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(pageNumber - 1); + if (pageView) { + thumbnailView?.setImage(pageView); + } + } + if (error) { + this._otherError("pdfjs-rendering-error", error); + } +} +function onPageMode({ mode }) { + let view; + switch (mode) { + case "thumbs": + view = SidebarView.THUMBS; + break; + case "bookmarks": + case "outline": + view = SidebarView.OUTLINE; + break; + case "attachments": + view = SidebarView.ATTACHMENTS; + break; + case "layers": + view = SidebarView.LAYERS; + break; + case "none": + view = SidebarView.NONE; + break; + default: + console.error('Invalid "pagemode" hash parameter: ' + mode); + return; + } + this.pdfSidebar?.switchView(view, true); +} +function onNamedAction(evt) { + switch (evt.action) { + case "GoToPage": + this.appConfig.toolbar?.pageNumber.select(); + break; + case "Find": + if (!this.supportsIntegratedFind) { + this.findBar?.toggle(); + } + break; + case "Print": + this.triggerPrinting(); + break; + case "SaveAs": + this.downloadOrSave(); + break; + } +} +function onSidebarViewChanged({ view }) { + this.pdfRenderingQueue.isThumbnailViewEnabled = view === SidebarView.THUMBS; + if (this.isInitialViewSet) { + this.store?.set("sidebarView", view).catch(() => {}); + } +} +function onUpdateViewarea({ location }) { + if (this.isInitialViewSet) { + this.store + ?.setMultiple({ + page: location.pageNumber, + zoom: location.scale, + scrollLeft: location.left, + scrollTop: location.top, + rotation: location.rotation, + }) + .catch(() => {}); + } + if (this.appConfig.secondaryToolbar) { + this.appConfig.secondaryToolbar.viewBookmarkButton.href = + this.pdfLinkService.getAnchorUrl(location.pdfOpenParams); + } +} +function onViewerModesChanged(name, evt) { + if (this.isInitialViewSet && !this.pdfViewer.isInPresentationMode) { + this.store?.set(name, evt.mode).catch(() => {}); + } +} +function onResize() { + const { pdfDocument, pdfViewer, pdfRenderingQueue } = this; + if (pdfRenderingQueue.printing && window.matchMedia("print").matches) { + return; + } + if (!pdfDocument) { + return; + } + const currentScaleValue = pdfViewer.currentScaleValue; + if ( + currentScaleValue === "auto" || + currentScaleValue === "page-fit" || + currentScaleValue === "page-width" + ) { + pdfViewer.currentScaleValue = currentScaleValue; + } + pdfViewer.update(); +} +function onHashchange(evt) { + const hash = evt.hash; + if (!hash) { + return; + } + if (!this.isInitialViewSet) { + this.initialBookmark = hash; + } else if (!this.pdfHistory?.popStateInProgress) { + this.pdfLinkService.setHash(hash); + } +} +function onPageNumberChanged(evt) { + const { pdfViewer } = this; + if (evt.value !== "") { + this.pdfLinkService.goToPage(evt.value); + } + if ( + evt.value !== pdfViewer.currentPageNumber.toString() && + evt.value !== pdfViewer.currentPageLabel + ) { + this.toolbar?.setPageNumber( + pdfViewer.currentPageNumber, + pdfViewer.currentPageLabel, + ); + } +} +function onImageAltTextSettings() { + this.imageAltTextSettings?.open({ + enableGuessAltText: AppOptions.get("enableGuessAltText"), + enableNewAltTextWhenAddingImage: AppOptions.get( + "enableNewAltTextWhenAddingImage", + ), + }); +} +function onFindFromUrlHash(evt) { + this.eventBus.dispatch("find", { + source: evt.source, + type: "", + query: evt.query, + caseSensitive: false, + entireWord: false, + highlightAll: true, + findPrevious: false, + matchDiacritics: true, + }); +} +function onUpdateFindMatchesCount({ matchesCount }) { + if (this.supportsIntegratedFind) { + this.externalServices.updateFindMatchesCount(matchesCount); + } else { + this.findBar?.updateResultsCount(matchesCount); + } +} +function onUpdateFindControlState({ + state, + previous, + entireWord, + matchesCount, + rawQuery, +}) { + if (this.supportsIntegratedFind) { + this.externalServices.updateFindControlState({ + result: state, + findPrevious: previous, + entireWord, + matchesCount, + rawQuery, + }); + } else { + this.findBar?.updateUIState(state, previous, matchesCount); + } +} +function onScaleChanging(evt) { + this.toolbar?.setPageScale(evt.presetValue, evt.scale); + this.pdfViewer.update(); +} +function onRotationChanging(evt) { + if (this.pdfThumbnailViewer) { + this.pdfThumbnailViewer.pagesRotation = evt.pagesRotation; + } + this.forceRendering(); + this.pdfViewer.currentPageNumber = evt.pageNumber; +} +function onPageChanging({ pageNumber, pageLabel }) { + this.toolbar?.setPageNumber(pageNumber, pageLabel); + this.secondaryToolbar?.setPageNumber(pageNumber); + if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) { + this.pdfThumbnailViewer?.scrollThumbnailIntoView(pageNumber); + } + const currentPage = this.pdfViewer.getPageView(pageNumber - 1); + this.toolbar?.updateLoadingIndicatorState( + currentPage?.renderingState === RenderingStates.RUNNING, + ); +} +function onWheel(evt) { + const { + pdfViewer, + supportsMouseWheelZoomCtrlKey, + supportsMouseWheelZoomMetaKey, + supportsPinchToZoom, + } = this; + if (pdfViewer.isInPresentationMode) { + return; + } + const deltaMode = evt.deltaMode; + let scaleFactor = Math.exp(-evt.deltaY / 100); + const isBuiltInMac = false; + const isPinchToZoom = + evt.ctrlKey && + !this._isCtrlKeyDown && + deltaMode === WheelEvent.DOM_DELTA_PIXEL && + evt.deltaX === 0 && + (Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) && + evt.deltaZ === 0; + const origin = [evt.clientX, evt.clientY]; + if ( + isPinchToZoom || + (evt.ctrlKey && supportsMouseWheelZoomCtrlKey) || + (evt.metaKey && supportsMouseWheelZoomMetaKey) + ) { + evt.preventDefault(); + if ( + this._isScrolling || + document.visibilityState === "hidden" || + this.overlayManager.active + ) { + return; + } + if (isPinchToZoom && supportsPinchToZoom) { + scaleFactor = this._accumulateFactor( + pdfViewer.currentScale, + scaleFactor, + "_wheelUnusedFactor", + ); + this.updateZoom(null, scaleFactor, origin); + } else { + const delta = normalizeWheelEventDirection(evt); + let ticks = 0; + if ( + deltaMode === WheelEvent.DOM_DELTA_LINE || + deltaMode === WheelEvent.DOM_DELTA_PAGE + ) { + ticks = + Math.abs(delta) >= 1 + ? Math.sign(delta) + : this._accumulateTicks(delta, "_wheelUnusedTicks"); + } else { + const PIXELS_PER_LINE_SCALE = 30; + ticks = this._accumulateTicks( + delta / PIXELS_PER_LINE_SCALE, + "_wheelUnusedTicks", + ); + } + this.updateZoom(ticks, null, origin); + } + } +} +function closeSecondaryToolbar(evt) { + if (!this.secondaryToolbar?.isOpen) { + return; + } + const appConfig = this.appConfig; + if ( + this.pdfViewer.containsElement(evt.target) || + (appConfig.toolbar?.container.contains(evt.target) && + !appConfig.secondaryToolbar?.toggleButton.contains(evt.target)) + ) { + this.secondaryToolbar.close(); + } +} +function closeEditorUndoBar(evt) { + if (!this.editorUndoBar?.isOpen) { + return; + } + if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) { + this.editorUndoBar.hide(); + } +} +function onClick(evt) { + closeSecondaryToolbar.call(this, evt); + closeEditorUndoBar.call(this, evt); +} +function onKeyUp(evt) { + if (evt.key === "Control") { + this._isCtrlKeyDown = false; + } +} +function onKeyDown(evt) { + this._isCtrlKeyDown = evt.key === "Control"; + if ( + this.editorUndoBar?.isOpen && + evt.keyCode !== 9 && + evt.keyCode !== 16 && + !( + (evt.keyCode === 13 || evt.keyCode === 32) && + getActiveOrFocusedElement() === this.appConfig.editorUndoBar.undoButton + ) + ) { + this.editorUndoBar.hide(); + } + if (this.overlayManager.active) { + return; + } + const { eventBus, pdfViewer } = this; + const isViewerInPresentationMode = pdfViewer.isInPresentationMode; + let handled = false, + ensureViewerFocused = false; + const cmd = + (evt.ctrlKey ? 1 : 0) | + (evt.altKey ? 2 : 0) | + (evt.shiftKey ? 4 : 0) | + (evt.metaKey ? 8 : 0); + if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) { + switch (evt.keyCode) { + case 70: + if (!this.supportsIntegratedFind && !evt.shiftKey) { + this.findBar?.open(); + handled = true; + } + break; + case 71: + if (!this.supportsIntegratedFind) { + const { state } = this.findController; + if (state) { + const newState = { + source: window, + type: "again", + findPrevious: cmd === 5 || cmd === 12, + }; + eventBus.dispatch("find", { + ...state, + ...newState, + }); + } + handled = true; + } + break; + case 61: + case 107: + case 187: + case 171: + this.zoomIn(); + handled = true; + break; + case 173: + case 109: + case 189: + this.zoomOut(); + handled = true; + break; + case 48: + case 96: + if (!isViewerInPresentationMode) { + setTimeout(() => { + this.zoomReset(); + }); + handled = false; + } + break; + case 38: + if (isViewerInPresentationMode || this.page > 1) { + this.page = 1; + handled = true; + ensureViewerFocused = true; + } + break; + case 40: + if (isViewerInPresentationMode || this.page < this.pagesCount) { + this.page = this.pagesCount; + handled = true; + ensureViewerFocused = true; + } + break; + } + } + if (cmd === 1 || cmd === 8) { + switch (evt.keyCode) { + case 83: + eventBus.dispatch("download", { + source: window, + }); + handled = true; + break; + case 79: + { + eventBus.dispatch("openfile", { + source: window, + }); + handled = true; + } + break; + } + } + if (cmd === 3 || cmd === 10) { + switch (evt.keyCode) { + case 80: + this.requestPresentationMode(); + handled = true; + this.externalServices.reportTelemetry({ + type: "buttons", + data: { + id: "presentationModeKeyboard", + }, + }); + break; + case 71: + if (this.appConfig.toolbar) { + this.appConfig.toolbar.pageNumber.select(); + handled = true; + } + break; + } + } + if (handled) { + if (ensureViewerFocused && !isViewerInPresentationMode) { + pdfViewer.focus(); + } + evt.preventDefault(); + return; + } + const curElement = getActiveOrFocusedElement(); + const curElementTagName = curElement?.tagName.toUpperCase(); + if ( + curElementTagName === "INPUT" || + curElementTagName === "TEXTAREA" || + curElementTagName === "SELECT" || + (curElementTagName === "BUTTON" && + (evt.keyCode === 13 || evt.keyCode === 32)) || + curElement?.isContentEditable + ) { + if (evt.keyCode !== 27) { + return; + } + } + if (cmd === 0) { + let turnPage = 0, + turnOnlyIfPageFit = false; + switch (evt.keyCode) { + case 38: + if (this.supportsCaretBrowsingMode) { + this.moveCaret(true, false); + handled = true; + break; + } + case 33: + if (pdfViewer.isVerticalScrollbarEnabled) { + turnOnlyIfPageFit = true; + } + turnPage = -1; + break; + case 8: + if (!isViewerInPresentationMode) { + turnOnlyIfPageFit = true; + } + turnPage = -1; + break; + case 37: + if (this.supportsCaretBrowsingMode) { + return; + } + if (pdfViewer.isHorizontalScrollbarEnabled) { + turnOnlyIfPageFit = true; + } + case 75: + case 80: + turnPage = -1; + break; + case 27: + if (this.secondaryToolbar?.isOpen) { + this.secondaryToolbar.close(); + handled = true; + } + if (!this.supportsIntegratedFind && this.findBar?.opened) { + this.findBar.close(); + handled = true; + } + break; + case 40: + if (this.supportsCaretBrowsingMode) { + this.moveCaret(false, false); + handled = true; + break; + } + case 34: + if (pdfViewer.isVerticalScrollbarEnabled) { + turnOnlyIfPageFit = true; + } + turnPage = 1; + break; + case 13: + case 32: + if (!isViewerInPresentationMode) { + turnOnlyIfPageFit = true; + } + turnPage = 1; + break; + case 39: + if (this.supportsCaretBrowsingMode) { + return; + } + if (pdfViewer.isHorizontalScrollbarEnabled) { + turnOnlyIfPageFit = true; + } + case 74: + case 78: + turnPage = 1; + break; + case 36: + if (isViewerInPresentationMode || this.page > 1) { + this.page = 1; + handled = true; + ensureViewerFocused = true; + } + break; + case 35: + if (isViewerInPresentationMode || this.page < this.pagesCount) { + this.page = this.pagesCount; + handled = true; + ensureViewerFocused = true; + } + break; + case 83: + this.pdfCursorTools?.switchTool(CursorTool.SELECT); + break; + case 72: + this.pdfCursorTools?.switchTool(CursorTool.HAND); + break; + case 82: + this.rotatePages(90); + break; + case 115: + this.pdfSidebar?.toggle(); + break; + } + if ( + turnPage !== 0 && + (!turnOnlyIfPageFit || pdfViewer.currentScaleValue === "page-fit") + ) { + if (turnPage > 0) { + pdfViewer.nextPage(); + } else { + pdfViewer.previousPage(); + } + handled = true; + } + } + if (cmd === 4) { + switch (evt.keyCode) { + case 13: + case 32: + if ( + !isViewerInPresentationMode && + pdfViewer.currentScaleValue !== "page-fit" + ) { + break; + } + pdfViewer.previousPage(); + handled = true; + break; + case 38: + this.moveCaret(true, true); + handled = true; + break; + case 40: + this.moveCaret(false, true); + handled = true; + break; + case 82: + this.rotatePages(-90); + break; + } + } + if (!handled && !isViewerInPresentationMode) { + if ( + (evt.keyCode >= 33 && evt.keyCode <= 40) || + (evt.keyCode === 32 && curElementTagName !== "BUTTON") + ) { + ensureViewerFocused = true; + } + } + if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) { + pdfViewer.focus(); + } + if (handled) { + evt.preventDefault(); + } +} +function beforeUnload(evt) { + evt.preventDefault(); + evt.returnValue = ""; + return false; +} // ./web/viewer.js + +const pdfjsVersion = "4.10.38"; +const pdfjsBuild = "f9bea397f"; +const AppConstants = { + LinkTarget: LinkTarget, + RenderingStates: RenderingStates, + ScrollMode: ScrollMode, + SpreadMode: SpreadMode, +}; +window.PDFViewerApplication = PDFViewerApplication; +window.PDFViewerApplicationConstants = AppConstants; +window.PDFViewerApplicationOptions = AppOptions; +function getViewerConfiguration() { + return { + appContainer: document.body, + principalContainer: document.getElementById("mainContainer"), + mainContainer: document.getElementById("viewerContainer"), + viewerContainer: document.getElementById("viewer"), + toolbar: { + container: document.getElementById("toolbarContainer"), + numPages: document.getElementById("numPages"), + pageNumber: document.getElementById("pageNumber"), + scaleSelect: document.getElementById("scaleSelect"), + customScaleOption: document.getElementById("customScaleOption"), + previous: document.getElementById("previous"), + next: document.getElementById("next"), + zoomIn: document.getElementById("zoomInButton"), + zoomOut: document.getElementById("zoomOutButton"), + print: document.getElementById("printButton"), + editorFreeTextButton: document.getElementById("editorFreeTextButton"), + editorFreeTextParamsToolbar: document.getElementById( + "editorFreeTextParamsToolbar", + ), + editorHighlightButton: document.getElementById("editorHighlightButton"), + editorHighlightParamsToolbar: document.getElementById( + "editorHighlightParamsToolbar", + ), + editorHighlightColorPicker: document.getElementById( + "editorHighlightColorPicker", + ), + editorInkButton: document.getElementById("editorInkButton"), + editorInkParamsToolbar: document.getElementById("editorInkParamsToolbar"), + editorStampButton: document.getElementById("editorStampButton"), + editorStampParamsToolbar: document.getElementById( + "editorStampParamsToolbar", + ), + download: document.getElementById("downloadButton"), + }, + secondaryToolbar: { + toolbar: document.getElementById("secondaryToolbar"), + toggleButton: document.getElementById("secondaryToolbarToggleButton"), + presentationModeButton: document.getElementById("presentationMode"), + openFileButton: document.getElementById("secondaryOpenFile"), + printButton: document.getElementById("secondaryPrint"), + downloadButton: document.getElementById("secondaryDownload"), + viewBookmarkButton: document.getElementById("viewBookmark"), + firstPageButton: document.getElementById("firstPage"), + lastPageButton: document.getElementById("lastPage"), + pageRotateCwButton: document.getElementById("pageRotateCw"), + pageRotateCcwButton: document.getElementById("pageRotateCcw"), + cursorSelectToolButton: document.getElementById("cursorSelectTool"), + cursorHandToolButton: document.getElementById("cursorHandTool"), + scrollPageButton: document.getElementById("scrollPage"), + scrollVerticalButton: document.getElementById("scrollVertical"), + scrollHorizontalButton: document.getElementById("scrollHorizontal"), + scrollWrappedButton: document.getElementById("scrollWrapped"), + spreadNoneButton: document.getElementById("spreadNone"), + spreadOddButton: document.getElementById("spreadOdd"), + spreadEvenButton: document.getElementById("spreadEven"), + imageAltTextSettingsButton: document.getElementById( + "imageAltTextSettings", + ), + imageAltTextSettingsSeparator: document.getElementById( + "imageAltTextSettingsSeparator", + ), + documentPropertiesButton: document.getElementById("documentProperties"), + }, + sidebar: { + outerContainer: document.getElementById("outerContainer"), + sidebarContainer: document.getElementById("sidebarContainer"), + toggleButton: document.getElementById("sidebarToggleButton"), + resizer: document.getElementById("sidebarResizer"), + thumbnailButton: document.getElementById("viewThumbnail"), + outlineButton: document.getElementById("viewOutline"), + attachmentsButton: document.getElementById("viewAttachments"), + layersButton: document.getElementById("viewLayers"), + thumbnailView: document.getElementById("thumbnailView"), + outlineView: document.getElementById("outlineView"), + attachmentsView: document.getElementById("attachmentsView"), + layersView: document.getElementById("layersView"), + currentOutlineItemButton: document.getElementById("currentOutlineItem"), + }, + findBar: { + bar: document.getElementById("findbar"), + toggleButton: document.getElementById("viewFindButton"), + findField: document.getElementById("findInput"), + highlightAllCheckbox: document.getElementById("findHighlightAll"), + caseSensitiveCheckbox: document.getElementById("findMatchCase"), + matchDiacriticsCheckbox: document.getElementById("findMatchDiacritics"), + entireWordCheckbox: document.getElementById("findEntireWord"), + findMsg: document.getElementById("findMsg"), + findResultsCount: document.getElementById("findResultsCount"), + findPreviousButton: document.getElementById("findPreviousButton"), + findNextButton: document.getElementById("findNextButton"), + }, + passwordOverlay: { + dialog: document.getElementById("passwordDialog"), + label: document.getElementById("passwordText"), + input: document.getElementById("password"), + submitButton: document.getElementById("passwordSubmit"), + cancelButton: document.getElementById("passwordCancel"), + }, + documentProperties: { + dialog: document.getElementById("documentPropertiesDialog"), + closeButton: document.getElementById("documentPropertiesClose"), + fields: { + fileName: document.getElementById("fileNameField"), + fileSize: document.getElementById("fileSizeField"), + title: document.getElementById("titleField"), + author: document.getElementById("authorField"), + subject: document.getElementById("subjectField"), + keywords: document.getElementById("keywordsField"), + creationDate: document.getElementById("creationDateField"), + modificationDate: document.getElementById("modificationDateField"), + creator: document.getElementById("creatorField"), + producer: document.getElementById("producerField"), + version: document.getElementById("versionField"), + pageCount: document.getElementById("pageCountField"), + pageSize: document.getElementById("pageSizeField"), + linearized: document.getElementById("linearizedField"), + }, + }, + altTextDialog: { + dialog: document.getElementById("altTextDialog"), + optionDescription: document.getElementById("descriptionButton"), + optionDecorative: document.getElementById("decorativeButton"), + textarea: document.getElementById("descriptionTextarea"), + cancelButton: document.getElementById("altTextCancel"), + saveButton: document.getElementById("altTextSave"), + }, + newAltTextDialog: { + dialog: document.getElementById("newAltTextDialog"), + title: document.getElementById("newAltTextTitle"), + descriptionContainer: document.getElementById( + "newAltTextDescriptionContainer", + ), + textarea: document.getElementById("newAltTextDescriptionTextarea"), + disclaimer: document.getElementById("newAltTextDisclaimer"), + learnMore: document.getElementById("newAltTextLearnMore"), + imagePreview: document.getElementById("newAltTextImagePreview"), + createAutomatically: document.getElementById( + "newAltTextCreateAutomatically", + ), + createAutomaticallyButton: document.getElementById( + "newAltTextCreateAutomaticallyButton", + ), + downloadModel: document.getElementById("newAltTextDownloadModel"), + downloadModelDescription: document.getElementById( + "newAltTextDownloadModelDescription", + ), + error: document.getElementById("newAltTextError"), + errorCloseButton: document.getElementById("newAltTextCloseButton"), + cancelButton: document.getElementById("newAltTextCancel"), + notNowButton: document.getElementById("newAltTextNotNow"), + saveButton: document.getElementById("newAltTextSave"), + }, + altTextSettingsDialog: { + dialog: document.getElementById("altTextSettingsDialog"), + createModelButton: document.getElementById("createModelButton"), + aiModelSettings: document.getElementById("aiModelSettings"), + learnMore: document.getElementById("altTextSettingsLearnMore"), + deleteModelButton: document.getElementById("deleteModelButton"), + downloadModelButton: document.getElementById("downloadModelButton"), + showAltTextDialogButton: document.getElementById( + "showAltTextDialogButton", + ), + altTextSettingsCloseButton: document.getElementById( + "altTextSettingsCloseButton", + ), + closeButton: document.getElementById("altTextSettingsCloseButton"), + }, + annotationEditorParams: { + editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"), + editorFreeTextColor: document.getElementById("editorFreeTextColor"), + editorInkColor: document.getElementById("editorInkColor"), + editorInkThickness: document.getElementById("editorInkThickness"), + editorInkOpacity: document.getElementById("editorInkOpacity"), + editorStampAddImage: document.getElementById("editorStampAddImage"), + editorFreeHighlightThickness: document.getElementById( + "editorFreeHighlightThickness", + ), + editorHighlightShowAll: document.getElementById("editorHighlightShowAll"), + }, + printContainer: document.getElementById("printContainer"), + editorUndoBar: { + container: document.getElementById("editorUndoBar"), + message: document.getElementById("editorUndoBarMessage"), + undoButton: document.getElementById("editorUndoBarUndoButton"), + closeButton: document.getElementById("editorUndoBarCloseButton"), + }, + }; +} +function webViewerLoad() { + const config = getViewerConfiguration(); + const event = new CustomEvent("webviewerloaded", { + bubbles: true, + cancelable: true, + detail: { + source: window, + }, + }); + try { + parent.document.dispatchEvent(event); + } catch (ex) { + console.error("webviewerloaded:", ex); + document.dispatchEvent(event); + } + PDFViewerApplication.run(config); +} +document.blockUnblockOnload?.(true); +if ( + document.readyState === "interactive" || + document.readyState === "complete" +) { + webViewerLoad(); +} else { + document.addEventListener("DOMContentLoaded", webViewerLoad, true); +} + +var __webpack_exports__PDFViewerApplication = + __webpack_exports__.PDFViewerApplication; +var __webpack_exports__PDFViewerApplicationConstants = + __webpack_exports__.PDFViewerApplicationConstants; +var __webpack_exports__PDFViewerApplicationOptions = + __webpack_exports__.PDFViewerApplicationOptions; +export { + __webpack_exports__PDFViewerApplication as PDFViewerApplication, + __webpack_exports__PDFViewerApplicationConstants as PDFViewerApplicationConstants, + __webpack_exports__PDFViewerApplicationOptions as PDFViewerApplicationOptions, +}; + +//# sourceMappingURL=viewer.mjs.map diff --git a/public/index.html b/public/index.html deleted file mode 100644 index a8ec00f..0000000 --- a/public/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - {siteName} - - - - -
- - {siteScript} - diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index dcfd15c..99e354f 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -1,13 +1,27 @@ { "login": { + "lastStep": "Last step", + "siginToYourAccount": "Sign in to your account", + "createNewAccount": "Create a new account", + "enterPassword": "Enter your password", + "enterPasswordHint": "Please enter password for {{email}}", + "noAccountSignupNow": "No account? <0>Sign up now", + "haveAccountSignInNow": "Got an account already?<0>Sign in now", + "privacyPolicy": "Privacy policy", + "termOfUse": "Terms of use", "email": "Email", + "signupHint": "The account {{email}} does not exist, do you want to signup now?", + "accountNotFoundHint": "The account \"{{email}}\" you entered does not exist.", + "or": "Or", + "selectAccountToUse": "Select an account to use", + "useOtherAccount": "Use another account", "password": "Password", "captcha": "CAPTCHA", "captchaError": "Cannot load CAPTCHA: {{message}}", "signIn": "Sign in", "signUp": "Sign up", "signUpAccount": "Sign up", - "useFIDO2": "Use Hardware Authenticator", + "useFIDO2": "Use Passkey", "usePassword": "Use Password", "forgetPassword": "Forgot password?", "2FA": "2FA Verification", @@ -32,16 +46,40 @@ "activateTitle": "Activate your account", "activateDescription": "An activation email has been sent to your email address, please visit the link in the email to complete your sign-up.", "continue": "Next", + "back": "Back", "logout": "Sign out", "loggedOut": "You are signed out now.", "clickToRefresh": "Click to refresh" }, "navbar": { + "notBefore": "Not before", + "notAfter": "Not after", + "minimum": "Minimum", + "maximum": "Maximum", + "fileSize": "File size", + "searchBase": "Search in", + "searchInBase": "Search in <0>", + "conditionDuplicate": "Condition already exists.", + "fileType": "File type", + "addCondition": "Add conditions", + "notNameOpOr": "All keywords must present", + "caseFolding": "Case folding", + "keywords": "Keywords", + "fileNameKeywordsHelp": "Press enter to add new keyword.", + "advancedSearch": "Advanced search", + "searchFilesTitle": "Search files", + "searchIn": "Search <0>{{keywords}}", + "recentlyViewed": "Recently viewed", + "searchFiles": "Search files...", + "showMore": "More", "myFiles": "My Files", - "myShare": "Shared", + "hisFiles": "His/Her files", + "myShare": "Shared by me", + "trash": "Trash", + "sharedWithMe": "Shared with me", "remoteDownload": "Remote Download", - "connect": "Connect", - "taskQueue": "Task Queue", + "connect": "Connect & Mount", + "taskQueue": "Background Tasks", "setting": "Settings", "videos": "Videos", "photos": "Photos", @@ -61,42 +99,177 @@ }, "storage": "Storage", "storageDetail": "{{used}} of {{total}} used", - "notLoginIn": "Not sign in", + "notLoginIn": "Signed out", "visitor": "Anonymous", - "objectsSelected": "{{num}} objects selected", - "searchPlaceholder": "Search...", - "searchInFiles": "Search <0>{{name}} in my files", - "searchInFolders": "Search <0>{{name}} under current folder", - "searchInShares": "Search <0>{{name}} in other users’ shares", + "objectsSelected": "{{num}} selected", + "searchPlaceholder": "Type <0>/ to search", "backToHomepage": "Back to homepage", - "toDarkMode": "Switch to dark theme", - "toLightMode": "Switch to light theme", + "darkModeSwitch": "Dark theme switch", + "toDarkMode": "Dark", + "toLightMode": "Light", "myProfile": "My profile", - "dashboard": "Dashboard", - "exceedQuota": "Your used capacity has exceeded the quota, please delete the extra files." + "dashboard": "Dashboard" }, "fileManager": { + "shareWithMeEmpty": "No shared files found", + "shareWithMeEmptyDes": "If you need to see others' shares here, please save the shortcut to any location in your files when you visit a shared link.", + "selectAll": "Select all", + "selectNone": "Select none", + "invertSelection": "Invert selection", + "imageSize": "Image size", + "focalLength": "Focal length", + "columnExisted": "Column already exists.", + "metadataColumn": "Metadata ({{metadata}})", + "column": "Column", + "listColumnSetting": "Column setting", + "addColumn": "Add columns", + "failedLoadPreview": "Failed to load preview.", + "recursiveLimitReached": "Search depth limit reached.", + "recursiveLimitReachedDes": "The system has stopped searching deeper folders, please try to narrow down the search scope.", + "searchConditions": "{{num}} condition(s)", + "createDate": "Date created", + "updatedDate": "Date updated", + "cameraMake": "Camera maker", + "cameraModel": "Camera model", + "lensModel": "Lens model", + "lensMake": "Lens maker", + "metadataKey": "Key", + "metadataValue": "Value", + "metadata": "Metadata", + "symbolicFile": "Symbolic link", + "relocation": "Relocate storage policy", + "downloadingFile": "Downloading \"{{name}}\", please do not close this page...", + "mountOwner": "Only the owner of current folder can mount policies.", + "uploading": "Uploading", + "noActionsCanBeDone": "No actions can be done.", + "newFileName": "New file.{{ext}}", + "newDocumentType": "{{display_name}} (.{{ext}})", + "text": "Text", + "diagram": "Diagram", + "whiteboard": "Whiteboard", + "selectApplications": "Select applications...", + "newlyCreatedFolder": "New folder", + "expandAllApp": "Expand all applications", + "epubViewer": "ePub Reader", + "googledocs": "Google Docs Viewer", + "m365viewer": "Microsoft Office Online Viewer", + "pdfViewer": "PDF Viewer", + "viewerFileSizeWarning": "Size of opened file ({{file_size}}) exceed limit ({{max}}) of {{app}}, it might not work properly.", + "testSubtitleStyle": "Test subtitle style AaBbCc", + "color": "Color", + "fontSize": "Font size", + "disableSubtitle": "Disable subtitle", + "noSubtitle": "No ASS/SRT/VTT subtitle files found under current folder.", + "subtitleStyles": "Subtitle styles", + "subtitles": "Subtitles", + "markdownEditor": "Markdown Editor", + "saveSuccess": "Saved successfully at {{time}}", + "drawioLng": "en", + "charset": "Charset", + "textType": "Text type", + "fileSaved": "File saved.", + "failedToLoadFile": "Failed to load file: {{msg}}", + "monacoEditor": "Monaco Code Editor", + "preparingOpenFile": "Preparing to open file...", + "openWithDescription": "Select an application to open .{{ext}} file.", + "openWith": "Open with", + "readOnly": "Read only", + "save": "Save", + "noMoreImages": "No images found in current page.", + "imageViewer": "Image Viewer", + "logFileDeleteShare": "Deleted a share link", + "logFileEditShare": "Edited a share link", + "deleteShareWarning": "Are you sure to delete this share link?", + "edit": "Edit", + "editAndReactivate": "Edit and reactivate", + "yes": "Yes", + "no": "No", + "permanentValid": "Permanent", + "manageShares": "Manage share links", + "deleteVersionWarning": "Are you sure to delete this version? This operation cannot be undone.", + "setAsCurrent": "Set as current version", + "current": "[Current]", + "createdBy": "Created by", + "manageVersions": "Manage versions", + "livePhoto": "Live Photo", + "version": "Version", + "actions": "Actions", + "versionEntity": "File data and versions", + "data": "Data", + "owned": "Owned", + "ownedSymbolic": "Owned (Symbolic link)", + "expires": "Expires", + "originalLocation": "Original location", + "descendant": "Descendant", + "folderChildren": "{{files}} file(s), {{folders}} folder(s)", + "moreThan": "More than {{text}}", + "calculate": "Calculate", + "unset": "Unset", + "folder": "Folder", + "file": "File", + "symbolicLink": "Symbolic link ({{srcType}})", + "type": "Type", + "storageUsed": "Storage used", + "location": "Location", + "basicInfo": "Basic info", + "format": "Format", + "duration": "Duration", + "artist": "Artist", + "album": "Album", + "title": "Title", + "resolution": "Resolution", + "takenAt": "Taken at", + "software": "Software", + "copyright": "Copyright", + "exposureBias": "Exposure bias", + "flash": "Flash", + "copyToClipboard": "Copy to clipboard", + "searchSomething": "Search \"{{text}}\"...", + "iso": "ISO", + "exposureValue": "{{num}} s", + "exposure": "Exposure", + "aperture": "Aperture", + "mediaInfo": "Media info", + "details": "Details", + "activity": "Activity", + "goToSharedLink": "Go to shared link", + "saveShortcut": "Save share link as shortcut", + "customizeIcon": "Customize icon", + "tags": "Tags", + "apply": "Apply", + "customizeColor": "Customize color", + "folderColor": "Folder color", + "restore": "Restore", + "unpin": "Unpin", + "youDontHaveReadPermissionToThisFile": "You don't have access permission.", + "sharedWithOthers": "Shared with others", + "new": "New", "open": "Open", - "openParentFolder": "Open parent folder", + "openParentFolder": "Go to parent folder", "download": "Download", "batchDownload": "Download in batch", "share": "Share", "rename": "Rename", + "organize": "Organize", + "pin": "Pin to sidebar", + "pinAlias": "Display name", + "alreadyPined": "This item is already pinned.", + "optional": "Optional", "move": "Move", - "delete": "Remove", - "moreActions": "More actions...", + "delete": "Delete", + "moreActions": "More actions", "refresh": "Refresh", - "compress": "Compress", + "createArchive": "Create archive file", "newFolder": "New folder", "newFile": "New file", "showFullPath": "Show full path", - "listView": "List view", - "gridViewSmall": "Grid view (no preview)", - "gridViewLarge": "Grid view", + "listView": "List", + "gridView": "Grid", + "galleryView": "Gallery", "paginationSize": "Pagination", "paginationOption": "{{option}} / page", "noPagination": "No pagination", - "sortMethod": "Sort by", + "sortMethod": "Sort", "sortMethods": { "A-Z": "A-Z", "Z-A": "Z-A", @@ -115,24 +288,22 @@ "backToParentFolder": "Back to the parent", "folders": "Folders", "files": "Files", - "listError": ":( Failed to lis files", + "listError": "Failed to list files", "dropFileHere": "Drag and drop the file here", - "orClickUploadButton": "Or click the \"Upload File\" button at the bottom right to add a file", + "orClickUploadButton": "Or click the \"New\" button at the top left to add a file", "nothingFound": "Nothing was found", "uploadFiles": "Upload files", "uploadFolder": "Upload folder", "newRemoteDownloads": "New remote download", "enter": "Enter", - "getSourceLink": "Get source link", - "getSourceLinkInBatch": "Get source links", + "getSourceLink": "Get direct link", "createRemoteDownloadForTorrent": "New remote download", - "decompress": "Decompress", + "extractArchive": "Extract archive", "createShareLink": "Share", "viewDetails": "View details", "copy": "Copy", "bytes": " ({{bytes}} Bytes)", "storagePolicy": "Storage policy", - "inheritedFromParent": "Inherited from parent", "childFolders": "Child folders", "childFiles": "Child files", "childCount": "{{num}}", @@ -140,12 +311,14 @@ "rootFolder": "Root folder", "modifiedAt": "Modified at", "createdAt": "Created at", - "statisticAt": "Statistic at <1>", - "musicPlayer": "Music player", + "statisticAt": "Statistic at", + "musicPlayer": "Music Player", "closeAndStop": "Close and stop", "playInBackground": "Play in background", "copyTo": "Copy to", - "copyToDst": "Copy to <0>{{dst}}", + "copyToDst": "Copy to <0>", + "moveTo": "Move to", + "moveToDst": "Move to <0>", "errorReadFileContent": "Failed to read file content: {{msg}}", "wordWrap": "Word wrap", "pdfLoadingError": "Failed to load PDF: {{msg}}", @@ -157,67 +330,136 @@ "searchResult": "Search Results", "preparingBathDownload": "Preparing batch download...", "preparingDownload": "Preparing to download...", + "browserDownload": "Browser-side download to a local folder", + "browserDownloadDescription": "Your browser downloads files one by one and retain the folder structure to the local directory you specified.", "browserBatchDownload": "Browser-side archiving", - "browserBatchDownloadDescription": "Downloaded and packaged by the browser in real time, not all environments are supported.", - "serverBatchDownload": "Server-side transit archiving", - "serverBatchDownloadDescription": "Archive by the server and sent to the client for download on-the-fly.", + "browserBatchDownloadDescription": "Downloaded and packaged to a Zip file by the browser in real time, it cannot handle data more than 4GB.", + "serverBatchDownload": "Server-side archiving", + "serverBatchDownloadDescription": "Archive by the server to a Zip file and sent to the client for download on-the-fly, share link shortcut is not supported.", "selectArchiveMethod": "Select archive method", - "batchDownloadStarted": "Batch download has started, please do not close this tab", + "batchDownloadStarted": "Batch download has started, please do not close this tab...", "batchDownloadError": "Failed to archive: {{msg}}", "userDenied": "User denied.", - "directoryDownloadReplace": "Overwrite", - "directoryDownloadReplaceDescription": "{{num}} objects including {{duplicates}} will be overwritten.", + "directoryDownloadReplace": "Replace", + "directoryDownloadReplaceDescription": "Local file \"{{name}}\" will be replaced by the downloaded file.", "directoryDownloadSkip": "Skip", - "directoryDownloadSkipDescription": "{{num}} objects including {{duplicates}} will be skipped.", - "selectDirectoryDuplicationMethod": "How to handle duplicate files?", + "directoryDownloadSkipDescription": "\"{{name}}\" will be skipped.", + "selectDirectoryDuplicationMethod": "Duplicated file", + "directoryDownloadReplaceAll": "Replace all", + "directoryDownloadReplaceAllDescription": "All files with the same name will be replaced by the downloaded files.", + "directoryDownloadSkipAll": "Skip all", + "directoryDownloadSkipAllDescription": "All files with the same name will be skipped.", "directoryDownloadStarted": "Download started, please do not close this tab.", "directoryDownloadFinished": "Download finished, no failed objects.", "directoryDownloadFinishedWithError": "Download finished, {{failed}} object failed.", - "directoryDownloadPermissionError": "Permission denied, please allow read and write local files." + "directoryDownloadPermissionError": "Permission denied, please allow read and write local files.", + "back": "Back", + "view": "View", + "layout": "Layout", + "thumbnails": "Thumbnails", + "on": "On", + "off": "Off" }, "modals": { - "processing": "Processing...", - "duplicatedObjectName": "Duplicated object name.", - "duplicatedFolderName": "Duplicated folder name.", + "showFileName": "Show file name", + "archiveFile": "Archive file", + "cancelDownload": "Cancel download", + "always": "Always", + "justOnce": "Just once", + "quality": "Quality", + "saveAsOtherFormat": "Save as other format", + "conflictDes1": "File version conflict, possible reasons are:", + "conflictDes2": "<0>The file was updated to a new version from elsewhere after you opened it.<1>If you saved it with a new name or a new location, the file name already exists.", + "saveAs": "Save as", + "versionConflict": "Version conflict", + "overwrite": "Overwrite", + "editShareLink": "Edit share link", + "clearPermissions": "Clear permission settings", + "shortcutCreated": "Shortcut created.", + "createShortcut": "Create shortcut", + "createShortcutTo": "Create shortcut at <0>", + "targetExisted": "Target already exists.", + "users": "Users", + "groups": "Groups", + "resetToDefault": "Reset to default", + "duplicateTag": "Tag \"{{tag}}\" already exists.", + "colorForTag": "Customize color for new tags", + "enterForNewTag": "Press enter to add new tag.", + "manageTags": "Manage tags", + "onlyOwner": "Only the owner of this file can force unlock it.", + "forceUnlock": "Force Unlock", + "forceUnlockAll": "Force Unlock All", + "forceUnlockDes": "Forcing unlock may corrupt the file state, we recommend waiting for the file being released proactively, are you sure to continue unlocking?", + "webdav": "WebDAV", + "soft-delete": "Move to trash bin", + "updateMetadata": "Update metadata", + "upload": "Upload", + "moveCopy": "Move or copy", + "view": "View", + "cannotPerformAction": "Moving or copying files to here is not supported.", + "cannotMoveCopyToChild": "Cannot move or copy to descendant folder.", + "copySuccess": "{{num}} file(s) copied successfully.", + "moveSuccess": "{{num}} file(s) moved successfully.", + "unknownParent": "Unknown parent", + "unknownParentDes": "The occupied folder is the parent folder of a shared folder, and it's not owned by you.", + "lockConflictTitle": "File occupied", + "lockConflictDescription": "This operation cannot complete because following file(s) is currently used by others, please try again later. If you are the file owner and you are sure that the file is not in use, you can force unlock the file and retry.", + "application": "Application", + "errorDetailsTitle": "Error details", + "processingMoving": "Moving files...", + "processingCopying": "Copying files...", + "processingRestoring": "Restoring files...", + "fileRestored": "{{num}} file(s) restored to its original location.", + "duplicatedObjectName": "Duplicated file name.", + "newNameLengthError": "Length of file name must be in range of 1 to 255.", + "newNameCharacterError": "Name must not contain any of those characters: \\ / : * ? \" < > |", + "newNameDotError": "Name cannot be \".\" or \"..\"", "taskCreated": "Task created.", "taskCreateFailed": "{{failed}} task(s) failed to be created: {{details}}.", "linkCopied": "Link copied.", - "getSourceLinkTitle": "Get source link", - "sourceLink": "Source link", + "getSourceLinkTitle": "Get direct link", + "sourceLink": "Direct link", "folderName": "Folder name", "create": "Create", "fileName": "File name", - "renameDescription": "Enter the new name for <0>{{name}} :", + "renameDescription": "Enter the new name for <0>{{name}}:", "newName": "New name", - "moveToTitle": "Move to", "moveToDescription": "Move to <0>{{name}}", "saveToTitle": "Save to", "saveToTitleDescription": "Save to <0>{{name}}", "deleteTitle": "Delete objects", - "deleteOneDescription": "Are you sure to delete <0>{{name}} ?", - "deleteMultipleDescription": "Are you sure to remove those {{num}} objects?", + "deleteOneDescription": "Are you sure to move <0>{{name}} to trash bin?", + "deleteMultipleDescription": "Are you sure to move those {{num}} objects to trash bin?", + "deleteOneDescriptionHard": "Are you sure to permanently delete <0>{{name}}?", + "trashRetention": "Files in the trash bin will be deleted after <0>{{num}}.", + "deleteMultipleDescriptionHard": "Are you sure to permanently delete those {{num}} objects?", "newRemoteDownloadTitle": "New remote download task", "remoteDownloadURL": "Download target URL", - "remoteDownloadURLDescription": "Paste the download URL, one URL per line, support HTTP(s) / FTP / Magnet link", + "remoteDownloadURLDescription": "Paste the download URL, one URL per line", "remoteDownloadDst": "Download to", + "processNode": "Target node", + "remoteDownloadNodeAuto": "Auto dispatch", "createTask": "Creat task", - "downloadTo": "Download to <0>{{name}}", - "decompressTo": "Decompress to", - "decompressToDst": "Decompress to <0>{{name}}", + "downloadToDst": "Download to <0>{{name}}", + "downloadTo": "Download to", + "decompressTo": "Extract to", + "decompressToDst": "Extract to <0>{{name}}", "defaultEncoding": "Default", "chineseMajorEncoding": "", - "selectEncoding": "Select the encoding for non-UTF8 characters", + "selectEncoding": "ZIP file encoding", "noEncodingSelected": "No encoding method selected", "listingFiles": "Listing files...", "listingFileError": "Failed to list files: {{message}}", "generatingSourceLinks": "Generating source links...", "noFileCanGenerateSourceLink": "There is no file that can be used to generate source link", "sourceBatchSizeExceeded": "The current user group can generate source links for a maximum of {{limit}} files at the same time.", - "zipFileName": "ZIP file name", + "zipFileName": "Archive file name", "shareLinkShareContent": "I shared with you: {{name}} Link: {{link}}", "shareLinkPasswordInfo": "Password: {{password}}", "createShareLink": "Create share link", - "usePasswordProtection": "Use password protection", + "privateShare": "Hide from public", + "privateShareDes": "If selected, other people cannot see this share link on your homepage.", + "expireAfterDownload": "Expire after being downloaded", "sharePassword": "Share password", "randomlyGenerate": "Random", "expireAutomatically": "Automatic expiration", @@ -229,29 +471,38 @@ "7days": "7 days", "30days": "30 days", "custom": "Custom", - "seconds": "seconds", + "minutes": "minutes", "downloads": "downloads", - "downloadSuffix": "", + "expireSuffix": "", + "expirePrefix": "Expire after", "allowPreview": "Enable preview", "allowPreviewDescription": "Whether to allow preview of file content from the share link", "shareLink": "Share link", "sendLink": "Send the link", "directoryDownloadReplaceNotifiction": "Overwrite {{name}}", "directoryDownloadSkipNotifiction": "Skipped {{name}}", - "directoryDownloadTitle": "Download", - "directoryDownloadStarted": "Start downloading {{name}}", - "directoryDownloadFinished": "Download finished", + "directoryDownloadTitle": "Batch download logs", + "directoryDownloadStarted": "Start downloading \"{{name}}\"", + "directoryDownloadFinished": "Download finished \"{{name}}\"", "directoryDownloadError": "Error: {{msg}}", "directoryDownloadErrorNotification": "Error occurs while download {{name}}: {{msg}}", "directoryDownloadAutoscroll": "Auto scroll", "directoryDownloadCancelled": "Download cancelled", "advanceOptions": "Advanced options", - "forceDelete": "Force delete ", - "forceDeleteDes": "Force delete file records, regardless of whether the physical file was successfully deleted.", - "unlinkOnly": "Unlink only", + "skipSoftDelete": "Permanently delete", + "skipSoftDeleteDes": "Skip moving to trash bin, permanently delete", + "unlinkOnly": "Keep physical files", "unlinkOnlyDes": "Delete file records only, physical files will not be deleted." }, "uploader": { + "fileCopyName": "Copy of ", + "overwriteTooltip": "Overwrite existing file if there's conflict, only works for newly added tasks.", + "rename": "Retry with new name", + "overwrite": "Overwrite existing file", + "pasteFilesHere": "Paste files here", + "clipboardDefaultFileName": "Clipboard {{date}}.png", + "uploadFromClipboard": "Upload from clipboard", + "uploadList": "Upload tasks", "fileNotMatchError": "The selected file does not match the original file.", "unknownError": "Unknown error occurs: {{msg}}", "taskListEmpty": "No upload task.", @@ -266,7 +517,7 @@ "progressDescription": "{{uploaded}} uploaded, {{total}} total - {{percentage}}%", "progressDescriptionFull": "{{uploaded}} uploaded, {{total}} total - {{percentage}}% ({{speed}})", "progressDescriptionPlaceHolder": " - uploaded", - "uploadedTo": "Uploaded to ", + "uploaded": "Uploaded", "rootFolder": "Root folder", "unknownStatus": "Unknown", "resumed": "Resumed", @@ -282,12 +533,13 @@ "noChunks": "(No chunks)", "destination": "Destination: ", "uploadSession": "Upload session: ", + "storagePolicy": "Storage policy: ", "errorDetails": "Error details: ", "uploadSessionCleaned": "All upload sessions cleared.", "hideCompletedTooltip": "Hide completed, failed and cancelled tasks.", "hideCompleted": "Hide completed tasks", "addTimeAscTooltip": "Tasks added first are ranked first.", - "addTimeAsc":"Oldest to newest", + "addTimeAsc": "Oldest to newest", "addTimeDescTooltip": "Latest added first are ranked first.", "addTimeDesc": "Newest to oldest", "showInstantSpeedTooltip": "Task upload speeds are shown as instantaneous speed.", @@ -322,33 +574,28 @@ "dropFileHere": "Drop file to upload" }, "share": { - "expireInXDays": "Expire in $t(share.days, {\"count\": {{num}} })", - "days":"{{count}} day", - "days_other":"{{count}} days", - "expireInXHours":"Expire in $t(share.hours, {\"count\": {{num}} })", - "hours":"an hour", - "hours_other":"{{count}} hours", - "createdBy": "Created by <0>{{nick}}", + "statistics": "Statistics", + "expireAt": "Expire <0>", + "expireAfterDownloads": "Expire after {{downloads}} download(s)", + "somebodyShare": "Shared by {{name}}", + "expiredLink": "Expired share", "sharedBy": "<0>{{nick}} shared $t(share.files, {\"count\": {{num}} }) to you.", - "files":"1 file", - "files_other":"{{count}} files", - "statistics": "$t(share.views, {\"count\": {{views}} }) • $t(share.downloads, {\"count\": {{downloads}} }) • {{time}}", - "views":"{{count}} view", - "views_other":"{{count}} views", - "downloads":"{{count}} download", - "downloads_other":"{{count}} downloads", + "files": "1 file", + "files_other": "{{count}} files", + "statisticsViews": "$t(share.views, {\"count\": {{views}} })", + "statisticsDownloads": "$t(share.downloads, {\"count\": {{downloads}} })", + "views": "{{count}} view", + "views_other": "{{count}} views", + "downloads": "{{count}} download", + "downloads_other": "{{count}} downloads", "privateShareTitle": "Private share from {{nick}}", - "enterPassword": "Enter share password", + "enterPassword": "Share password", "continue": "Continue", - "shareCanceled": "Share is canceled.", + "shareCanceled": "Share link is deleted.", "listLoadingError": "Failed to load.", "sharedFiles": "Shared files", - "createdAtDesc": "Date (Descending)", - "createdAtAsc": "Date (Ascending)", - "downloadsDesc": "Number of downloads (Descending)", - "downloadsAsc":"Number of downloads (Ascending)", - "viewsDesc":"Number of views (Descending)", - "viewsAsc":"Number of views (Ascending)", + "createdAtDesc": "Newest", + "createdAtAsc": "Oldest", "noRecords": "No shared files.", "sourceNotFound": "[Source not exist]", "expired": "Expired", @@ -367,9 +614,11 @@ "cannotShare": "This file cannot be previewed.", "preview": "Preview", "incorrectPassword": "Password incorrect.", - "shareNotExist": "Invalid or expired share link." + "shareNotExist": "Share link is invalid or expired.", + "copyLinkToClipboard": "Copy link to clipboard" }, "download": { + "cancelTaskConfirm": "Are you sure to cancel this download task?", "failedToLoad": "Failed to load.", "active": "Active", "finished": "Finished", @@ -385,16 +634,16 @@ "selectDownloadingFile": "Select files to download", "cancelTask": "Cancel", "updatedAt": "Updated at: ", - "uploaded": "Uploaded: ", - "uploadSpeed": "Upload speed: ", - "InfoHash": "InfoHash: ", + "uploaded": "Uploaded", + "uploadSpeed": "Upload speed", + "InfoHash": "InfoHash", "seederCount": "Seeders:", "seeding": "Seeding: ", "downloadNode": "Node: ", "isSeeding": "Yes", "notSeeding": "No", "chunkSize": "Chunk size:", - "chunkNumbers": "Chunks:", + "chunkNumbers": "Chunks", "taskDeleted": "Task deleted.", "transferFailed": "Failed to transfer files.", "downloadFailed": "Download failed: {{msg}}", @@ -406,17 +655,84 @@ "createdAt": "Created at: " }, "setting": { - "avatarUpdated": "The avatar has been updated and will take effect after refreshing.", + "noAuthenticator": "Add a passkey to sign in using fingerprint, face or USB key.", + "neverUsed": "Never used", + "usedAt": "Last used at <0>", + "passkeyName": "{browser} on {os}", + "versionRetentionMax": "Maximum number of versions, 0 means no limit.", + "versionRetentionEnabledExt": "Enabled file extensions", + "versionRetentionEnabledExtDes": "Press enter to add, leave blank to enable for all files", + "enableVersionRetention": "Enable version retention", + "enableVersionRetentionDes": "If enabled, historical versions of files that meet the conditions will be retained.", + "versionRetention": "Version retention", + "languageDes": "Select the display language and preferred email language.", + "timezoneDes": "Set the display timezone, default is system timezone", + "nickNameDes": "This is your public display name. It can be your real name or a pseudonym.", + "cropAvatar": "Crop avatar", + "preference": "Preference", + "accountCreatedAt": "Created at <0>", + "shoeQr": "Show", + "deviceNothing": "WebDAV is not supported in your user group.", + "connectionInfo": "Connection details", + "proxyTooltip": "Proxy all file download requests.", + "readonlyTooltip": "User can only read files through this account.", + "rootFolderIn": "Select <0>", + "createWebDavAccount": "Create WebDAV account", + "editWebDavAccount": "Edit {{name}}", + "saveChanges": "Save changes", + "seeding": "Seeding", + "awaitSeeding": "Await seeding", + "awaitSeedingDes": "Await seeding completion.", + "downloadTransferDes": "Transfer files to destination.", + "downloadDes": "Download desired files.", + "retryErrorHistory": "Retry error history", + "retryCount": "Retried", + "resumeAt": "Resume at", + "executeDuration": "Execution duration", + "input": "Input", + "output": "Output", + "suspended": " (Suspended)", + "updatedAt": "Updated at", + "taskDetails": "Task details", + "partialSuccessWarning": "Failed to process {{num}} object(s), they were skipped.", + "sendTask": "Send task", + "sendTaskDes": "Send the task to a node to process.", + "downloaded": "Downloaded", + "extractedFiles": "Extracted files", + "extractedFilesSize": "Extracted files size", + "extractingFiles": "Extracting files", + "extractingFilesDes": "Extract all files to the given folder.", + "downloadingZip": "Download archive", + "downloadingZipDes": "Download archive to temporary workspace.", + "progressNotAvailable": "Progress not available yet.", + "uploadedSize": "Relocated size", + "archivedFiles": "Processed files", + "transferredFiles": "Relocated files", + "archivedFilesSize": "Processed file size", + "createArchiveFinishing": "Commit changes for new files", + "indexForArchiveDes": "Index for files to be archived.", + "prepare": "Prepare", + "preparingWorkspaceDes": "Prepare temporary workspace.", + "compressFiles": "Create archive", + "compressFilesDes": "Create archive to temporary workspace.", + "uploadArchiveFileDes": "Transfer archive file to the target folder.", + "uploadWorker": "Upload worker #{{num}}", + "queueToStart": "Queue to start", + "indexingFiles": "Index files", + "transferring": "Transfer", + "committingChanges": "Commit changes", + "autoRefresh": "Auto refresh", + "avatarUpdated": "The avatar has been updated and will take effect with a delay.", "nickChanged": "Nickname changed and will take effect after refreshing.", "settingSaved": "Setting saved.", "themeColorChanged": "Theme color changed.", "profile": "Profile", - "avatar": "Avatar", + "avatar": "Profile picture", "uid": "UID", - "nickname": "Nickname", + "nickname": "Display name", "group": "Group", "regTime": "Sign up date", - "privacyAndSecurity": "Privacy and security", + "security": "Password and security", "profilePage": "Public profile", "accountPassword": "Password", "2fa": "2FA authentication", @@ -425,7 +741,7 @@ "appearance": "Appearance", "themeColor": "Theme color", "darkMode": "Dark mode", - "syncWithSystem": "Sync with system", + "syncWithSystem": "System", "fileList": "File list", "timeZone": "Timezone", "webdavServer": "Server", @@ -434,10 +750,10 @@ "uploadImage": "Upload from file", "useGravatar": "Use Gravatar ", "changeNick": "Change nickname", - "originalPassword": "Original password", + "originalPassword": "Current password", "enable2FA": "Enable 2FA authentication", "disable2FA": "Disable 2FA authentication", - "2faDescription": "Please use any 2FA mobile app or password management software that supports 2FA to scan the QR code on the left to add this site. After scanning, please fill in the 6-digit verification code given by the 2FA app to enable 2FA.", + "2faDescription": "Please use any 2FA mobile app or password management software that supports 2FA to scan the QR code to add this site. After scanning, please fill in the 6-digit verification code given by the 2FA app to enable 2FA.", "inputCurrent2FACode": "Enter current 2FA verification code.", "timeZoneCode": "IANA timezone code", "authenticatorRemoved": "Authenticator removed.", @@ -445,7 +761,7 @@ "browserNotSupported": "Not supported by current browser or environment.", "removedAuthenticator": "Remove authenticator", "removedAuthenticatorConfirm": "Are you sure to remove this authenticator?", - "addNewAuthenticator": "Add a authenticator", + "addNewAuthenticator": "Add a passkey", "hardwareAuthenticator": "Hardware authenticator", "copied": "Copied to clipboard.", "pleaseManuallyCopy": "Current browser does not support, please copy manually.", @@ -454,17 +770,18 @@ "annotation": "Annotation", "rootFolder": "Relative root folder", "createdAt": "Created at", - "action": "Action", - "readonlyOn": "Turn on readonly", - "readonlyOff": "Turn off readonly", - "useProxyOn": "Turn on reverse proxy", - "useProxyOff": "Turn off reverse proxy", + "action": "Actions", + "readonlyOn": "Readonly", + "readonlyOff": "Read & Write", + "proxy": "Reverse Proxy", + "none": "None", + "proxied": "Proxied", "delete": "Delete", "listEmpty": "No records.", "createNewAccount": "Create new account", "taskType": "Task type", "taskStatus": "Status", - "lastProgress": "Last progress", + "taskProgress": "Task progress", "errorDetails": "Error details", "queueing": "Queueing", "processing": "Processing", @@ -479,7 +796,6 @@ "compressing": "Compressing", "decompressing": "Decompressing", "downloading": "Downloading", - "transferring": "Transferring", "indexing": "Indexing", "listing": "Inserting", "allShares": "Shared", @@ -490,13 +806,19 @@ "downloadNumber": "Downloads", "viewNumber": "Views", "language": "Language", - "iOSApp": "iOS App", - "connectByiOS": "Connect to <0>{{title}} through iOS devices.", - "downloadOurApp": "Download our iOS APP:", + "iOSApp": "iOS/iPadOS App", + "connectByiOS": "Connect to <0>{{title}} through iOS/iPadOS devices.", + "downloadOurApp": "Download our APP:", "fillInEndpoint": "Scan below QR Code with our App (DO NOT use other app to scan):", - "loginApp": "You can start using the iOS App now. If you encounter problems with the QR Code, you can also try to manually enter your username and password to log in.", - "aboutCloudreve": "About Cloudreve", - "githubRepo": "GitHub Repository", - "homepage": "Homepage" + "loginApp": "You can start using the App now. If you encounter problems with the QR Code, you can also try to manually enter your username and password to log in.", + "relocateFileTo": "Relocate storage policy to {{policy}} for <0>{{more}}", + "extractFileTo": "Extract <0>{{more}} to <1>", + "createArchiveTo": "Create archive file to <1> for <0>{{more}}" + }, + "vas": { + "points": "Points", + "quota": "Quota", + "used": "Used - {{size}}", + "total": "Total - {{size}}" } -} +} \ No newline at end of file diff --git a/public/locales/en-US/common.json b/public/locales/en-US/common.json index 8d3c7d5..da1bbb2 100644 --- a/public/locales/en-US/common.json +++ b/public/locales/en-US/common.json @@ -3,17 +3,31 @@ "unknownError": "Unknown error", "errLoadingSiteConfig": "Unable to load site configuration: ", "newVersionRefresh": "A new version of the current page is available and ready to be refreshed.", - "errorDetails": "Error details", + "errorDetails": "Details", "renderError": "There is an error in the page rendering, please try refreshing this page.", "ok": "OK", "cancel": "Cancel", "select": "Select", "copyToClipboard": "Copy", "close": "Close", + "dismiss": "Dismiss", "intlDateTime": "{{val, datetime}}", + "seconds": "s [seconds]", + "minutes": "m [minutes] s [seconds]", + "hours": "H [hours] m [minutes]", + "days": "{{d}} days", "timeAgoLocaleCode": "en_US", "forEditorLocaleCode": "en", "artPlayerLocaleCode": "en", + "requestID": "Request ID: {{id}}", + "object": "Object", + "error": "Error", + "areYouSure": "Are you sure?", + "incorrectSizeInput": "Incorrect size input", + "of": "of", + "rowsPerPage": "Rows per page", + "custom": "Custom", + "enter": "Enter", "errors": { "401": "Please login.", "403": "You are not allowed to perform this action.", @@ -35,7 +49,7 @@ "40017": "This account has been blocked.", "40018": "This account is not activated.", "40019": "This feature is not enabled.", - "40020": "Wrong password or email address.", + "40020": "Invalid or expired credential.", "40021": "User not found.", "40022": "Verification code not correct.", "40023": "Login session not exist.", @@ -68,6 +82,12 @@ "40069": "Incorrect password.", "40070": "This share doesn't support preview.", "40071": "Invalid signature.", + "40073": "File being occupied.", + "40074": "Too many files selected.", + "40079": "Max walked files limit exceeded, try to narrow down the scope of the operation.", + "40081": "Operation not fully succeeded.", + "40082": "Only file owner can perform this action.", + "40080": "Incorrect email or password.", "50001": "Database operation failed. ({{message}})", "50002": "Failed to sign the URL or request. ({{message}})", "50004": "I/O operation failed. ({{message}})", diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index 0dd11e7..68f6af7 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -1,7 +1,7 @@ { - "errors":{ + "errors": { "40036": "Default storage policy cannot be deleted.", - "40037": "{{message}} file(s) are using this policy, please delete those files first.", + "40037": "Soem file blob(s) are using this policy, please delete those file blobs first.", "40038": "{{message}} group(s) are using this policy, please unlink those groups first.", "40040": "Cannot perform such action on system group.", "40041": "{{message}} users are still in this group, please delete or unlink those users first.", @@ -10,6 +10,7 @@ "40046": "Cannot perform such action on master node.", "40060": "Slave node cannot send callback request to master, please check master node setting: Basic - Site Information - Site URL, please make sure slave node can access this url. ({{message}})", "40061": "Mismatched Cloudreve version. ({{message}})", + "40086": "The node is being used by the following storage policies: {{message}}.", "50008": "Failed to update setting. ({{message}})", "50009": "Failed to add CORS policy." }, @@ -17,7 +18,6 @@ "summary": "Summary", "settings": "Settings", "basicSetting": "Basic", - "publicAccess": "Public Access", "email": "Email", "transportation": "Transmission", "appearance": "Appearance", @@ -28,18 +28,28 @@ "groups": "Groups", "users": "Users", "files": "Files", + "entities": "File Blobs", "shares": "Shares", - "tasks": "Tasks", + "tasks": "Background Tasks", "remoteDownload": "Remote Download", "generalTasks": "General", "title": "Dashboard", - "dashboard": "Cloudreve Dashboard" + "dashboard": "Cloudreve Dashboard", + "userSession": "User session", + "fileSystem": "Filesystem", + "mediaProcessing": "Media processing", + "queue": "Queue", + "events": "Events", + "server": "Server" }, "summary": { - "newsletterError": "Failed to load newsletter.", + "generatedAt": "Generated at <0>", "confirmSiteURLTitle": "Confirm site URL", - "siteURLNotSet": "You have not set the site URL yet, do you want to set it to the current {{current}} ?", - "siteURLNotMatch": "The site URL you set does not match the current one, do you want to set it to the {{current}} ?", + "siteURLNotMatch": "The site URL you set does not contains the current one ({{current}}), do you want to add it to the list?", + "setAsPrimary": "Set as primary site URL", + "setAsPrimaryDes": "Set {{current}} as the primary site URL, used for communication with external services and receiving callbacks. Please use a URL that can be accessed by WAN.", + "setAsSecondary": "Add to secondary URLs", + "setAsSecondaryDes": "Add {{current}} to secondary URLs, Cloudreve will automatically select whether to use it based on the URL actually accessed by the user.", "siteURLDescription": "This setting is very important, make sure it matches the actual URL of your site. You can change this setting in Settings - Basic.", "ignore": "Ignore", "changeIt": "Change it", @@ -47,35 +57,230 @@ "summary": "Summary", "totalUsers": "Users", "totalFiles": "Files", - "publicShares": "Public shares", - "privateShares": "Private shares", + "shareLinks": "Share links", + "totalBlobs": "Blobs", "homepage": "Homepage", "documents": "Documents", "forum": "Forum", "forumLink": "https://github.com/cloudreve/Cloudreve/discussions", - "telegramGroup": "Telegram group", - "telegramGroupLink": "https://t.me/cloudreve_global", + "discordCommunity": "Discord community", "buyPro": "Upgrade to Pro", "publishedAt": "published at <0>", - "newsTag": "announcements" + "newsTag": "announcements", + "licenseExpireAt": "License expiration date", + "permanentLicense": "Permanent license", + "offlineLicenseExpireAy": "Offline license expiration date", + "offlineLicenseDes": "Cloudreve will automatically update the offline license before it expires if your server is connected to the network.", + "licensedDomains": "Licensed domains", + "renew": "Refresh offline license", + "manageLicense": "Manage license", + "volPurchase": "The client VOL license needs to be purchased separately from the <0>License Management Dashboard. The VOL license allows your users to connect to your site using the <1>Cloudreve iOS for free, without the need for users to pay for a subscription for the iOS app itself. After purchasing a license, please click \"Refresh offline license\" below.", + "iosVol": "iOS client volume license (VOL)", + "refreshSuccessfully": "Refreshed successfully.", + "manualRefresh": "Manually refresh offline license", + "manualRefreshDes": "Failed to refresh offline license automatically, please try to log in to the <0>License Management Dashboard to get the latest offline license and paste it below." + }, + "queue": { + "queueName_io_intense": "IO Intensive", + "queueName_io_intenseDes": "Queue for handling large amounts of IO operations, including: storage policy transfer, decompression, compression.", + "queueName_media_meta": "Media Metadata Extraction", + "queueName_media_metaDes": "Used to extract metadata from media files.", + "queueName_recycle": "Blob Recycling", + "queueName_recycleDes": "Used to delete expired file blobs.", + "queueName_thumb": "Thumbnail Generation", + "queueName_thumbDes": "Used to generate thumbnails for files.", + "queueName_remote_download": "Remote Download", + "queueName_remote_downloadDes": "Used to process remote download tasks.", + "failed": "Failed ({{count}})", + "success": "Success ({{count}})", + "suspending": "Suspended ({{count}})", + "busyWorker": "Processing ({{count}})", + "submited": "Submitted ({{count}})", + "editQueueSettings": "Edit queue settings - {{name}}", + "workerNum": "Worker threads", + "workerNumDes": "Maximum number of tasks to be executed in parallel in the task queue", + "maxExecution": "Maximum execution time", + "maxExecutionDes": "Maximum execution time (seconds) for a task, after which the task will be terminated.", + "backoffFactor": "Backoff factor", + "backoffFactorDes": "Growth factor for task retry time intervals.", + "backoffMaxDuration": "Maximum backoff time", + "backoffMaxDurationDes": "Maximum backoff time (seconds) for task retries.", + "maxRetry": "Maximum retries", + "maxRetryDes": "Maximum number of retries after a task failure.", + "retryDelay": "Retry delay", + "retryDelayDes": "Initial delay time (seconds) for task retries." }, "settings": { + "resetUrl": "Reset URL", + "exceedToleranceDays": "Tolerance days for banning", + "activateUrl": "Activate URL", + "domainNotLicensed": "Domain not licensed", + "domainNotLicensedDes": "The site URL you set contains an unauthorized domain, please add this subdomain in the <0>License Management Dashboard and click the button below to update the license and try again.", + "showSettings": "Show settings", + "perPage": "{{num}} per page", + "noNodes": "No nodes available.", + "extractMediaMeta": "Extract media metadata", + "extractMediaMetaDes": "Extract media file metadata for display and search. By default, non-local storage policies will only use the \"Native in storage policy\" generator. You can extend the thumbnail capability of third-party storage policies by enabling the \"Extractor proxy\" feature in storage policy setting page.", + "exif": "EXIF", + "exifDes": "Extract EXIF metadata from image files for display and search.", + "music": "Music metadata", + "musicDes": "Extract metadata from music files, including title, artist, album, etc.", + "ffprobe": "FFprobe", + "ffprobeDes": "Use FFprobe to extract metadata from video and audio files.", + "maxSizeLocal": "Max file size (Local storage)", + "maxSizeLocalDes": "Maximum file size for metadata extraction when the file is stored in local storage policy, 0 means no limit.", + "maxSizeRemote": "Max file size (Remote storage)", + "maxSizeRemoteDes": "Maximum file size for metadata extraction when the file is stored in third-party storage policies, 0 means no limit.", + "exifBruteForce": "Use brute force if necessary", + "exifBruteForceDes": "When enabled, the entire file will be scanned to find EXIF data if it cannot be found in the standard header location. This may increase processing time but can find EXIF data in non-standard locations.", + "musicCover": "Music cover", + "musicCoverDes": "Extract album cover from music files, supports ID3 (v1, 2.2, 2.3 and 2.4) container. This generator depends on any other image thumbnail generator (Cloudreve built-in or VIPS).", + "notAppliedToNativeGenerator": "{{prefix}}Not applicable to native generator of storage policies.", + "fileBlobMargin": "File Blob URL Cache Margin (seconds)", + "fileBlobMarginDes": "When the same file Blob is requested multiple times, if the initial URL has a remaining validity period greater than the margin, the same URL will be reused.", + "fileBlobTimeout": "File Blob URL TTL (seconds)", + "fileBlobTimeoutDes": "Limit the validity period of the temporary URL obtained when users open or download files, only applicable to local storage policies, WebDAV, or files downloaded through Cloudreve relaying.", + "wopiSessionTimeout": "WOPI session TTL (seconds)", + "wopiSessionTimeoutDes": "Limit the validity period of a single session when users edit files using WOPI. After expiration, users need to reopen the file from Cloudreve.", + "oauthRefresh": "Refresh interval for OAuth storage policy", + "oauthRefreshDes": "Set how often to refresh the OAuth credentials for storage policies (e.g. OneDrive) that require OAuth. This can prevent credential expiration due to long periods of inactivity", + "transitParallelNum": "Max parallel relaying transfers", + "transitParallelNumDes": "The maximum number of parallel uploads when a single server-side file relaying transfer task contains multiple files.", + "failedChunkRetry": "Maximum number of retries for chunk upload failures", + "failedChunkRetryDes": "The maximum number of retries for chunk upload failures, only applicable to server-side uploads or relaying transfers.", + "cacheChunks": "Cache streaming chunks", + "cacheChunksDes": "If enabled, the chunk data will be cached in the system temporary directory during streaming transfer, so that it can be used for retrying failed chunk uploads;\n If disabled, streaming transfer chunk uploads will not take up extra disk space, but the entire upload will fail immediately if the chunk upload fails.", + "folderPropsTimeout": "Folder statistics cache TTL (seconds)", + "folderPropsTimeoutDes": "The validity period of the result cache when users calculate folder statistics (size, number of files, etc.).", + "slaveAPIExpiration": "Slave API signature TTL (seconds)", + "slaveAPIExpirationDes": "The signature validity period used by the master node when accessing the slave node API.", + "uploadSessionTimeout": "Upload session TTL (seconds)", + "uploadSessionDes": "In a valid upload session period, for supported storage policies, users can resume unfinished tasks. The maximum value that can be set is limited by the rules of different storage policy providers.", + "archiveTimeout": "Server-side batch download session TTL (seconds)", + "advanceOptions": "Advanced options", + "emojiOptions": "Emoji options", + "addCategorize": "Add a category", + "category": "Category", + "searchQuery": "File categorize query", + "importWopi": "Import WOPI app settings", + "wopiEndpoint": "WOPI Discovery Endpoint", + "wopiDes": "Extend Cloudreve's online preview and editing capabilities by integrating with online document processing systems that support the WOPI protocol. Please fill in the WOPI service discovery address here, such as https://example.com/hosting/discovery", + "embeddedWebpageViewer": "Embedded Webpage Viewer", + "wopiViewer": "WOPI Application", + "ext": "Extension", + "invalidWopiActionMapping": "Invalid WOPI action mapping", + "woapiActionMapping": "WOPI action mappings", + "drawioHost": "DrawIO instance", + "drawioHostDes": "You can use URL for self-hosted instance.", + "openInNew": "Open in new window", + "openInNewDes": "If checked, it will directly pop up a new tab to open this application.", + "maxSize": "Max file size", + "maxSizeDes": "The maximum file size supported by this application. 0 means no limit. If the file exceeds this size, it will still bec opened, but user will be warned.", + "srcEncodedVar": "URL-encoded file Blob temporary access URL", + "srcVar": "File blob temporary access URL", + "nameEncodedVar": "URL-encoded file name", + "versionEntityVar": "The Blob ID of the opened file version, empty means the latest version.", + "fileIdVar": "File ID", + "userIdVar": "User ID, empty when not logged in.", + "userDisplayNameVar": "URL-encoded user display name.", + "fileViewers": "File applications", + "addViewer": "Add an application", + "viewerGroupTitle": "Application group #{{index}}", + "viewerType": "Type", + "displayName": "Display name", + "displayNameDes": "Display name to users, support i18next key.", + "viewerEnabled": "Enabled", + "newFileAction": "New file actions", + "newFileActionDes": "By adding this mapping, users will see this application option when clicking the \"New\" button.", + "addNewFileAction": "Add a mapping", + "builtinViewerType": "Builtin application", + "wopiViewerType": "WOPI", + "customViewerType": "Customized", + "nMapping": "{{num}} mapping(s)", + "editViewerTitle": "Edit {{name}}", + "builtInIconUrlDes": "This built-in application has a default icon. When the icon URL is left blank, the default icon will be used.", + "viewerUrl": "Application URL", + "viewerUrlDes": "URL of customized application, <0>magical variables are supported.", + "addIcon": "Add a icon", + "exts": "Extension list", + "icon": "Icon", + "iconUrl": "Icon URL", + "iconColor": "Color", + "iconColorDark": "Color (Dark mode)", + "fileIcons": "File icons", + "builtinIcon": "Built in", + "mimeMapping": "MIME type mapping", + "mimeMappingDes": "MIME type mapping in JSON format, where the key is the file extension and the value is the MIME type. Cloudreve will determine the file MIME type based on the file extension and this setting.", + "mapProvider": "Map provider", + "mapProviderDes": "Map provider used to display media location information.", + "mapGoogle": "Google Maps", + "mapOpenStreetMap": "OpenStreetMap", + "tileType": "Default tile type", + "tileTypeDes": "Default tile type for Google Maps.", + "tileTypeTerrain": "Terrain", + "tileTypeSatellite": "Satellite", + "tileTypeGeneral": "Regular", + "maxPageSize": "Max page size", + "maxPageSizeDes": "Limit the maximum number of files that users can adjust per page.", + "maxRecursiveSearch": "Max recursive search count", + "maxRecursiveSearchDes": "The maximum number of recursive searches allowed when searching for files. If the number of files searched exceeds this limit, the search will stop and warn the user.", + "maxBatchSize": "Max batch size", + "maxBatchSizeDes": "The maximum number of files that users can operate in a batch, only the top-level will be counted, and the number of files under subdirectories will not be counted.", + "defaultPagination": "Pagination method for file list", + "cursorPagination": "Cursor pagination", + "cursorPaginationDes": "More files will be automatically loaded when the user scrolls to the bottom. This method performs better for large file lists, but the total number of pages cannot be seen.", + "offsetPagination": "Offset pagination", + "offsetPaginationDes": "Pagination navigation will be displayed at the bottom of the page, users can see the total number of pages and jump to a specific page. This method performs slightly worse for large file lists.", + "defaultPaginationDes": "Cursor pagination will be forced to use when searching, regardless of the above settings.", + "publicResourceMaxAge": "Static resource cache max age (seconds)", + "publicResourceMaxAgeDes": "The max age of cache for public accessible static resources (e.g. files, thumbnails and user profile pictures).", + "cronDes": "{{des}} A correct <0>Cron syntax is required here. Restarting Cloudreve is needed to take effect.", + "entityCollectInterval": "File Blob recycle interval", + "entityCollectIntervalDes": "Set how often to scan and delete expired file blobs.", + "trashBinInterval": "Trash bin scan interval", + "trashBinIntervalDes": "Set how often to scan and delete expired files in the trash bin.", + "logtoName": "Sign-in method name", + "logtoNameDes": "Name of the sign-in method, displayed to users. Default is \"SSO\", support i18next key.", + "logtoDirectSSO": "Direct sign-in", + "logtoDirectSSODes": "If you want to skip the Logto login screen and directly jump to the third-party login or SSO, please fill in the identifier of the social connector here. For details, please refer to <0>Logto documentation.", + "logtoEndpoint": "Logto endpoint", + "logtoEndpointDes": "The Logto endpoint url obtained from the application management panel, which can be a self-hosted instance.", + "logtoKey": "Application secret", + "logtoKeyDes": "Application secret created in the application management page.", + "logtoAppIDDes": "Application ID created in the application management page.", + "logto": "Logto", + "logtoDes": "With <0>Logto, you can achieve more third-party platform sign-ins, such as Apple, GitHub, Microsoft Entra ID, Google, SMS, etc. Please create a \"Traditional Web Application\" in the Logto management portal and add {{url}} to the \"Redirect URIs\".", + "thirdPartySignIn": "Third-party sign-in", + "logo": "LOGO", + "logoDes": "URL of the LOGO, please provide different logos for dark and light modes.", + "dark": "Dark mode", + "light": "Light mode", + "tosUrl": "Terms of service URL", + "tosUrlDes": "Will be displayed in the footer of the login or registration page, leave it blank to not display.", + "privacyUrl": "Privacy policy URL", + "privacyUrlDes": "Will be displayed in the footer of the login or registration page, leave it blank to not display.", + "addSecondary": "Add secondary site URL", + "secondarySiteURL": "Secondary", + "secondaryDes": "You can also add other secondary URLs, Cloudreve will automatically select whether to use it based on the URL actually accessed by the user.", + "primarySiteURL": "Primary", + "primarySiteURLDes": "Primary site URL is used for communication with external services and receiving callbacks (e.g. storage provider), please use a URL that can be accessed by WAN.", + "revert": "Revert changes", "saved": "Settings saved.", "save": "Save", "basicInformation": "Basic Information", - "mainTitle": "Main title", - "mainTitleDes": "Main title of the website.", - "subTitle": "Subtitle", - "subTitleDes": "Subtitle of the website.", + "mainTitle": "Site name", + "mainTitleDes": "Name of the instance.", "siteDescription": "Site description", "siteDescriptionDes": "Description of the website, which may be displayed in the shared page summary.", "siteURL": "Site URL", - "siteURLDes": "Very important, please make sure it is consistent with the actual situation. When using cloud storage policy and payment platform, please fill in the address that can be accessed by WAN.", "customFooterHTML": "Custom footer HTML", "customFooterHTMLDes": "Custom HTML code inserted at the bottom of the page.", - "pwa": "Progressive Web Application (PWA)", + "announcement": "Announcement", + "announcementDes": "Announcements displayed to logged-in users. Blank value will not be displayed. After this content is changed, all users will see the announcement again.", + "supportHTML": "Enter HTML or plain text.", + "branding": "Branding", "smallIcon": "Small icon", - "smallIconDes": "URL of the small icon with the ico as extension", + "smallIconDes": "URL of the small icon with the ico as extension.", "mediumIcon": "Medium icon", "mediumIconDes": "URL of the medium icon, prefer size at 192x192, png format.", "largeIcon": "Large icon", @@ -92,20 +297,20 @@ "allowNewRegistrations": "Accept new signups", "allowNewRegistrationsDes": "After disabled, no new users can be registered, unless manually added by admins.", "emailActivation": "Email activation", - "emailActivationDes": "After enabled, new users need to click the activation link in the email to complete signups. Please make sure the email delivery settings are correct, otherwise the activation email will not be delivered.", + "emailActivationDes": "After enabled, new users need to click the activation link in the email to complete signups. Please make sure the <0>email delivery settings are correct, otherwise the activation email will not be delivered.", "captchaForSignup": "Captcha for signups", "captchaForSignupDes": "Whether to enable the captcha for signups.", "captchaForLogin": "Captcha for logins", "captchaForLoginDes": "Whether to enable the captcha for logins.", "captchaForReset": "Captcha for resetting password", "captchaForResetDes": "Whether to enable the captcha for resetting password.", - "webauthnDes": "Whether to allow users to log in using a hardware authenticator, the website must enable HTTPS for this to work.", - "webauthn": "Hardware authenticator", + "webauthnDes": "Whether to allow users to sign-in with hardware authentication devices, such as: face, fingerprint or USB key; the site must enable HTTPS.", + "webauthn": "Sign-in with Passkeys", "defaultGroup": "Default group", "defaultGroupDes": "The initial user group after user registration.", "testMailSent": "Test email is sent.", "testSMTPSettings": "Test SMTP settings", - "testSMTPTooltip": "Before sending a test email, please save the changed SMTP settings; the email delivery results will not be fed back immediately, if you do not receive a test email for a long time, please check the error log output by Cloudreve in the terminal.", + "testSMTPTooltip": "Cloudreve will use your current SMTP settings to send a test email, no need to save settings before testing.", "recipient": "Recipient", "send": "Send", "smtp": "SMTP", @@ -136,105 +341,49 @@ "transportation": "Transmission", "workerNum": "Number of worker", "workerNumDes": "The maximum number of tasks to be executed in parallel by the master node task queue, restarting Cloudreve is needed to take effect.", - "transitParallelNum": "Number of transfer in parallel", - "transitParallelNumDes": "Maximum number of parallel co-processes for transfer tasks.", "tempFolder": "Temp folder", "tempFolderDes": "Used to store temporary files generated by tasks such as decompression, compression, etc.", "textEditMaxSize": "Max size of editable document files", - "textEditMaxSizeDes": "The maximum size of a document file that can be edited online, files beyond this size cannot be edited online. This setting applies to plain text, code and Office documents (WOPI).", - "failedChunkRetry": "Max chunk error retries", - "failedChunkRetryDes": "Maximum number of retries after a failed chunk, only for server-side uploads or transferring.", - "cacheChunks": "Cache chunk for retries", - "cacheChunksDes": "If enabled, streaming chunk uploads will cache chunk data in a temporary directory for retrying after failed uploads.\n If disabled, streaming chunk uploads do not take up additional hard disk space, but the entire upload will fail immediately after a single chunk failure.", + "textEditMaxSizeDes": "The maximum size of a document file that can be edited online, files beyond this size cannot be edited online. This setting applies to online Web editors such as plain text, code and Office documents (WOPI).", "resetConnection": "Reset connection after failed upload", "resetConnectionDes": "If enabled, the server will force to reset the connection if upload verification fails.", - "expirationDuration": "Expire Durations (seconds)", "batchDownload": "Batch download", - "downloadSession": "Download session", "previewURL": "Preview URL", - "docPreviewURL": "Doc preview URL", - "uploadSession": "Upload session", - "uploadSessionDes": "For supported storage policy, user can resume uploads within upload session expiration. Max value various from third-party storage providers.", - "downloadSessionForShared": "Download session in shares", - "downloadSessionForSharedDes": "Repeated downloads of shared files within this set period of time will not be counted in the total number of downloads.", - "onedriveMonitorInterval": "OneDrive upload monitor interval", - "onedriveMonitorIntervalDes": "At set intervals, Cloudreve will request OneDrive to check client uploads to ensure they're under control.", - "onedriveCallbackTolerance": "OneDrive callback timeout", - "onedriveCallbackToleranceDes": "Maximum time to wait for the callback after the OneDrive client has finished uploading, if it exceeds it, the upload will be considered failed.", - "onedriveDownloadURLCache": "OneDrive download cache", - "onedriveDownloadURLCacheDes": "Cloudreve can cache the result after getting the file download URL to reduce the frequency of hot API requests.", - "slaveAPIExpiration": "Slave API timeout (seconds)", - "slaveAPIExpirationDes": "Timeout time for master to wait for slave API request responses.", - "heartbeatInterval": "Node heartbeat interval (seconds)", - "heartbeatIntervalDes": "The interval at which the master node sends heartbeats to slave nodes.", - "heartbeatFailThreshold": "Heartbeat failure retry threshold", - "heartbeatFailThresholdDes": "The maximum number of retries the master can make after sending a heartbeat to a slave that fails. After all failed retries, the node will enter recovery mode.", - "heartbeatRecoverModeInterval": "Recover mode heartbeat interval (seconds)", - "heartbeatRecoverModeIntervalDes": "Interval between master attempts to reconnect to a node after the node has been marked as recovery mode.", - "slaveTransitExpiration": "Slave transfer timeout (seconds)", - "slaveTransitExpirationDes": "Maximum time that can be consumed by a slave to execute a file transfer task.", - "nodesCommunication": "Node Communication", "cannotDeleteDefaultTheme": "Cannot delete default theme.", - "keepAtLeastOneTheme": "Please reserve at least one theme.", - "duplicatedThemePrimaryColor": "Duplicated primart color.", - "themes": "Themes", - "colors": "Colors", "themeConfig": "Configs", "actions": "Actions", "wrongFormat": "Incorrect format.", - "createNewTheme": "Create new theme", - "themeConfigDoc": "https://v4.mui.com/customization/default-theme/", - "themeConfigDes": "Full available configurations can be referred at <0>Default Theme - Material-UI.", - "defaultTheme": "Default theme", - "defaultThemeDes": "The default them to use when the user does not specify a preferred one.", - "appearance": "Appearance", - "personalFileListView": "Default view for personal file list", - "personalFileListViewDes": "The default display view to use when the user does not specify a preferred one.", - "sharedFileListView": "Default view for shared file list", - "sharedFileListViewDes": "The default display view to use when the user does not specify a preferred one.", - "primaryColor": "Primary color", - "primaryColorText": "Text on primary color", - "secondaryColor": "Secondary color", - "secondaryColorText": "Text on secondary color", "avatar": "Avatar", "gravatarServer": "Gravatar server", "gravatarServerDes": "URL of Gravatar mirror server.", "avatarFilePath": "Avatar file path", - "avatarFilePathDes": "Path to save user's avatar files.", + "avatarFilePathDes": "Path to save user's avatar files, relative to the Cloudreve data folder.", "avatarSize": "Max avatar file size", "avatarSizeDes": "Maximum size of avatar files that users can upload.", - "smallAvatarSize": "Small avatar width", - "mediumAvatarSize": "Medium avatar width", - "largeAvatarSize": "Large avatar width", + "avatarImageSize": "Image size (px)", + "avatarImageSizeDes": "Selected profile image will be resized to the given size, in pixels.", "filePreview": "File Preview", - "officePreviewService": "Office preview service", - "officePreviewServiceDes": "You can use following magic variables:", - "officePreviewServiceSrcDes": "File URL", - "officePreviewServiceSrcB64Des": " Base64 encoded file URL", - "officePreviewServiceName": "File name", "thumbnails": "Thumbnails", "thumbnailDoc": "For more information about thumbnail, see the <0>document.", - "thumbnailDocLink":"https://docs.cloudreve.org/v/en/use/thumbnails", + "thumbnailDocLink": "https://docs.cloudreve.org/v/en/use/thumbnails", "thumbnailBasic": "Basic", - "generators": "Generators", + "generators": "Thumbnail generators", "thumbMaxSize": "Maximum original file size", "thumbMaxSizeDes": "The maximum original file size for which thumbnails can be generated, thumbnails will not be generated if files exceed this size.", - "generatorProxyWarning": "By default, non-local storage policies will only use the \"Native in storage policy\" generator. You can extend the thumbnail capability of third-party storage policies by enabling the \"Generator proxy\" feature.", + "generatorProxyWarning": "By default, non-local storage policies will only use the \"Native in storage policy\" generator. You can extend the thumbnail capability of third-party storage policies by enabling the \"Generator proxy\" feature in storage policy setting page.", "policyBuiltin": "Native in storage policy", - "policyBuiltinDes": "Use the native API from storage provider to process thumbnails. For local and S3 policy, this generator is not available and will automatically fallback to other generators. For other storage policies, please refer to the Cloudreve documentation for supported image formats.", - "cloudreveBuiltin":"Cloudreve built-in", + "policyBuiltinDes": "Use the native API from storage provider to process thumbnails. For local and S3 policy, this generator is not available and will automatically fallback to other generators. For other storage policies, please go to storage policy setting page to configure this generator.", + "cloudreveBuiltin": "Cloudreve built-in", "cloudreveBuiltinDes": "Only images in PNG, JPEG, GIF formats are supported using Cloudreve's built-in image processing capabilities.", "libreOffice": "LibreOffice", "libreOfficeDes": "Use LibreOffice to generate thumbnails for Office documents. This generator depends on any other image thumbnail generator (Cloudreve built-in or VIPS).", "vips": "VIPS", "vipsDes": "Use libvips to process thumbnail images, support more image formats, and consume less resources.", - "thumbDependencyWarning": "LibreOffice generators depend on Cloudreve built-in or VIPS generators, please enable either one.", + "thumbDependencyWarning": "LibreOffice or music cover generator depend on Cloudreve built-in or VIPS generators, please enable either one.", "ffmpeg": "FFmpeg", "ffmpegDes": "Use FFmpeg to generate video thumbnails.", - "libRaw": "LibRaw", - "libRawDes": "Use LibRaw to process RAW images", "executable": "Executable", - "executableDes": "The address or command of the third-party generator executable.", + "executableDes": "The path or command of the third-party generator executable.", "executableTest": "Test", "executableTestSuccess": "Generator works, version: {{version}}", "generatorExts": "Available extensions", @@ -245,23 +394,24 @@ "enableThumbProxy": "Use generator proxy", "proxyPolicyList": "Enabled storage policy", "proxyPolicyListDes": "Multi-selectable. If enabled, files whose storage policy does not support native generation, its thumbnails will be proxy generated by the Cloudreve.", - "thumbWidth": "Width", - "thumbHeight": "Height", - "thumbSuffix": "File suffix", - "thumbConcurrent": "Concurrent count", - "thumbConcurrentDes": "-1 means auto.", + "thumbWidth": "Max width", + "thumbHeight": "Max height", + "thumbSuffix": "Blob file suffix", + "thumbSuffixDes": "The suffix appended to the original Blob file name for the generated thumbnail, ", "thumbFormat": "Image format", "thumbFormatDes": "Available: png/jpg", "thumbQuality": "Quality", - "thumbQualityDes": "Compression quality percentage, valid only for jpg encoding.", + "thumbQualityDes": "Compression quality percentage, valid only for jpg encoding. ", "thumbGC": "Run GC after thumb generated", "captcha": "Captcha", "captchaType": "Captcha type", - "plainCaptcha": "Plain", + "captchaTypeDes": "Select captcha type and provider.", + "plainCaptcha": "Plain graphic", "reCaptchaV2": "reCAPTCHA V2", - "tencentCloudCaptcha": "Tencent Cloud Captcha", - "captchaProvider": "Provider of the captcha service.", - "plainCaptchaTitle": "Plain Captcha", + "turnstile": "Cloudflare Turnstile", + "turnstileSiteKey": "Site Key", + "turnstileSiteKSecret": "Secret", + "captchaProvider": "Captcha provider", "captchaWidth": "Width", "captchaHeight": "Height", "captchaLength": "Length", @@ -278,7 +428,7 @@ "showNoiseText": "Show noise text", "showSlimeLine": "Show slime lines", "showSineLine": "Show sine lines", - "siteKey": "Site KEY", + "siteKey": "Site Key", "siteKeyDes": "You can find it at <0>App Management Page.", "siteSecret": "Secret", "siteSecretDes": "You can find it at <0>App Management Page.", @@ -292,16 +442,332 @@ "tCaptchaSecretKeyDes": "You can find it at <0>Captcha Management Page.", "staticResourceCache": "Public static resources cache", "staticResourceCacheDes": "Max age of cache for public accessible static resources (e.g. local policy source link, download link).", - "wopiClient": "WOPI Client", - "wopiClientDes": "Extend Cloudreve's document online preview and editing capabilities by interfacing with online document processing systems that support the WOPI protocol. For more information, please refer to <0>Official Documentation.", - "wopiDocLink": "https://docs.cloudreve.org/v/en/use/wopi", - "enableWopi": "Enable WOPI", - "wopiEndpoint": "WOPI Discovery Endpoint", - "wopiEndpointDes": "Endpoint URL of WOPI Discovery API.", - "wopiSessionTtl": "Edit session TTL (seconds)", - "wopiSessionTtlDes": "The user opens an online editing document session with an expiration date, beyond which the session cannot continue to save new changes." + "creditSystem": "Credit system", + "creditAndVAS": "Credit and VAS", + "enableCredit": "Enable credit system", + "enableCreditDes": "Enable credit system to allow users to set prices for their share links.", + "creditPrice": "Credit price", + "creditPriceDes": "Price for recharging credit points with money (in minimum currency unit). Fill 0 to disable credit recharge.", + "shareScoreRate": "Share owner's commission rate", + "shareScoreRateDes": "Percentage (1-100) of credit points that share owners receive when their share links are purchased.", + "cronNotifyUser": "Scan interval for over-limit users", + "cronNotifyUserDes": "Scan and send email reminders to over-limit users, ", + "cronBanUser": "User ban schedule", + "cronBanUserDes": "Scan and ban users exceeding storage limits and buffer periods", + "anonymousPurchase": "Anonymous purchase", + "anonymousPurchaseDes": "Allow non-logged-in users to purchase share links directly", + "shopNavEnabled": "Show Shop Navigation", + "shopNavEnabledDes": "Display 'Shop' items in the sidebar navigation", + "paymentSettings": "Payment settings", + "currencyCode": "Currency code", + "currencyCodeDes": "Three-letter currency code (e.g., USD, CNY, EUR)", + "currencySymbol": "Currency symbol", + "currencySymbolDes": "Currency symbol to display (e.g., $, ¥, €)", + "currencyUnit": "Currency unit", + "currencyUnitDes": "Minimum currency unit (e.g., 100 for dollars/cents)", + "paymentProviders": "Payment providers", + "providerName": "Provider name, used to display to users.", + "providerType": "Provider type", + "providerKey": "Secret key", + "selectCurrency": "Select common currency", + "addPaymentProvider": "Add payment provider", + "stripeProvider": "Stripe", + "weixinProvider": "WeChat Pay", + "alipayProvider": "Alipay", + "customProvider": "Custom payment provider", + "customProviderDes": "Create a plugin to connect to other payment gateways, see <0>documentation for further details.", + "providerKeyDes": "API secret key from Stripe.", + "storageProductSettings": "Storage product", + "storageProductsDes": "Configure products that users can purchase to extend their storage space.", + "addStorageProduct": "Add SKU", + "editStorageProduct": "Edit SKU", + "storageSize": "Storage size", + "storageSizeBytes": "Size included in this SKU", + "duration": "Duration", + "durationSeconds": "Duration in seconds (e.g. 2592000 for 30 days)", + "price": "Price", + "priceInUnits": "Price (in minimum currency unit)", + "priceInUnitsDes": "Price will be displayed as:", + "chipLabel": "Label (optional)", + "chipLabelHelp": "A short text label displayed next to the product name", + "usePoints": "Allow paying with points", + "points": "Points", + "pointsHelp": "Number of points required to purchase this product", + "pointsUnit": "points", + "groupProductSettings": "Group product", + "groupProductsDes": "Configure products that users can purchase to join specific user groups.", + "addGroupProduct": "Add group product", + "editGroupProduct": "Edit group product", + "groupId": "Group ID", + "groupIdHelp": "The user group to upgrade to after purchasing this product.", + "description": "Description", + "descriptionHelp": "Enter features or benefits, one per line", + "receiptEmailTemplate": "Payment receipt template", + "receiptEmailTemplateDes": "Email template sent to users when a payment is confirmed.", + "activationEmailTemplate": "Account activation template", + "activationEmailTemplateDes": "Email template sent to users to activate their accounts.", + "quotaExceededEmailTemplate": "Storage quota exceeded template", + "quotaExceededEmailTemplateDes": "Email template sent to users when they exceed their storage quota.", + "resetPasswordEmailTemplate": "Password reset template", + "resetPasswordEmailTemplateDes": "Email template sent to users when they request a password reset.", + "addLanguage": "Add language", + "languageCodeDes": "Please select the language you want to add.", + "emailSubject": "Email saveChanges", + "emailSubjectDes": "The subject line of the email.", + "emailBody": "Email body", + "emailBodyDes": "HTML content of the email. You can use <0>magic variables to customize the email content.", + "orderTitle": "Order title", + "themeOptions": "Theme options", + "themeOptionsDes": "Configure custom theme options for your site. These themes will be available for users to select in their preferences.", + "primaryColor": "Primary color", + "secondaryColor": "Secondary color", + "primaryColorDark": "Primary color (Dark)", + "secondaryColorDark": "Secondary color (Dark)", + "addThemeOption": "Add theme option", + "editThemeOption": "Edit theme option", + "invalidThemeConfig": "Invalid theme configuration. Please check your JSON syntax.", + "themeConfiguration": "Theme configuration", + "themePreview": "Theme preview", + "lightTheme": "Light theme", + "darkTheme": "Dark theme", + "previewTitle": "Preview title", + "previewTextField": "Input field", + "previewPrimary": "Primary", + "previewSecondary": "Secondary", + "invalidThemePreview": "Invalid theme configuration for preview", + "duplicateThemeColor": "A theme with this primary color already exists. Please choose a different color.", + "themeDes": "Full available configurations can be referred at <0>Default theme viewer - Material-UI.", + "defaultTheme": "Default", + "auditLog": "Events", + "auditLogDes": "Configure which events should be recorded. Some events might be used by the system to provide additional features, e.g. file activity and sign in activity.", + "systemEvents": "System events", + "systemEventsDes": "Events related to system operations and status.", + "userEvents": "User events", + "userEventsDes": "Events related to user accounts, authentication, and profile changes.", + "fileEvents": "File events", + "fileEventsDes": "Events related to file operations such as upload, download, and modification.", + "shareEvents": "Share events", + "shareEventsDes": "Events related to file sharing and link access.", + "versionEvents": "Version events", + "versionEventsDes": "Events related to file version management.", + "mediaEvents": "Media events", + "mediaEventsDes": "Events related to media processing such as thumbnail generation.", + "filesystemEvents": "Filesystem events", + "filesystemEventsDes": "Events related to filesystem operations such as mounting and archive handling.", + "webdavEvents": "WebDAV events", + "webdavEventsDes": "Events related to WebDAV account management and access.", + "paymentEvents": "Payment events", + "paymentEventsDes": "Events related to payments, points, and membership management.", + "emailEvents": "Email events", + "emailEventsDes": "Events related to email sending and notifications.", + "toggleAll": "Toggle all", + "toggleAllDes": "Enable or disable all events in this category.", + "event": { + "server_start": "Server start", + "user_signup": "User signup", + "email_sent": "Email sent", + "user_activated": "User activated", + "user_login_failed": "Login failed", + "user_login": "User login", + "user_token_refresh": "Token refresh", + "file_create": "File created", + "file_rename": "File renamed", + "set_file_permission": "Permission changed", + "entity_uploaded": "File uploaded or updated", + "entity_downloaded": "File downloaded", + "copy_from": "Copy from", + "copy_to": "Copy to", + "move_to": "Move to", + "delete_file": "File deleted", + "move_to_trash": "Move to trash", + "share": "Share created", + "share_link_viewed": "Share link viewed", + "set_current_version": "Set current version", + "delete_version": "Delete version", + "thumb_generated": "Thumbnail generated", + "live_photo_uploaded": "Live photo uploaded", + "update_metadata": "Metadata updated", + "edit_share": "Share edited", + "delete_share": "Share deleted", + "mount": "Mount", + "relocate": "Relocate", + "create_archive": "Create archive", + "extract_archive": "Extract archive", + "webdav_login_failed": "WebDAV login failed", + "webdav_account_create": "WebDAV account created", + "webdav_account_update": "WebDAV account updated", + "webdav_account_delete": "WebDAV account deleted", + "payment_created": "Payment created", + "points_change": "Points changed", + "payment_paid": "Payment paid", + "payment_fulfilled": "Order fulfilled", + "payment_fulfill_failed": "Order fulfill failed", + "storage_added": "Storage added", + "group_changed": "Group changed", + "user_exceed_quota_notified": "Quota exceeded notification", + "user_changed": "User status changed", + "get_direct_link": "Get direct link", + "link_account": "Link external account", + "unlink_account": "Unlink external account", + "change_nick": "Change nickname", + "change_avatar": "Change avatar", + "membership_unsubscribe": "Membership unsubscribe", + "change_password": "Change password", + "enable_2fa": "Enable 2FA", + "disable_2fa": "Disable 2FA", + "add_passkey": "Add passkey", + "remove_passkey": "Remove passkey", + "redeem_gift_code": "Redeem gift code" + }, + "server": "Server", + "tempPath": "Temporary path", + "tempPathDes": "The directory for storing temporary files, relative to the Cloudreve data directory. Please ensure that no queue tasks are running before modifying it.", + "siteID": "Site ID", + "siteIDDes": "A unique ID for identifying the site, generally not needed to be modified.", + "siteSecretKey": "Master key", + "siteSecretKeyDes": "The master key used to encrypt user tokens and signatures. After rotation, all user tokens and signatures will be invalid. It takes effect after restarting Cloudreve.", + "rotateSecretKey": "Rotate master key", + "hashidSalt": "HashID salt", + "hashidSaltDes": "The salt value used to generate HashID. Please be cautious when changing it, as it will invalidate existing direct links and share links.", + "accessTokenTTL": "Access token TTL", + "accessTokenTTLDes": "The TTL of access tokens, in seconds.", + "refreshTokenTTL": "Refresh token TTL", + "refreshTokenTTLDes": "The TTL of refresh tokens, in seconds. It affects the duration of user login status.", + "cronGarbageCollect": "Garbage collection scan interval", + "cronGarbageCollectDes": "Set how often to scan and recycle expired data in temporary files and KV storage.", + "startWithProtocol": "Must start with http:// or https://", + "tlsWarning": "The current site is using https, filling in an http URL here may cause exceptions.", + "blobUrlCache": "Blob URL cache", + "clearBlobUrlCache": "Clear Blob URL cache", + "clearBlobUrlCacheDes": "To increase cache hit rate, Cloudreve caches and reuses Blob URLs. When the CDN address or other settings change, please clear the cache.", + "cacheCleared": "Cache cleared." + }, + "giftCodes": { + "giftCodesSettings": "Gift Codes", + "giftCodesManagement": "Gift Codes Management", + "giftCodesDescription": "Manage gift codes that users can redeem to obtain points, storage space, or group membership.", + "generateGiftCodes": "Generate Gift Codes", + "giftCodeQuantity": "Quantity", + "giftCodeQuantityHelp": "Number of gift codes to generate", + "giftCodeProductType": "Product Type", + "giftCodeTypePoints": "Points", + "giftCodeTypeStorage": "Storage", + "giftCodeTypeGroup": "Group", + "giftCodePointsAmount": "Points Amount", + "giftCodePointsAmountHelp": "Number of points to credit when code is redeemed", + "giftCodeProduct": "Product", + "selectStorageProduct": "Select storage product", + "selectGroupProduct": "Select group product", + "giftCodeId": "ID", + "giftCodeType": "Type", + "giftCodeAmount": "Amount", + "giftCode": "Gift Code", + "giftCodeStatus": "Status", + "giftCodeUsed": "Used", + "giftCodeUnused": "Available", + "giftCodeDeleted": "Gift code deleted successfully", + "giftCodesGenerated": "Gift codes generated successfully", + "noGiftCodes": "No gift codes available", + "generatedCodesTitle": "Generated Gift Codes", + "generatedCodesDescription": "Copy these gift codes to share with users. Each code can be used once.", + "copyAndClose": "Copy and Close", + "duratonTimes": "Quantity", + "duratonTimesDes": "How many quantity of the product is included in each gift code.", + "unknownProduct": "Unknown Product" }, "policy": { + "deletePolicyConfirmation": "Are you sure you want to delete the storage policy {{name}}?", + "streamSaver": "Download via browser", + "streamSaverDes": "When enabled, users' download requests will be handled by the browser. Due to the OneDrive storage policy limitation, the file name of the file downloaded directly by users cannot be the same as the file name in Cloudreve, using the browser to handle downloads can solve this problem.", + "oauthCallbackFailed": "Authorization failed", + "httpsRequired": "Entra ID application requires HTTPS redirect URL, but the current site is using HTTP, which may cause redirect failure after login, please manually replace the HTTPS in the browser address bar with HTTP.", + "authorizeMicrosoft": "Sign-in with Microsoft", + "redirectUrl": "Redirect URL", + "redirectUrlDes": "The current display is the latest redirect URL that meets the requirements. Please confirm if the redirect URL in the application settings is consistent with the current one.", + "authorizeOneDrive": "Confirm Entra ID application settings", + "authorizeOneDriveDes": "Please confirm if the following Entra ID application information is still valid. If needed, please make changes.", + "authorizeNow": "Authorize", + "authorizeAgain": "Authorize again", + "notGranted": "No authorized account, storage policy cannot be used.", + "granted": "Account authorized, credential refreshed at <0>{{time}}.", + "grantedNotRefresh": "Account authorized, credential not refreshed since last startup.", + "batchDeleteSize": "Maximum batch delete size", + "batchDeleteSizeDes": "Limit the maximum number of files that can be deleted in a single API request. This setting will not affect user batch file deletion. If not filled, the default value <0>1000 will be used. This is the maximum allowed value for official S3 API.", + "bucketPolicy": "Bucket policy", + "cdnOrCustomDomain": "CDN or custom CNAME", + "bucketDomain": "Bucket domain", + "bucketDomainDes": "Fill in the CDN-accelerated domain or custom CNAME domain you have bound for the storage bucket.", + "storageNodeInternal": "Storage node (Intranet Endpoint)", + "chunkSizeDesOssObs": "Allowed range: 100 KB ~ 5 GB.", + "chunkSizeDesQiniuCos": "Allowed range: 1 MB ~ 1 GB.", + "chunkSizeDesS3": "Allowed range: 5 MB ~ 5 GB.", + "thisIsACustomDomain": "This is a custom domain", + "thisIsACustomDomainDes": "If you have bound a custom domain to the storage bucket, and need to manage the bucket via the custom domain, please check this option. After enabled, Cloudreve will not attempt to append the Bucket name in the request domain.", + "addedManually": "I have set it manually", + "accessCredential": "Access credential", + "downloadTrafficDiagram": "Download traffic path demonstrationion", + "downloadRelay": "Download relay", + "downloadRelayDes": "When enabled, users' download requests will be proxyed by Cloudreve.", + "download": "Download", + "downloadCdn": "Download CDN", + "useDownloadCdn": "Use CDN for download traffic", + "skipSign": "Skip URL signature for CDN", + "skipSignDes": "If you have enabled \"Use source auth\" for this domain in bucket settings, please check this option.", + "cdnHost": "CDN host", + "downloadCdnDes": "The host, protocol, and port of the URL that users use to access files will be replaced with the CDN host you specified.", + "mediaExtractorProxy": "Proxy media extraction", + "mediaExtractorProxyDes": "Enable this feature to extract media metadata from files that are not supported by the storage provider's native extractors. Please configure the media extractor in <0>Media processing.", + "mediaExtractorNative": "native extractors", + "mediaExtractorOss": "Intelligent Media Management (IMM)", + "mediaExtractorQiniu": "Qiniu DORA", + "mediaExtractorCos": "Tencent Cloud Data Processing", + "mediaExtractorObs": "image processing service", + "nativeMediaMetaExts": "Enabled file extensions for <0>{{name}}", + "nativeMediaMetaExtsGeneralDes": "Separated by commas, empty value means disable <0>{{name}}.", + "nativeMediaMetaExtsRemote": "For slave storage, the default support is EXIF and music metadata, you can override this by configuring the slave node with more extractors.", + "nativeMediaMetaExtOss": " The Intelligent Media Management (IMM) service supports processing audio, video, and images. Image processing does not require manual configuration, but if you need to process audio or video, you need to manually activate IMM and bind it to the Bucket, please refer to <0>document for binding. After binding, please add the extensions you want to process to the above field.", + "nativeMediaMetaExtQiniu": "The Qiniu DORA service supports processing common audio, video, and images, no additional configuration is required, please fill in the extensions you want to process above.", + "nativeMediaMetaExtCos": "The Tencent Cloud Data Processing service supports processing audio, video, and images. Image processing does not require manual configuration, but if you need to process audio or video, please first go to <0>Tencent Cloud Data Processing to activate and bind the storage bucket, then go to Bucket settings - Media processing to activate the image processing service. After binding, please add the extensions you want to process to the above field.", + "nativeMediaMetaExtObs": "The image processing service supports <0>extracting image EXIF. No manual configuration is required, just add the extensions you want to process above.", + "thumbProxy": "Proxy thumbnail generation", + "thumbProxyDes": "Enable this feature to generate thumbnails for files that do not meet the native thumbnail conditions. Cloudreve will try to generate thumbnails and upload them to the storage side. Please configure the thumbnail generator in <0>Media processing.", + "nativeThumbnailMaxSize": "Max size of native thumbnails", + "nativeThumbnailMaxSizeDes": "Enter 0 to disable the size limit, files larger than this size will not use native thumbnails.", + "nativeThumbNailsSupportAllExts": "Enable for all file extension", + "nativeThumbNails": "File extensions for native thumbnails", + "nativeThumbNailsGeneralDes": "Separated by commas, emoty value means disable native thumb, for the file extensions listed above, Cloudreve will use the native thumbnail feature of the storage provider to generate thumbnails.", + "nativeThumbNailsGeneralRemote": " For slave storage, the builtin support is simple image and music cover thumbnails, you can override this by configuring the slave node with more generators..", + "nativeThumbNailsGeneralOss": "For Alibaba Cloud OSS storage, <0>image processing service will be used to generate thumbnails.", + "nativeThumbNailsGeneralQiniu": "For Qiniu Cloud storage, <0>image basic processing(imageView2) service will be used to generate thumbnails.", + "nativeThumbNailsGeneralCos": "For Tencent Cloud COS storage, <0>Tencent Cloud Data Processing service will be used to generate thumbnails.", + "nativeThumbNailsGeneralObs": "For Huawei Cloud OBS storage, <0>image processing service will be used to generate thumbnails.", + "preallocate": "Pre-allocate disk space", + "preallocateDes": "When enabled, the user's upload request will be pre-allocated disk space on the storage node, only effective on Linux or Darwin.", + "sourceWebEdit": "Web online editing", + "uploadRelay": "Upload relay", + "uploadRelayDes": "If enabled, users' upload requests will be relayed to the storage node via Cloudreve, due to the inability to perform chunked uploads, please adjust the maximum upload size limit of the web server accordingly.", + "customProxy": "Custom proxy", + "storageNode": "Storage provider", + "sourceWeb": "Web / Official app", + "sourceDav": "WebDAV", + "uploadTrafficDiagram": "Upload traffic path demonstrationion", + "node": "Storage node", + "nodeDes": "Please select a slave node for file storage, you can create or manage slave storage nodes in <0>Node list.", + "noBindedGroupWarning": "The current storage policy is not bound to any user group, please go to <0>Group list to bind the current storage policy to a user group.", + "nameRuleImmutable": " Modifying settings will not affect existing files in the storage policy. The Blob path is fixed after creation, even if the magic variables in it change, the path will not be updated.", + "uniqueVarRequired": "Please include at least one unique variable: {{uuid}}, {{randomkey8}}, {{randomkey16}}.", + "storageAndUpload": "Storage and Upload", + "blobFolderNaming": "Blob Storage Directory", + "blobFolderNamingDes": "The directory where file Blobs are stored, you can use <0>magic variables.", + "blobNameDes": "The name of the file Blob, you can use <0>magic variables, make sure it's absolutely unique, even for multiple uploads of the same file name in same path in a short time.", + "blobName": "Blob Name", + "basicInfo": "Basic info", + "editX": "Edit {{name}}", + "noGroupBinded": "No group binded", + "create": "Create", + "addXStoragePolicy": "Add {{type}} storage policy", + "loadSummary": "Load summary", + "policySummary": "{{count}} file Blobs ({{size}})", "sharp": "#", "name": "Name", "type": "Type", @@ -319,58 +785,20 @@ "oss": "Alibaba Cloud OSS", "cos": "Tencent Cloud COS", "onedrive": "OneDrive", - "s3": "AWS S3", + "s3": "S3 Compatible", + "obs": "Huawei Cloud OBS", "refresh": "Refresh", "delete": "Delete", "edit": "Edit", - "editInProMode": "Edit in pro mode", - "editInWizardMode": "Edit in wizard mode", "selectAStorageProvider": "Select a storage provider", - "comparesStoragePolicies": "Compare storage policies", - "comparesStoragePoliciesLink": "https://docs.cloudreve.org/v/en/use/policy/compare", - "storagePathStep": "Storage path", - "sourceLinkStep": "Source links", - "uploadSettingStep": "Uploading", - "finishStep": "Finish", - "policyAdded": "Storage policy added.", - "policySaved": "Storage policy saved.", - "editLocalStoragePolicy": "Edit local storage policy", - "addLocalStoragePolicy": "Add local storage policy", - "optional": "Optional", - "pathMagicVarDes": "Enter the physical path to the folder you want to store files. Either absolute or relative (relative to Cloudreve executable) path is supported. You can use magic variables in the path, which will be automatically replaced with the corresponding values when the file is uploaded; see <0>List of path magic variables for available magic variables.", - "pathOfFolderToStoreFiles": "Path of the folder", - "filePathMagicVarDes": "Do you want to rename the physical files that are uploaded? The renaming here will not affect the final file name presented to the user. Magic variables can also be used for file names, see <0>List of Magic Variables for file names for available magic variables.", - "autoRenameStoredFile": "Enable auto-renaming", - "keepOriginalFileName": "Use original file name", - "renameRule": "Rename rule", - "next": "Next", - "enableGettingPermanentSourceLink": "Allow user to get permanent file source link?", - "enableGettingPermanentSourceLinkDes": "When enabled, users can obtain a direct link to the contents of the file, for use in the Image Bed application or for your own use. You may also need to enable this feature in the user group settings to make it available for users.", - "allowed": "Enable", - "forbidden": "Disable", - "useCDN": "Do you want to use CDN for download and source links?", - "useCDNDes": "When enabled, the domain part of the URL which user uses to access files will be replaced with the CDN domain.", - "use": "Enable", - "notUse": "Disable", - "cdnDomain": "Select a protocol and enter the CDN domain:", - "cdnPrefix": "CDN domain", - "back": "Back", - "limitFileSize": "Do you want to limit the max size of one single file that can be uploaded?", - "limit": "Yes", - "notLimit": "No", - "enterSizeLimit": "Enter the max file size:", "maxSizeOfSingleFile": "Max single file size", - "limitFileExt": "Do you want to limit file extensions?", - "enterFileExt": "Enter the file extensions allowed to be uploaded, separated by semi-colon commas:", - "extList": "File extension list", - "chunkSizeLabel": "Specify the chunk size for resumable uploads. A value of 0 means no resumable uploads are used.", - "chunkSizeDes": "After enabling resumable upload, the files uploaded by users will be sliced into chunks and uploaded to the storage side one by one. After the upload is interrupted, users can choose to continue uploading from the last uploaded chunk.", + "maxSizeOfSingleFileDes": "Enter 0 to disable the limit.", + "enterFileExt": "Separated by semi-colon commas, leave blank to allow all file extensions.", + "extList": "Allowed file extensions", + "chunkSizeDes": "Specify the chunk size for chunked uploads. A value of 0 means no chunked uploads are used, but the maximum upload size may be limited by the web server.", + "chunkSizeDesSuffix": "{{prefix}} With chunked upload, the files uploaded by users will be sliced into chunks and uploaded to the storage side one by one. After the upload is interrupted, users can choose to continue uploading from the last uploaded chunk.", "chunkSize": "Chunk size", - "nameThePolicy": "Last step, name the storage policy:", - "policyName": "Storage policy name", - "finish": "Finish", - "furtherActions": "To use this storage policy, go to the user group setting page and bind this storage policy for the appropriate user group.", - "backToList": "Back to storage policy list", + "policyName": "The display name of the storage policy, also used to be presented to users.", "magicVar": { "fileNameMagicVar": "File name magic variables", "pathMagicVar": "Path magic variables", @@ -388,73 +816,57 @@ "uuidV4": "UUID V4", "date": "Date", "dateAndTime": "Date and time", + "randomNumber": "Random number within range", "year": "Year", "month": "Month", "day": "Day", "hour": "Hour", "minute": "Minute", "second": "Second", - "userUploadPath": "Upload path" + "path": "The initial path while user uploads the file" }, - "storageNode": "Storage node", - "communicationOK": "Communication successful.", - "editRemoteStoragePolicy": "Edit remote storage policy", - "addRemoteStoragePolicy": "Add remote storage policy", - "remoteDescription": "The remote storage policy allows you to use a server that is also running Cloudreve as the slave storage node, and users' upload and download traffic are directly transmitted over HTTP.", - "remoteCopyBinaryDescription": "Copy the Cloudreve executable with the same version as master to the server you want to use as a slave storage node.", - "remoteSecretDescription": "The following is the randomly generated slave secret, usually no need to change. If you have customization requirement, you can fill in your own secret into the following field.", - "remoteSecret": "Slave node secret", - "modifyRemoteConfig": "Modify the Cloudreve config file on slave node.", - "addRemoteConfigDes": " Create a new <0>conf.ini file in the same directory as the slave Cloudreve, fill in the slave configuration, and start/restart the slave Cloudreve. The following is an example configuration for your slave Cloudreve, where the secret section is pre-filled in for you as generated in the previous step.", - "remoteConfigDifference": "The configuration file format on the slave side is roughly the same as the master side, with the following differences:", - "remoteConfigDifference1": "The <1>mode field under the <0>System section must be changed to <2>slave.", - "remoteConfigDifference2": "You must specify the <1>Secret field under the <0>Slave section, whose value is the secret filled in or generated in step 2.", - "remoteConfigDifference3": "The cross-origin configuration, i.e. the contents of the <0>CORS field, must be enabled, as described in the example above or in the official documentation. If the configuration is not correct, users will not be able to upload files to the slave node via the web browser.", - "inputRemoteAddress": "Enter slave node address.", - "inputRemoteAddressDes": "If HTTPS is enabled on the master, the slave also needs to enable it and fill in the address with HTTPS protocol below.", - "remoteAddress": "Slave node address", - "testCommunicationDes": "After completing the above steps, you can test if the communication is working by clicking the test button below.", - "testCommunication": "Test slave communication", - "pathMagicVarDesRemote": "Enter the physical path to the folder you want to store files. Either absolute or relative (relative to slave Cloudreve executable) path is supported. You can use magic variables in the path, which will be automatically replaced with the corresponding values when the file is uploaded; see <0>List of path magic variables for available magic variables.", "storageBucket": "Storage bucket", "editQiniuStoragePolicy": "Edit Qiniu storage policy", "addQiniuStoragePolicy": "Add Qiniu storage policy", "wanSiteURLDes": "Before using this policy, please make sure that the address you entered in Basic Settings - Site Information - Site URL matches the actual address and <0>can be accessed properly by WAN.", - "createQiniuBucket": "Go to <0>Qiniu dashboard to create a storage bucket.。", - "enterQiniuBucket": "Enter the \"bucket name\" you just created:", + "enterQiniuBucket": "Go to <0>Qiniu dashboard to create a storage bucket. Enter the \"Bucket name\" you just created.", + "aclType": "Access control type", + "accessTypePulic": "Public read private write", + "accessTypePrivate": "Private read/write", + "accessType": "Access type", "qiniuBucketName": "Bucket name", - "bucketTypeDes": "Select the type of bucket you just created. We recommend selecting \"Private bucket\" for higher security.", - "privateBucket": "Private bucket", - "publicBucket": "Public bucket", + "cosObsBucketName": "Bucket name", + "bucketType": "Bucket ACL", + "bucketTypeDes": "Select the type of ACL for the bucket you just created.", + "privateBucket": "Private", + "privateDes": "Cloudreve will sign the file URL.", + "publicBucket": "Public read", + "publicStorage": "Public", + "publicDes": "Not recommended, Cloudreve will directly return the file's direct link, which cannot effectively control the access of files.", "bucketCDNDes": "Fill in the CDN-accelerated domain name you have bound for the storage bucket.", "bucketCDNDomain": "CDN domain", "qiniuCredentialDes": "Go to Personal Center - Credential Management in the Qiniu dashboard and fill in the obtained AK, SK.", "ak": "AK", "sk": "SK", "cannotEnableForPrivateBucket": "If this feature is enabled for private bucket, you need to enable \"Use redirected source link\" for user groups.", - "limitMimeType": "Do you want to limit MimeTypes of file that can be uploaded?", - "mimeTypeDes": "Enter the MimeType of the allowed files, and separate multiple MimeTypes with a comma. Qiniu will detect the file content to determine the MimeType, and allow the upload if the MimeType is presented in your list.", - "mimeTypeList": "MimeTypes list", "chunkSizeLabelQiniu": "Specify the chunk size for resumable uploads. Allowed range is 1 MB - 1 GB.", - "createPlaceholderDes": "Do you want to create a placeholder file and deduct user capacity when users start uploading? If enabled, Cloudreve will prevent users from maliciously initiating multiple upload requests but not completing the upload.", - "createPlaceholder": "Create placeholder files", - "notCreatePlaceholder": "Don't create", "corsSettingStep": "CORS policy", - "corsPolicyAdded": "CORS policy is added successfully.", + "corsPolicyAdded": "CORS policy is added.", "editOSSStoragePolicy": "Edit Alibaba Cloud OSS storage policy", "addOSSStoragePolicy": "Add Alibaba Cloud OSS storage policy", - "createOSSBucketDes": "Go to <0>OSS Dashboard to create a Bucket。Attention: You can only use bucket with SKU of <1>Standard storage or <2>Low frequency storage, <3>Archive storage is not supported.", + "createOSSBucketDes": "Go to <0>OSS Dashboard to create a Bucket. Only <1>Standard and <2>IA storage classes are supported.", "ossBucketNameDes": "Enter the your specified <0>Bucket name:", "bucketName": "Bucket name", "publicReadBucket": "Public read", - "ossEndpointDes": "Go to Bucket summary page, enter the <2>Endpoint under <1>External access section, in <0>Access domain page.", + "ossEndpointDes": "Go to Bucket summary page, enter the <2>Port under <1>Access Over Internet section, in <0>Endpoint page.", + "ossEndpointDesInternalHint": "If you need to configure Intranet or custom domain endpoint, you can set it after creating the storage policy.", + "obsEndpointCnameHint": "If you need to configure custom domain endpoint, you can set it after creating the storage policy.", "endpoint": "EndPoint", - "endpointDomainOnly": "Wrong format, just enter the hostname of the domain.", - "ossLANEndpointDes": "If your Cloudreve is deployed in Alibaba Cloud compute related services which are under the same availability zone as the OSS bucket, you can additionally specify a intranet endpoint, Cloudreve will try to use this endpoint on server side to reduce traffic cost. Do you want to use the OSS intranet endpoint?", + "ossLANEndpointDes": "Leave blank means not use it. If your Cloudreve is deployed in Alibaba Cloud compute related services which are under the same availability zone as the OSS bucket, you can additionally specify a intranet endpoint, Cloudreve will try to use this endpoint on server side to reduce traffic cost.", "intranetEndPoint": "Intranet endpoint", "ossCDNDes": "Do you want to use Alibaba Cloud CDN to speed up file access?", "createOSSCDNDes": "Go to <0>Alibaba Cloud CDN Dashboard to create a CDN domain, the source of the CDN should be your OSS bucket. Enter the CDN domain and select if you want to use HTTPS:", - "ossAKDes": "Obtain your AccessKey in <0>Security Information Management page, fill in the AccessKey below:", + "ossAKDes": "Obtain your AccessKey in <0>Security Information Management page. You can also create an AccessKey with <1>AliyunOSSFullAccess permission in <2>RAM Access Control.", "shouldNotContainSpace": "This cannot contain spaces.", "nameThePolicyFirst": "Name the storage policy:", "chunkSizeLabelOSS": "Specify the chunk size for resumable uploads. Allowed range is 100 KB - 5 GB.", @@ -463,308 +875,301 @@ "skip": "Skip", "editUpyunStoragePolicy": "Edit Upyun storage policy", "addUpyunStoragePolicy": "Add Upyun storage policy", - "createUpyunBucketDes": "Go to <0>Upyun Dashboard to create a storage service.", - "storageServiceNameDes": "Enter the name of your storage service:", + "createUpyunBucketDes": "Fill in the name of the storage service you created in <0>Upyun Dashboard.", "storageServiceName": "Service name", - "operatorNameDes": "Create an operators for this service, authorize the operators with read, write, and delete permissions, fill in the operator information below.", "operatorName": "Operator name", "operatorPassword": "Operator password", - "upyunCDNDes": "Fill in the domain bound for the storage service, and choose whether to use HTTPS.", - "upyunOptionalDes": "You can skip this step and keep it as default, but we strongly suggest you follow below instructions.", - "upyunTokenDes": "Go to the Feature Configuration panel of the created storage service, go to the Access Configuration tab, enable Token Anti-Hotlinking and set a secret.", + "tokenStatus": "Token anti-hotlinking", + "upyunTokenDes": "It is strongly recommended to enable Token Anti-Hotlinking, go to the <0>Feature Configuration panel of the created storage service, go to the <1>Access Control tab, enable Token Anti-Hotlinking and set a secret.", "tokenEnabled": "Enable Token Anti-Hotlinking", "tokenDisabled": "Not use Token Anti-Hotlinking", "upyunTokenSecretDes": "Enter the secret of the Token Anti-Hotlinking.", "upyunTokenSecret": "Token Anti-Hotlinking secret", - "cannotEnableForTokenProtectedBucket": "This feature is not supported if you enable Token Anti-Hotlinking.", - "callbackFunctionStep": "Serverless callback", - "callbackFunctionAdded": "Callback function is added.", "editCOSStoragePolicy": "Edit COS storage policy", "addCOSStoragePolicy": "Add COS storage policy", - "createCOSBucketDes": "Go to <0>COS Dashboard to create a storage bucket.", - "cosBucketNameDes": "Go to basic setting page of created bucket, enter <0>Bucket name below:", - "cosBucketFormatError": "Bucket name format is not correct, an example: ccc-1252109809", - "cosBucketTypeDes": "Below you can select the type of access control for the bucket you created. We recommend selecting <0>Private Read/Write for higher security, private bucket do not have the \"Get Source Link\" feature.", + "createCOSBucketDes": "Go to <0>COS Dashboard to create a storage bucket. Go to the basic configuration page of the created bucket, and copy the <1>Bucket name to above.", + "obsBucketDes": "Go to <0>OBS Dashboard to create a storage bucket. Enter the <1>Bucket name you just created. Storage class only supports <2>Standard or <3>Infrequent Access.", "cosPrivateRW": "Private Read/Write", "cosPublicRW": "Public Read and Private Write", - "cosAccessDomainDes": "Go to the base configuration of the created Bucket and fill in the <1>Access Domain given under the <0>Basic Information section.", + "cosAccessDomainDes": "On the overview page of the created Bucket, fill in the <1>Access Domain given under the <0>Domain Information section. You can also use your CNAME domain or CDN acceleration domain.", + "obsEndpointDes": "On the overview page of the created Bucket, fill in the <1>Endpoint given under the <0>Domain Information section.", "accessDomain": "Access domain", - "cosCDNDes": "Do you want to use Tencent Cloud CDN to speed up file access?", "cosCDNDomainDes": "Go to <0>Tencent Cloud CDN Management Console to create a CDN acceleration domain and set the source site to the COS bucket you just created. Fill in the CDN domain name below and select whether to use HTTPS.", - "cosCredentialDes": "Get a pair of access keys from the Tencent Cloud <0>Access Keys page and fill them in below. Please make sure the pair of keys has access permission to COS and SCF services.", - "secretId": "SecretId", - "secretKey": "SecretKey", - "cosCallbackDes": "COS Storage Bucket Client-side direct transfer requires the use of Tencent Cloud's <0>Cloud Functions product to ensure controlled upload callbacks. This step can be skipped if you intend to use this storage policy on your own, or assign it to a trusted user group. If it is for public use, please make sure to create callback cloud functions.", - "cosCallbackCreate": "Cloudreve can try to automatically create the callback cloud function for you, please select the region of the COS bucket and continue. It may take a few seconds to create, so please be patient. Please make sure your Tencent Cloud account has cloud function service enabled before creating.", - "cosBucketRegion": "Bucket region", - "ap-beijing": "ap-beijing", - "ap-chengdu": "ap-chengdu", - "ap-guangzhou": "ap-guangzhou", - "ap-guangzhou-open": "ap-guangzhou-open", - "ap-hongkong": "ap-hongkong", - "ap-mumbai": "ap-mumbai", - "ap-shanghai": "ap-shanghai", - "na-siliconvalley": "na-siliconvalley", - "na-toronto": "na-toronto", - "applicationRegistration": "Application registration", + "cosCredentialDes": "Fill in the access keys obtained from the <0>Access Keys page of Tencent Cloud. Please make sure the pair of keys has access permission to COS services. You can also create a <2>sub-user with <1>Programmatic Access permission and grant it access to COS service.", + "obsCredentialDes": "Fill in the access keys obtained from the <0>Access Keys page of Huawei Cloud. You can also create a <2>IAM user with <1>Programmatic Access permission and grant it <3>OBS OperateAccess permission.", "grantAccess": "Grant access", - "warning": "Warning", + "grantAccessLater": "After creating the storage policy, you need to sigin in and grant access in the storage policy settings page.", "odHttpsWarning": "You must enable HTTPS to use OneDrive/SharePoint storage policies; after enabled, make sure to change Settings - Basic - Site Information - Site URL.", "editOdStoragePolicy": "Edit OneDrive/SharePoint storage policy", "addOdStoragePolicy": "Add OneDrive/SharePoint storage policy", - "creatAadAppDes": "Go to <0>Azure Active Directory Dashboard (Worldwide cloud) or <1>Azure Active Directory Dashboard (21V Chinese cloud), after logging in, go to the <2>Azure Active Directory admin panel, you can optionally use an account different from the one used to store files to login.", - "createAadAppDes2": "Go to the <0>App Registrations menu on the left and click the <1>New registration button.", - "createAadAppDes3": "Fill out the application registration form. Makse sure <0>Supported account types is selected as <1>\tAccounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox); <2>Redirect URI (optional) is selected as <3>Web and fill in <4>{{url}}; For other fields, just leave it as default.", - "aadAppIDDes": "Once created, go to the <0>Overview page in Application Management, copy the <1>Application (Client) ID and fill in the following fields:", + "creatAadAppDes": "Go to <0>Microsoft Entra ID Dashboard, after logging in, go to the <1>Microsoft Entra ID admin panel, you can optionally use an account different from the one used to store files to login.", + "createAadAppDes2": "Go to the <0>App Registrations menu on the left and click the <1>New registration button. Fill out the application registration form. Make sure <2>Supported account types is selected as <3>Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox); <4>Redirect URI (optional) is selected as <5>Web and fill in <6>{{url}}; For other fields, just leave it as default.", + "entraIdApp": "Entra ID app information", + "aadAppIDDes": "Go to the <0>Overview page in Application Management, the value of <1>Application (Client) ID.", "aadAppID": "Application (Client) ID", - "addAppSecretDes": "Go to the <0>Certificates & secrets menu on the left side, click the <1>New client secret button, and select <3>Never for the <2>Expires. After you are done creating the client secret, fill in the value of the client secret below:", + "addAppSecretDes": "The way to create client secret: Go to the <0>Certificates & secrets menu on the left side, click the <1>New client secret button, and select the longest time for the <2>Expires. You need to create a new client secret after the old one expires, and update the new one in the storage policy settings.", "aadAppSecret": "Client secret", - "aadAccountCloudDes": "Select your Microsoft 365 account type:", + "aadAccountCloud": "Microsoft Graph endpoint", + "aadAccountCloudDes": "Please select the endpoint according to the Microsoft 365 account type you are using.", "multiTenant": "Worldwide public cloud", "gallatin": "21V Chinese cloud", "sharePointDes": "Do you want to store files in SharePoint?", - "saveToSharePoint": "Store files to SharePoint", "saveToOneDrive": "Store files to default OneDrive", "spSiteURL": "SharePoint Site URL", "odReverseProxyURLDes": "Do you want to use custom reverse proxy server for file downloading?", "odReverseProxyURL": "URL of reverse proxy server", - "chunkSizeLabelOd": "Specify the chunk size for resumable uploads. OneDrive requires it must be an integer multiple of 320 KiB (327,680 bytes).", - "limitOdTPSDes": "Do you want to add a limit to the frequency of server-side OneDrive API requests?", + "chunkSizeLabelOd": "Allowed range: 5 MB ~ 5GB, OneDrive requires it must be an integer multiple of 320 KiB (327,680 bytes).", + "limitOdTPSDes": "Limit OneDrive API request frequency", "tps": "TPS limit", - "tpsDes": "Limit this storage policy the maximum number of API requests sent to OneDrive per second. Requests that exceed this frequency will be rate-limited. When multiple Cloudreve nodes transferring files, they each use their own token bucket, so please scale this number down as appropriate in this condition.", + "tpsDes": "Leave blank to indicate no limit. Limit this storage policy the maximum number of API requests sent to OneDrive per second. Requests that exceed this frequency will be rate-limited. When multiple Cloudreve nodes transferring files, they each use their own token bucket, so please scale this number down as appropriate in this condition.", "tpsBurst": "TPS burst", "tpsBurstDes": "When requested is idle, Cloudreve can reserve a specified number of slots for future bursts of traffic.", "odOauthDes": "However, you will need to click the button below and authorize with Microsoft account login to complete the initialization before you can use it. You can re-authorize later on the Storage Policy List page.", - "gotoAuthPage": "Go to authorization page", - "s3SelfHostWarning": "AWS S3 storage policies are currently only safe for your own use, or for trusted user groups.", "editS3StoragePolicy": "Edit AWS S3 storage policy", "addS3StoragePolicy": "Add AWS S3 storage policy", "s3BucketDes": "Go to AWS S3 dashboard to create a bucket, enter the <0>Bucket name you just created:", - "publicAccessDisabled": "Public read disabled", - "publicAccessEnabled": "Public read enabled", - "s3EndpointDes": "(Optional) Specify the EndPoint (geographical node) of the storage bucket in full URL format, e.g. <0>https://bucket.region.example.com. Leaving it blank will use the system-generated default endpoint.", - "selectRegionDes": "Select the region where the storage bucket is located or enter the region code manually.", - "enterAccessCredentials": "Obtain a pair of access credential and fill in below:", - "accessKey": "AccessKey", + "s3EndpointDes": "Specify the EndPoint (geographical node) of the storage bucket in full URL format, e.g. <0>https://bucket.region.example.com.", + "selectRegionDes": "Enter the region code of the storage bucket, e.g. <0>us-east-1. For non-AWS S3 compatible storage providers, please refer to their documentation for how to fill this field.", "chunkSizeLabelS3": "Specify the chunk size for resumable uploads. Allowed range is 5 MB - 5 GB.", - "editPolicy": "Edit storage policy", - "setting":"Setting name", - "value": "Value", - "description": "Description", - "id": "ID", - "policyID": "ID of storage policy.", - "policyType": "Type of storage policy.", - "server": "Server", - "policyEndpoint": "Storage node endpoint.", - "bucketID": "Identifier of storage bucket.", - "yes": "Yes", - "no": "No", - "privateBucketDes": "Whether the storage bucket is private.", - "resourceRootURL": "File resource root URL", - "resourceRootURLDes": "Prefix of URL generated for previewing and downloading.", - "akDes": "AccessKey / RefreshToken", - "maxSizeBytes": "Max single file size (Bytes)", - "maxSizeBytesDes": "Max file size can be uploaded, 0 means no limit.", - "autoRename": "Auto rename", - "autoRenameDes": "Whether to automatically rename files.", - "storagePath": "Storage path", - "storagePathDes": "Physical path of uploaded files.", - "fileName": "File name", - "fileNameDes": "Physical name of uploaded files.", - "allowGetSourceLink": "Allow getting source link", - "allowGetSourceLinkDes": "Whether to allow getting source links. Note that some storage policy types are not supported, and even if they are turned on here, the obtained source links will be invalid.", - "upyunToken": "Upyun anti-hotlinking token", - "upyunOnly": "Available only for Upyun policy.", - "allowedFileExtension": "Allowed file extensions", - "emptyIsNoLimit": "Blank means not limit.", - "allowedMimetype": "Allowed MimeType", - "qiniuOnly": "Available only for Qiniu policy.", - "odRedirectURL": "OneDrive redirect URL", - "noModificationNeeded": "Generally no modification is needed.", - "odReverseProxy": "OneDrive reverse proxy server", - "odOnly": "Available only for OneDrive policy.", - "odDriverID": "OneDrive/SharePoint driver ID", - "odDriverIDDes": "Available only for OneDrive policy, blank means use default OneDrive driver.", - "s3Region": "Amazon S3 Region", - "s3Only": "Available only for AWS S3 policy.", - "lanEndpoint": "Intranet EndPoint.", - "ossOnly": "Available only for OSS policy.", - "chunkSizeBytes": "Chunk size (Bytes)", - "chunkSizeBytesDes": "Size of chunk for resumable uploads. Only supported in partial storage policy.", - "placeHolderWithSize": "Use placeholder before uploading", - "placeHolderWithSizeDes": "Whether to create a placeholder file before uploading .Only supported in partial storage policy.", - "saveChanges": "Save changes", - "s3EndpointPathStyle": "Select the format of the S3 Endpoint address, or if you don't know what to select, just leave to the default. Some third-party S3-compatible storage policies may require this option to work. When turned on, we will force to use of path-like format addresses, such as <0>http://s3.amazonaws.com/BUCKET/KEY.", + "policyEndpoint": "Endpoint.", + "s3Region": "Region", + "s3EndpointPathStyle": "Select the format of the S3 Endpoint address. Some third-party S3-compatible storage policies may require this option to work. When turned on, we will force to use of path-like format addresses, such as <0>http://s3.amazonaws.com/BUCKET/KEY.", "usePathEndpoint": "Force path style", - "useHostnameEndpoint": "Use host name if possible", "thumbExt": "Extensions that supports thumbnails", - "thumbExtDes": "Leave blank to indicate that the storage policy predefined set is used. Not valid for local, S3 storage policies." + "thumbExtDes": "Leave blank to indicate that the storage policy predefined set is used. Not valid for local, S3 storage policies.", + "driverRoot": "Driver Root", + "driverRootDes": "Choose where to save files in your OneDrive account. Changing this option will make existing files in the storage policy inaccessible.", + "saveToDefaultOneDrive": "Save files to default OneDrive driver", + "saveToSharePoint": "Save files to SharePoint", + "sharePointUrlDes": "Enter the SharePoint site URL. After losing focus, the system will automatically convert it to the correct driver identifier." }, "node": { - "#": "#", - "name": "Name", - "status": "Status", - "features": "Enabled features", - "action": "Actions", - "remoteDownload": "Remote download", - "nodeDisabled": "Node is disabled.", - "nodeEnabled": "Node is enabled.", - "nodeDeleted": "Node is deleted.", - "disabled": "Disabled", - "online": "Online", - "offline": "Offline", - "addNewNode": "New node", - "refresh": "Refresh", - "enableNode": "Enable node", - "disableNode": "Disable node", - "edit": "Edit", - "delete": "Delete", - "slaveNodeDes": "You can add a server that is also running Cloudreve as a slave node. A slave node can share the load of certain asynchronous tasks (such as remote downloads) for the master. Please refer to the following wizard to deploy and configure a slave node. <0> If you have already deployed a remote node storage policy on the target server, you can skip some steps on this page and just fill in the slave secret and server address here, keep them as the same in the remote storage policy. In subsequent releases, the configuration related to the remote storage policy will be merged into here.", - "overwriteDes": "; The following settings are optional and correspond to the relevant parameters of the master node, <0>; which can be applied to the slave node via the configuration file, please adjust them according to <0>; the actual situation. Changing the following settings requires a restart of the slave node to take effect.", - "workerNumDes": "Maximum number of tasks to be executed in parallel in the task queue.", - "parallelTransferDes": "Maximum number of parallel goroutine when transferring files in the task queue", - "chunkRetriesDes": "Maximum number of retries after a failed upload of a chunk.", - "multipleMasterDes": "A slave Cloudreve instance can interface to multiple Cloudreve master nodes; simply add this slave node to all master nodes and keep the secret consistent.", - "ariaSuccess": "Successfully connected, Aria2 version: {{version}}", "slave": "slave", "master": "master", - "aria2Des": "Cloudreve's remote download functionality is powered by <0>Aria2. To use it, start Aria2 as the same user running Cloudreve on the target node server, and enable the RPC service in the Aria2 config file, <1>Aria2 needs to share the same file system as the {{mode}} Cloudreve process. For more information and guidelines, refer the <2>Offline Downloads section of the documentation.", - "slaveTakeOverRemoteDownload": "Do you need this node to take over remote download tasks?", - "masterTakeOverRemoteDownload": "Do you need the master to take over the remote download task?", - "routeTaskSlave": "After enabled, users' remote download requests can be scheduled to this node for processing.", - "routeTaskMaster": "After enabled, users' remote download requests can be scheduled to master node for processing.", - "enable": "Enable", - "disable": "Disable", - "slaveNodeTarget": "on slave node which shares the same file system with slave Cloudreve", - "masterNodeTarget": "which shares the same file system with Cloudreve", - "aria2ConfigDes": "Boot Aria2 {{target}} which shares the same file system with Cloudreve. When you start Aria2, you need to enable the RPC service in its config file and set the RPC Secret for future use. The following is a config example for reference.", - "enableRPCComment": "Enable RPC service", - "rpcPortComment": "RPC port to listen on", - "rpcSecretComment": "RPC secret, you can change it on your own.", - "rpcConfigDes": "It is recommended to start Aria2 before the node Cloudreve in the routine startup process, so that node Cloudreve can subscribe to event notifications to Aria2 and download status changes are handled in a more timely manner. Of course, if this process is not available, node Cloudreve will also track the task status via polling.", - "rpcServerDes": "Fill in the address of the RPC service that {{mode}} Cloudreve uses to communicate with Aria2. This can be filled in as <0>http://127.0.0.1:6800/, where the port number <1>6800 is consistent with <2>rpc-listen-port in the config file above.", - "rpcServer": "RPC Server", - "rpcServerHelpDes": "RPC server address contain full port number, e.g. http://127.0.0.1:6800/, Leave blank to indicate that the Aria2 service is not enabled.", - "rpcTokenDes": "RPC secret, consistent with <0>rpc-secret in the Aria2 configuration file; leave blank if not set.", - "aria2PathDes": "Fill in the <0>absolute path on the node that Aria2 uses as a temporary download directory. The Cloudreve process on the node needs read, write, and execute permissions on this directory.", - "aria2SettingDes": "Fill in some additional Aria2 parameter information below, as your need.", - "refreshInterval": "Status refresh interval (seconds)", - "refreshIntervalDes": "The interval at which Cloudreve requests a refresh of the task state from Aria2.", - "rpcTimeout": "RPC timeouts (seconds)", - "rpcTimeoutDes": "Maximum wait time when calling RPC services.", - "globalOptions": "Global job options", - "globalOptionsDes": "Additional settings carried when creating a download job, written in JSON encoded format, you can also write these settings in the Aria2 config file, see the Aria2 official documentation for available settings.", - "testAria2Des": "Once you have completed these steps, you can click the Test button below to test that if {{mode}} Cloudreve is communicating properly to Aria2.", - "testAria2DesSlaveAddition": "Please make sure you have performed and passed the \"Slave Communication Test\" on the previous page before performing the test.", - "testAria2": "Testing Aria2 Communication", - "aria2DocURL": "https://docs.cloudreve.org/v/en/use/aria2", - "nameNode": "Enter the name of this node:", - "loadBalancerRankDes": "Specify a load balancing weight for this node, the value is an integer. Some load balancing policies will weight the nodes based on this value.", + "noCapabilities": "No capabilities enabled.", + "active": "Active", + "suspended": "Suspended", + "deleteNodeConfirmation": "Are you sure you want to delete node {{name}}?", + "editNode": "Edit node {{node}}", + "thisIsMasterNodes": "You are editing a master node, which is serving the current site.", + "enableNode": "Enable node", + "enableNodeDes": "After enabled, the node will accept and process the features that have been enabled.", + "name": "Name", + "nameNode": "Node name, also used to display to users.", + "type": "Type", + "server": "Node endpoint", + "serverDes": "Endpoint used for node communication. If you want to store files on this node, this address will also be exposed to the user side for file uploads.", + "loadBalancerRankDes": "Specify a load balancing weight for this node, the value is an integer, the higher the value, the higher the probability of being selected.", "loadBalancerRank": "Load balancing weight", - "nodeSaved": "Node saved successfully.", - "nodeSavedFutureAction": "If you add a new node, you will also need to manually enable the node in the node list for it to work properly.", - "backToNodeList": "Back to node list", - "communication": "Communication", - "otherSettings": "Other settings", - "finish": "Finish", - "nodeAdded": "Node added successfully.", - "nodeSavedNow": "Node saved successfully.", - "editNode": "Edit node", - "addNode": "Add node" + "slaveSecret": "Slave secret", + "slaveSecretDes": "Secret used for slave node communication with master node. It needs to be consistent with <1>Secret in the <1>Slave section of the slave node configuration file.", + "testNode": "Test node communication", + "testNodeSuccess": "Node comminucate successfully.", + "createArchiveDes": "Accept create archive task requests.", + "extractArchiveDes": "Accept extract archive task requests.", + "remoteDownloadDes": "Accept remote download task requests. After enabled, you also need to configure the remote download related information below.", + "downloader": "Downloader", + "aria2Des": "Start Aria2 as the same user/access level running Cloudreve on the target node server, enable the RPC service in the Aria2 config file, for more information and guidelines, refer the \"Remote download\" section of the documentation.", + "qbittorrentDes": "Start qBittorrent as the same user running Cloudreve on the target node server, enable the Web UI service in the qBittorrent settings, for more information and guidelines, refer the \"Remote download\" section of the documentation.", + "rpcServer": "RPC Server", + "rpcServerHelpDes": "RPC server address contain full port number, e.g. <0>http://127.0.0.1:6800/.", + "rpcToken": "RPC Token", + "rpcTokenDes": "Consistent with <0>rpc-secret in the Aria2 configuration file; leave blank if not set.", + "downloaderOptionDes": "Additional downloader configuration when creating a download task, written in JSON key-value format, see the <0>downloader official documentation for available parameters.", + "refreshInterval": "Status refresh interval (seconds)", + "refreshIntervalDes": "The interval at which Cloudreve requests a refresh of the task state from the downloader. The actual refresh interval also depends on the configuration of the \"Remote download\" queue and the busyness of the downloader.", + "waitForSeeding": "Wait for seeding", + "waitForSeedingDes": "After enabled, when the remote download task is completed, the node will keep the task in the seeding state until the seeding completion condition in the downloader configuration is met. This feature only takes effect after the remote download task is completed, and will not affect the user's use of the downloaded files.", + "webUIEndpoint": "Web UI endpoint", + "webUIEndpointDes": "The endpoint of the qBittorrent Web UI, e.g. <0>http://127.0.0.1:8080/.", + "tempPath": "Temporary download directory", + "tempPathDes": "The directory on the node that Aria2 uses as a temporary download directory. The Cloudreve process on the node needs read, write, and execute permissions on this directory, and the downloader also needs to be able to access this directory. Leave blank to use the default temporary file path.", + "webUIUsername": "Web UI username", + "webUIPassword": "Web UI password", + "webUICredDes": "Leave blank if authentication is not enabled.", + "downloaderTestPass": "Successfully connected to downloader, version: {{version}}", + "testDownloader": "Test downloader communication", + "addNewNode": "New node", + "nameTheNode": "Name the node:", + "copyBinary": "", + "runCrSlave": "Run Cloudreve on the node with the same version as the master, and start it with the following configuration file:", + "keepIfUpload": "If you need to use this node for storage policies in the future, please keep the following CORS configuration.", + "storeFiles": "Store files", + "storeFilesDes": "Use this node to store user files.", + "storeFilesHint": "If you want to use this node for storage policies, please create a slave storage policy and select this node.", + "runCrWithConfig": "Save the above file as <0>config.ini file, and start Cloudreve with this file: <0>./cloudreve -c config.ini. A slave Cloudreve instance can serve multiple Cloudreve master nodes; simply add this slave node to all master nodes and keep the secret same.", + "inputServer": "Enter the node endpoint:", + "testButton": "You can click the button below to test if the communication is successful.", + "hostHeaderHint": "If there is a signature error, please check if the reverse proxy in front of the node is passing the <0>Host header.", + "features": "Enabled features", + "remoteDownload": "Remote download", + "refresh": "Refresh" }, "group": { + "countUser": "Count", + "anonymous": "Anonymous user group", + "sysGroup": "System user group", + "adminGroup": "Admin user group", "#": "#", "name": "Name", "type": "Storage policy", "count": "Child users", "size": "Storage quota", - "action": "Actions", - "deleted": "Group deleted.", - "new": "New group", - "aria2FormatError": "Aria2 options format error.", - "atLeastOnePolicy": "At least one storage policy is required.", - "added": "Group added successfully.", - "saved": "Group saved successfully.", - "editGroup": "Edit {{group}}", "nameOfGroup": "Name", - "nameOfGroupDes": "Name of the group.", - "storagePolicy": "Storage policy", - "storageDes": "Select the storage policy that this group use.", + "nameOfGroupDes": "Name of the group, used to display to users.", + "availablePolicies": "Available storage policies", + "availablePoliciesDes": "Select the storage policies that this group can use. Modifying this setting will not affect the files uploaded by users.", + "availablePolicyDesPro": "Multi-selectable, users can freely switch storage policies within the selected range.", "initialStorageQuota": "Initial storage quota", "initialStorageQuotaDes": "Max storage can used by single user under this group.", - "downloadSpeedLimit": "Max download speed", - "downloadSpeedLimitDes": "Fill in 0 to indicate no limit. When the restriction is turned on, the maximum download speed will be limited when users under this user group download all files under the storage policy that supports the speed limit.", - "bathSourceLinkLimit": "Max size of batch source links", - "bathSourceLinkLimitDes": "For the files under the supported storage policy, the maximum number of files allowed for users to obtain source links in a single batch, fill in 0 means no batch generation of source links is allowed.", - "allowCreateShareLink": "Share files", + "isAdmin": "Admin group", + "isAdminDes": "When enabled, users under this group will have admin permissions.", + "share": "Share", + "allowCreateShareLink": "Create share link", "allowCreateShareLinkDes": "If disabled, users cannot create sharing links.", - "allowDownloadShare": "Download shared files", - "allowDownloadShareDes": "If disabled, user cannot download shared files.", + "shareFree": "Free share link", + "shareFreeDes": "When enabled, users can access all paid sharing links without purchasing.", + "fileManagement": "File management", "allowWabDAV": "WebDAV", "allowWabDAVDes": "If disabled, users cannot connect to the storage via the WebDAV protocol", "allowWabDAVProxy": "WebDAV Proxy", - "allowWabDAVProxyDes": "If enabled, users can configure the WebDAV to proxy traffic when downloading files", - "disableMultipleDownload": "Disable multiple download requests", - "disableMultipleDownloadDes": "Valid only for local storage policies. When disabled, users cannot use the multi-threaded download tool.", - "allowRemoteDownload": "Remote download", - "allowRemoteDownloadDes": "Whether to allow users to create remote download tasks", - "aria2Options": "Aria2 job options", - "aria2OptionsDes": "The additional parameters that this user group carries when creating remote download tasks. Options are written in JSON encoded format, you can also write these settings in the Aria2 configuration file, see the official documentation for available parameters.", - "aria2BatchSize": "Max size of batch Aria2 tasks", - "aria2BatchSizeDes": "The number of simultaneous remote download tasks allowed for the user, fill in 0 or leave blank to indicate no limit.", - "serverSideBatchDownload": "Serverside batch download", - "serverSideBatchDownloadDes": "Whether to allow users to select multiple files to use the server-side relay batch download, after disabled, users can still use the pure browser based batch download feature.", + "allowWabDAVProxyDes": "If enabled, users can configure the WebDAV to be proxyed by Cloudreve when downloading files.", + "allowCompressTask": "Compression/Decompression tasks", + "allowCompressTaskDes": "If enabled, users can create compression/decompression tasks.", "compressTask": "Compression/Decompression tasks", - "compressTaskDes": "Whether allow the user to create the compression/decompression task", + "compressTaskDes": "If enabled, users can do compression/decompression for files online.", "compressSize": "Maximum file size to be compressed", - "compressSizeDes": "The maximum total file size of compression jobs that can be created by the user, fill in 0 to indicate no limit.", + "compressSizeDes": "The maximum total file size of compression jobs that can be created by the user, fill in 0 to indicate no limit. This limit is not checked when creating compression tasks, and if the total size of the original files exceeds this limit when executing, the task will fail.", "decompressSize": "Maximum file size to be decompressed", "decompressSizeDes": "The maximum total file size of decompression jobs that can be created by the user, fill in 0 to indicate no limit.", - "redirectedSource": "Use redirected source link", - "redirectedSourceDes": "When enabled, the source link to the file obtained by the user will be redirected by Cloudreve with a shorter link. When disabled, the source link to the file obtained by the user becomes the original URL to the file. Some policies produce non-redirected source links that do not remain persistent, see <0>Comparing Storage Policies.", - "advanceDelete": "Allow advanced file deletion options", - "advanceDeleteDes": "Once enabled, users can choose whether to force deletion and whether to unlink only physical links when deleting files. These options are similar to the administration dashboard when deleting files." + "allowRemoteDownload": "Remote download", + "allowRemoteDownloadDes": "Whether to allow users to create remote download tasks. If you need to use remote download, you also need to have nodes with remote download enabled in the <0>Node List.", + "aria2Options": "Downloader job options", + "aria2OptionsDes": "Extra parameters for downloaders (qBittorrent or Aria2), written in JSON key-value format, see the downloader official documentation for available parameters.", + "aria2BatchSize": "Max batch size of remote download tasks", + "aria2BatchSizeDes": "Max number for submiting batched remote download tasks, fill in 0 to indicate no limit.", + "migratePolicy": "Relocate storage policy", + "migratePolicyDes": "Whether the user creates a storage policy relocation task.", + "advanceDelete": "Advanced file deletion options", + "advanceDeleteDes": "Once enabled, users can choose whether to keep physical files when deleting files. Please only enable this option for trusted user groups.", + "allowSelectNode": "Allow select node", + "allowSelectNodeDes": "When enabled, user can select preferred node before creating tasks. When disabled, the node will be load-balanced by system within the allowed nodes for the group.", + "allowedNodes": "Allowed nodes", + "allowedNodesDes": "Specify the nodes that this group can use to create tasks. Empty list means all nodes are available. Users can only select or be assigned nodes within this list by load balancer. Currently, the tasks covered are: remote download, file compression/decompression. Other tasks will be assigned to the master node.", + "allNodes": "All nodes", + "esclateAnonymity": "Escalate anonymity", + "esclateAnonymityDes": "When enabled, users can assign higher permissions for anonymous users (write/delete/create). When disabled, users can only assign read-only permission for anonymous users. Changing this setting will not affect existing sharing links or files.", + "allowDownloadShare": "Access shared links", + "allowDownloadShareDes": "When disabled, users cannot view others' shared links. This setting takes precedence over the sharing link permission settings.", + "deletedNode": "Deleted node #{{id}}", + "maxWalkedFiles": "Max walked files", + "maxWalkedFilesDes": "In some operations that require deep traversal of files, the maximum number of files allowed to be traversed.", + "trashBinDuration": "Trash bin duration (seconds)", + "trashBinDurationDes": "The retention time of files in the trash bin, files will be permanently deleted after the expiration time. Changing this setting will not affect files already in the trash bin.", + "serverSideBatchDownload": "Serverside batch download", + "serverSideBatchDownloadDes": "Whether to allow users to select multiple files to use the server-side relay batch download, after disabled, users can still use the pure browser based batch download feature.", + "uploadDownload": "Upload and download", + "getDirectLink": "Get direct link", + "getDirectLinkDes": "Whether to allow users to get the direct link of the file.", + "bathSourceLinkLimit": "Max size of batch direct links", + "bathSourceLinkLimitDes": "The maximum number of files allowed for users to obtain direct links in a single batch, fill in 0 means no batch generation of direct links is allowed.", + "redirectedSource": "Use redirected direct link", + "redirectedSourceDes": "Recommended to enable. commended to enable. When enabled, the direct link to the file obtained by the user will be redirected by Cloudreve with a shorter link. When disabled, the direct link to the file obtained by the user becomes the ori, and is bound to the file versionginal URL to the file, and is bound to the file version. Some policies produce non-redirected direct links that do not remain persistent, see Cloudreve documents for detials.", + "downloadSpeedLimit": "Max download speed", + "downloadSpeedLimitDes": "Fill in 0 to indicate no limit. When the restriction is turned on, the maximum download speed will be limited when users download all files under the storage policy that supports the speed limit.", + "anonymousHint": "This user group corresponds to the anonymous visitor who is not signed in.", + "create": "Create", + "copyFromExisting": "Copy from existing group?", + "notCopy": "Not copy", + "confirmDelete": "Are you sure you want to delete group {{group}}?", + "new": "New group", + "editGroup": "Edit {{group}}" }, "user": { + "createdAt": "Created at", + "originUserGroup": "Original user group", + "originUserGroupDes": "User group that the user belongs to before purchasing the current group, the current group will revert to this group after expiration.", + "noOriginUserGroup": "No", + "groupExpired": "Group expired date", + "groupExpiredDes": "ISO8601 format group expired date, leave blank means the group is permanent.", + "openUserFiles": "Open user files", + "id": "ID", + "idValue": "{{id}} ({{hash_id}})", + "avatar": "Profile picture", + "removeAvatar": "Remove profile picture", + "userDialogTitle": "User details", + "2FAEnabled": "2FA enabled", + "qqEnabled": "QQ enabled", + "logtoEnabled": "Logto enabled", "deleted": "User deleted.", "new": "New user", "filter": "Filter", + "emptyNoFilter": "Leave blank means no filter.", "selectedObjects": "{{num}} objects selected.", - "nick": "Nickname", + "nick": "Display name", "email": "Email", "group": "Group", "status": "Status", "usedStorage": "Used storage", - "active": "Active", - "notActivated": "Inactive", - "banned": "Blocked", - "bannedBySys": "Banned by system", + "status_active": "Active", + "status_inactive": "Inactive", + "status_manual_banned": "Manual blocked", + "status_sys_banned": "System blocked", "toggleBan": "Block/Unblock", "filterCondition": "Filter conditions", "all": "All", "userStatus": "User status", - "searchNickUserName": "Search nickname / username", "apply": "Apply", - "added": "User added.", - "saved": "User saved.", "editUser": "Edit {{nick}}", "password": "Password", "passwordDes": "Leave blank means no modification.", "groupDes": "Group that the user belongs to.", - "2FASecret": "2FA Secret", - "2FASecretDes": "Secret of the 2FA authenticator, leave blank to disable 2FA." + "2FA": "2FA", + "notEnabled": "Not enabled", + "reset2Fa": "Disable", + "reset": "Reset", + "confirmDelete": "Are you sure you want to delete user {{user}}?", + "deleteXUsers": "Delete {{num}} users", + "confirmBatchDelete": "Are you sure you want to delete {{num}} users?", + "calibrateStorage": "Calibrate storage", + "calibrateStorageSuccess": "Storage calibrated successfully." }, "file": { + "deleteXFiles": "Delete {{num}} files", + "confirmBatchDelete": "Are you sure you want to delete {{num}} files?", + "confirmDelete": "Are you sure you want to delete file {{file}}?", + "haveShares": "Shared", + "haveDirectLinks": "Have redirected direct links", + "directLinkId": "Link identifier", + "directLinks": "Redirected direct links", + "noRecords": "No records", + "speed": "Speed limit", + "downloads": "Downloads", + "shareLink": "Share links", + "shareLinkNum": "{{num}} (<0>View)", + "blobType": "Type", + "noEntities": "No Blobs", + "blobs": "Blobs", + "creator": "Creator", + "source": "Source", + "key": "Key", + "value": "Value", + "isPublic": "Public", + "noMetadata": "No metadata", + "metadata": "Metadata", + "id": "ID", + "primaryStoragePolicy": "Primary storage policy", + "fileDialogTitle": "File details", "name": "File name", "deleteAsync": "Delete task will be executed in background.", - "import": "Import external files", "forceDelete": "Force delete", "size": "Size", - "uploader": "Uploader", + "sizeUsed": "Used storage", + "uploader": "Owner", "createdAt": "Created at", "uploading": "Uploading", "unknownUploader": "Unknown", - "uploaderID": "Uploader ID", + "uploaderID": "Owner ID", "searchFileName": "Search file name", "storagePolicy": "Storage policy", "selectTargetUser": "Select target user", @@ -786,27 +1191,95 @@ "createImportTask": "Create import task", "unlink": "Unlink (Keep physical file)" }, + "entity": { + "refenenceCount": "Reference count", + "waitForRecycle": "Waiting for recycle", + "entityDialogTitle": "Blob details", + "uploadSessionID": "Upload session ID", + "referredFiles": "Referred files", + "confirmBatchDelete": "Are you sure you want to delete {{num}} Blobs?", + "deleteXEntities": "Delete {{num}} Blobs", + "forceDelete": "Force delete", + "forceDeleteDes": "Whether to delete the Blob record regardless of whether the physical file is deleted." + }, + "event": { + "initiator": "Initiator", + "event": "Event", + "userID": "User ID", + "ip": "IP", + "type": "Type", + "correlationId": "Correlation ID", + "fileID": "File ID", + "emailSend": "Send email \"{{title}}\" to {{email}}", + "emailFailed": "Email queue failed to start", + "signinFailed": "Sign in failed: {{reason}}", + "createDavAccount": "Create WebDAV account: {{account}}", + "updateDavAccount": "Update WebDAV account: {{account}}", + "deleteDavAccount": "Delete WebDAV account: {{account}}", + "pointsChange": "Points change: {{points}}", + "storageAdded": "Purchased {{size}} storage", + "nickChange": "Display name changed from {{old}} to {{new}}", + "eventDialogTitle": "Event details", + "userAgent": "User agent", + "linkedUser": "Linked user", + "datetime": "Time", + "linkedFile": "Linked file", + "linkedEntity": "Linked Blob", + "linkedShare": "Linked share", + "rawContent": "Raw content", + "confirmDelete": "Are you sure you want to delete this event?", + "deleteXEvents": "Delete {{num}} events", + "confirmBatchDelete": "Are you sure you want to delete {{num}} events?" + }, "share": { - "deleted": "Share deleted.", - "objectName": "Object name", + "confirmBatchDelete": "Are you sure you want to delete {{num}} shares?", + "confirmDelete": "Are you sure you want to delete this share?", + "deleteXShares": "Delete {{num}} shares", + "shareDialogTitle": "Share details", + "shareLink": "Share link", + "deleted": "File deleted", + "srcFileName": "Source file", "views": "Views", "downloads": "Downloads", "price": "Price", "autoExpire": "Auto expire", "owner": "Owner", "createdAt": "Created at", - "public": "Public", - "private": "Private", - "afterNDownloads":"After {{num}} download(s).", + "private": "Hide from profile page", + "yes": "Yes", + "no": "No", + "afterNDownloads": "After {{num}} download(s).", "none": "None", "srcType": "Source object type", "folder": "Folder", "file": "File" }, "task": { - "taskDeleted": "Task deleted.", - "howToConfigAria2": "How to configure remote download?", - "srcURL": "Source URL", + "confirmDelete": "Are you sure you want to delete this task?", + "confirmBatchDelete": "Are you sure you want to delete {{num}} tasks?", + "deleteXTasks": "Delete {{num}} tasks", + "blobID": "Blob ID", + "retryIndex": "Retry index", + "entityError": "Blobs that failed to recycle", + "updatedAt": "Updated at", + "taskDialogTitle": "Task details", + "explicitEntityRecycle": "Explicitly recycle files Blobs: {{blobs}}", + "entityRecycleRoutine": "Scan and recycle files Blob", + "mediaMetadata": "Extract media meta of Blob <0>#{{entityID}}", + "uploadSentinelCheck": "Check status of upload session {{uploadSessionID}}", + "remoteDownload": "Remote download: ", + "owner": "Owner", + "content": "Content", + "status": "Status", + "create_archive": "Create archive", + "extract_archive": "Extract archive", + "relocate": "Relocate", + "remote_download": "Remote download", + "media_meta": "Media metadata", + "entity_recycle_routine": "Entity recycle routine", + "explicit_entity_recycle": "Explicit entity recycle", + "upload_sentinel_check": "Upload sentinel check", + "type": "Type", "node": "Distributed node", "createdBy": "Created by", "ready": "Ready", @@ -817,11 +1290,165 @@ "finished": "Finished", "canceled": "Canceled/Stopped", "unknown": "Unknown", - "aria2Des": "Cloudreve's remote download support a master-slave decentralized mode. You can configure multiple Cloudreve slave nodes that can be used to handle remote download tasks, spreading the pressure on the master node. Of course, you can also configure to handle remote downloads only on the master node, which is the easiest way.", - "masterAria2Des": "If you only need to enable remote downloads on the master node, <0>click here to edit the master node.", - "slaveAria2Des": "If you want to distribute remote download tasks on slave nodes, <0>click here to add and configure a new node.", - "editGroupDes": "When you add multiple nodes that can be used for remote downloads, the master node will send remote download requests to these nodes in turn for processing. You may also need to <0>go here to enable remote download permissions for the corresponding groups.", - "lastProgress": "Last progress", "errorMsg": "Error message" + }, + "payment": { + "tradeNo": "Trade No.", + "productType": "Product type", + "providerID": "Provider ID", + "status": "Status", + "deleteXPayments": "Delete {{num}} payments" + }, + "vas": { + "confirmDelete": "Are you sure you want to delete these orders?", + "vas": "VAS", + "reports": "Reports", + "orders": "Payments", + "initialFiles": "Initial files", + "initialFilesDes": "Specify the files that the user initially owns after signups. Enter a file ID to search existing files.", + "filterEmailProvider": "Filter email provider", + "filterEmailProviderDisabled": "Disabled", + "filterEmailProviderWhitelist": "Whitelist", + "filterEmailProviderBlacklist": "Blacklist", + "filterEmailProviderDes": "Restrict the email provider for registration, third-party SSO login is not restricted.", + "filterEmailProviderRule": "Email domain filter rules", + "filterEmailProviderRuleDes": "Separate multiple fields with a semi-colon comma.", + "qqConnect": "QQ Connect", + "qqConnectHint": "When creating the application, please fill in the callback URL: {{url}}", + "enableQQConnect": "Enable QQ Connect", + "enableQQConnectDes": "Whether to allow binding QQ, use QQ to login website.", + "loginWithoutBinding": "Login without registration", + "loginWithoutBindingDes": "After enabled, if a user sign-in from the 3rd-party but does not have a linked account, the system will create an account for them. Users sign-in this way will only be able to sign in using this 3rd-party in the future.", + "appid": "APP ID", + "appidDes": "The APP ID obtained from the application management page.", + "appKey": "APP KEY", + "appKeyDes": "The APP KEY obtained from the application management page.", + "overuseReminder": "Overuse reminder", + "overuseReminderDes": "Reminder email template sent to users after their capacity exceeds the limit due to expired VAS.", + "vasSetting": "VAS settings", + "storagePack": "Storage packs", + "purchasableGroups": "Memberships", + "giftCodes": "Gift codes", + "enable": "Enable", + "appID": "App- ID", + "appIDDes": "APPID of payment application.", + "rsaPrivate": "RSA application private key", + "rsaPrivateDes": "The RSA2 (SHA256) private key for the payment application, typically generated by you. For details, refer to <0>Generating RSA Keys.", + "alipayPublicKey": "Alipay public key", + "alipayPublicKeyDes": "Provided by Alipay, available in Application Management - Application Information - API Signing Method.", + "wechatPay": "WeChat Pay", + "applicationID": "Application ID", + "applicationIDDes": "Public number or mobile application appid applied by merchants.", + "merchantID": "Merchant number", + "merchantIDDes": "The merchant number generated and issued by WeChat Pay.", + "apiV3Secret": "API v3 secret", + "apiV3SecretDes": "The merchant needs to set the secret in [Merchant Platform] - [API Security] before the request WeChat Pay. The length of the key is 32 bytes.", + "mcCertificateSerial": "Merchant certificate serial number", + "mcCertificateSerialDes": "Navigate to [API Security] - [API Certificate] - [View Certificate] to view the merchant API certificate serial number.", + "mcAPISecret": "Merchant API Secrey", + "mcAPISecretDes": "Content of the secret file apiclient_key.pem.", + "payjs": "PAYJS", + "payjsWarning": "This service is provided by <0>PAYJS, a third-party platform, and any disputes arising from it are not the responsibility of Cloudreve developers.", + "mcNumber": "Merchant number", + "mcNumberDes": "Available in the PAYJS admin panel home page.", + "communicationSecret": "Communication key", + "otherSettings": "Other Settings", + "banBufferPeriod": "Suspend buffer period (seconds)", + "banBufferPeriodDes": "The maximum length of time that a user can maintain the capacity overage status, beyond which the user will be suspend by the system.", + "allowSellShares": "Allow pricing for shares", + "allowSellSharesDes": "Once enabled users can set a credit price for sharing and credit will be deducted for downloading.", + "creditPriceRatio": "Credit arrival rate (%)", + "creditPriceRatioDes": "The rate of credits actually arriving to the sharer for the purchase of a share with a set price for download.", + "creditPrice": "Credit price (penny)", + "creditPriceDes": "Price when recharging credits", + "add": "Add", + "name": "Name", + "price": "Price", + "duration": "Duration", + "size": "Size", + "actions": "Actions", + "orCredits": " Or {{num}} credits", + "highlight": "Highlight", + "yes": "Yes", + "no": "No", + "productName": "Product name", + "qyt": "Qyt.", + "code": "Code", + "status": "Status", + "invalidProduct": "Invalid product", + "used": "Used", + "notUsed": "Not used", + "generatingResult": "Result", + "addStoragePack": "Add storage pack", + "editStoragePack": "Edit storage pack", + "productNameDes": "Product display name", + "packSizeDes": "Size of storage pack", + "durationDay": "Duration (day)", + "durationDayDes": "Valid duration of each storage pack.", + "priceYuan": "Price (Yuan)", + "packPriceDes": "Price of storage pack.", + "priceCredits": "Price (Credits)", + "priceCreditsDes": "The price when using credits to buy, fill in 0 means you can't use credits to buy.", + "editMembership": "Edit membership", + "addMembership": "Add membership", + "group": "Group", + "groupDes": "User groups upgraded after purchase.", + "durationGroupDes": "The validity of the purchase time of the user group unit upgraded after the purchase.", + "groupPriceDes": "Membership price", + "productDescription": "Product description (Once per line)", + "productDescriptionDes": "Description of the product displayed on the purchase page.", + "highlightDes": "After enabled, it will be highlighted on the product selection page.", + "generateGiftCode": "Generate gift codes", + "numberOfCodes": "Number of codes", + "numberOfCodesDes": "Number of gift codes to generate.", + "linkedProduct": "Linked product", + "productQyt": "Product qyt.", + "productQytDes": "For credit products, this is the number of points and other products are multiples of durations.", + "freeDownload": "Download shared files for free", + "freeDownloadDes": "After enabled, user can download paid shares for free.", + "credits": "Credits", + "markSuccessful": "Marked successfully.", + "markAsResolved": "Mark as resolved", + "reportedContent": "Reported content", + "reason": "Reason", + "description": "Description", + "reportTime": "Reported at", + "invalid": "[Invalid]", + "deleteShare": "Delete share link", + "orderDeleted": "Order deleted.", + "orderName": "Name", + "product": "Product", + "orderNumber": "Trade No.", + "amount": "Amount", + "paidBy": "Paid with", + "orderOwner": "Created by", + "unpaid": "Unpaid", + "paid": "Paid", + "shareLink": "Shared link", + "mobileApp": "Mobile application", + "showAppPromotion": "Show promotion page", + "showAppPromotionDes": "After enabled, user can see the guidance page for mobile application in \"Connect & Mount\" page.", + "customPaymentName": "Payment method name", + "customPaymentNameDes": "Name of the payment method used to display to the user.", + "customPaymentSecretDes": "Secret key for signing payment requests.", + "customPaymentEndpoint": "Payment API URL", + "customPaymentEndpointDes": "URL to be requested when creating a payment order.", + "appFeedback": "Feedback URL", + "appForum": "User forum URL", + "appLinkDes": "Will be displayed in mobile client, leave empty to hide menu item, This setting will take effect only if VOL license is valid." + }, + "pro": { + "title": "Pro edition exclusive features", + "description": "The feature you are trying to access is only available in the Cloudreve Pro edition, upgrade to unlock all advanced features.", + "proInclude": "Pro edition includes:", + "shareLinkCollabration": "Collaboration via share links", + "filePermission": "File permission management", + "multipleStoragePolicy": "Multiple storage policies and directory storage policy switching", + "auditAndActivity": "File and system activity logs", + "vasService": "VAS service and credit system", + "sso": "SSO single sign-on", + "more": "......", + "later": "Maybe later", + "learnMore": "Learn more" } -} +} \ No newline at end of file diff --git a/public/locales/en-US/image_editor.json b/public/locales/en-US/image_editor.json new file mode 100644 index 0000000..e96e31c --- /dev/null +++ b/public/locales/en-US/image_editor.json @@ -0,0 +1,113 @@ +{ + "name": "Name", + "save": "Save", + "saveAs": "Save as", + "back": "Back", + "loading": "Loading...", + "resetOperations": "Reset/delete all operations", + "changesLoseWarningHint": "If you press button “reset” your changes will lost. Would you like to continue?", + "discardChangesWarningHint": "If you close modal, your last change will not be saved.", + "cancel": "Cancel", + "apply": "Apply", + "warning": "Warning", + "confirm": "Confirm", + "discardChanges": "Discard changes", + "undoTitle": "Undo last operation", + "redoTitle": "Redo last operation", + "showImageTitle": "Show original image", + "zoomInTitle": "Zoom in", + "zoomOutTitle": "Zoom out", + "toggleZoomMenuTitle": "Toggle zoom menu", + "adjustTab": "Adjust", + "finetuneTab": "Finetune", + "filtersTab": "Filters", + "watermarkTab": "Watermark", + "annotateTabLabel": "Annotate", + "resize": "Resize", + "resizeTab": "Resize", + "imageName": "Image name", + "invalidImageError": "Invalid image provided.", + "uploadImageError": "Error while uploading the image.", + "areNotImages": "are not images", + "isNotImage": "is not image", + "toBeUploaded": "to be uploaded", + "cropTool": "Crop", + "original": "Original", + "custom": "Custom", + "square": "Square", + "landscape": "Landscape", + "portrait": "Portrait", + "ellipse": "Ellipse", + "classicTv": "Classic TV", + "cinemascope": "Cinemascope", + "arrowTool": "Arrow", + "blurTool": "Blur", + "brightnessTool": "Brightness", + "contrastTool": "Contrast", + "ellipseTool": "Ellipse", + "unFlipX": "Un-Flip X", + "flipX": "Flip X", + "unFlipY": "Un-Flip Y", + "flipY": "Flip Y", + "hsvTool": "HSV", + "hue": "Hue", + "brightness": "Brightness", + "saturation": "Saturation", + "value": "Value", + "imageTool": "Image", + "importing": "Importing...", + "addImage": "+ Add image", + "uploadImage": "Upload image", + "fromGallery": "From gallery", + "lineTool": "Line", + "penTool": "Pen", + "polygonTool": "Polygon", + "sides": "Sides", + "rectangleTool": "Rectangle", + "cornerRadius": "Corner Radius", + "resizeWidthTitle": "Width in pixels", + "resizeHeightTitle": "Height in pixels", + "toggleRatioLockTitle": "Toggle ratio lock", + "resetSize": "Reset to original image size", + "rotateTool": "Rotate", + "textTool": "Text", + "textSpacings": "Text spacings", + "textAlignment": "Text alignment", + "fontFamily": "Font family", + "size": "Size", + "letterSpacing": "Letter Spacing", + "lineHeight": "Line height", + "warmthTool": "Warmth", + "addWatermark": "+ Add watermark", + "addTextWatermark": "+ Add text watermark", + "addWatermarkTitle": "Choose the watermark type", + "uploadWatermark": "Upload watermark", + "addWatermarkAsText": "Add as text", + "padding": "Padding", + "paddings": "Paddings", + "shadow": "Shadow", + "horizontal": "Horizontal", + "vertical": "Vertical", + "blur": "Blur", + "opacity": "Opacity", + "transparency": "Transparency", + "position": "Position", + "stroke": "Stroke", + "saveAsModalTitle": "Save as", + "extension": "Extension", + "format": "Format", + "nameIsRequired": "Name is required.", + "quality": "Quality", + "imageDimensionsHoverTitle": "Saved image size (width x height)", + "cropSizeLowerThanResizedWarning": "Note, the selected crop area is lower than the applied resize which might cause quality decrease", + "actualSize": "Actual size (100%)", + "fitSize": "Fit size", + "addImageTitle": "Select image to add...", + "mutualizedFailedToLoadImg": "Failed to load image.", + "tabsMenu": "Menu", + "download": "Download", + "width": "Width", + "height": "Height", + "plus": "+", + "cropItemNoEffect": "No preview available for this crop item" +} \ No newline at end of file diff --git a/public/locales/en-US/markdown_editor.json b/public/locales/en-US/markdown_editor.json new file mode 100644 index 0000000..0f422f4 --- /dev/null +++ b/public/locales/en-US/markdown_editor.json @@ -0,0 +1,105 @@ +{ + "frontmatterEditor": { + "title": "Edit document frontmatter", + "key": "Key", + "value": "Value", + "addEntry": "Add entry" + }, + "dialogControls": { + "save": "Save", + "cancel": "Cancel" + }, + "uploadImage": { + "uploadInstructions": "Upload an image from your device:", + "addViaUrlInstructions": "Or add an image from an URL:", + "autoCompletePlaceholder": "Select or paste an image src", + "alt": "Alt:", + "title": "Title:" + }, + "imageEditor": { + "editImage": "Edit image" + }, + "createLink": { + "url": "URL", + "urlPlaceholder": "Select or paste an URL", + "title": "Title", + "saveTooltip": "Set URL", + "cancelTooltip": "Cancel change" + }, + "linkPreview": { + "open": "Open {{url}} in new window", + "edit": "Edit link URL", + "copyToClipboard": "Copy to clipboard", + "copied": "Copied!", + "remove": "Remove link" + }, + "table": { + "deleteTable": "Delete table", + "columnMenu": "Column menu", + "textAlignment": "Text alignment", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "insertColumnLeft": "Insert a column to the left of this one", + "insertColumnRight": "Insert a column to the right of this one", + "deleteColumn": "Delete this column", + "rowMenu": "Row menu", + "insertRowAbove": "Insert a row above this one", + "insertRowBelow": "Insert a row below this one", + "deleteRow": "Delete this row" + }, + "toolbar": { + "blockTypes": { + "paragraph": "Paragraph", + "quote": "Quote", + "heading": "Heading {{level}}" + }, + "blockTypeSelect": { + "selectBlockTypeTooltip": "Select block type", + "placeholder": "Block type" + }, + "toggleGroup":"toggle group", + "removeBold": "Remove bold", + "bold": "Bold", + "removeItalic": "Remove italic", + "italic": "Italic", + "underline": "Remove underline", + "removeUnderline": "Underline", + "removeInlineCode": "Remove code format", + "inlineCode": "Inline code format", + "link": "Create link", + "richText": "Rich text", + "diffMode": "Diff mode", + "source": "Source mode", + "admonition": "Insert Admonition", + "codeBlock": "Insert Code Block", + "editFrontmatter": "Edit frontmatter", + "insertFrontmatter": "Insert frontmatter", + "image": "Insert image", + "insertSandpack": "Insert Sandpack", + "table": "Insert Table", + "thematicBreak": "Insert thematic break", + "bulletedList": "Bulleted list", + "numberedList": "Numbered list", + "checkList": "Check list", + "deleteSandpack": "Delete this code block", + "undo": "Undo {{shortcut}}", + "redo": "Redo {{shortcut}}" + }, + "admonitions": { + "note": "Note", + "tip": "Tip", + "danger": "Danger", + "info": "Info", + "caution": "Caution", + "changeType": "Select admonition type", + "placeholder": "Admonition type" + }, + "codeBlock": { + "language": "Code block language", + "selectLanguage": "Select code block language" + }, + "contentArea":{ + "editableMarkdown": "editable markdown" + } +} diff --git a/public/locales/it-IT/application.json b/public/locales/it-IT/application.json deleted file mode 100644 index 191345d..0000000 --- a/public/locales/it-IT/application.json +++ /dev/null @@ -1,502 +0,0 @@ -{ - "login": { - "email": "E-mail", - "password": "Password", - "captcha": "CAPTCHA", - "captchaError": "Impossibile caricare il CAPTCHA: {{message}}", - "signIn": "Accedi", - "signUp": "Registrati", - "signUpAccount": "Registrati", - "useFIDO2": "Utilizza l'autenticatore hardware", - "usePassword": "Utilizza Password", - "forgetPassword": "Password dimenticata?", - "2FA": "Verifica 2FA", - "input2FACode": "Inserisci il codice di verifica 2FA a sei cifre", - "passwordNotMatch": "Quelle password non corrispondevano.", - "findMyPassword": "Trova la mia password", - "passwordReset": "La password è stata reimpostata.", - "newPassword": "Nuova password", - "repeatNewPassword": "Ripeti la nuova password", - "repeatPassword": "Ripeti la password", - "resetPassword": "Resetta la mia password", - "backToSingIn": "Torna all'accesso", - "sendMeAnEmail": "Mandami un email", - "resetEmailSent": "È stata inviata un'e-mail, prestare attenzione a controllare.", - "browserNotSupport": "Non supportato dal browser o dall'ambiente corrente.", - "success": "Accesso riuscito", - "signUpSuccess": "Iscrizione riuscita", - "activateSuccess": "Iscrizione completata", - "accountActivated": "Il tuo account è stato attivato con successo.", - "title": "Iscriviti a {{title}}", - "sinUpTitle": "Iscriviti a {{title}}", - "activateTitle": "Attiva il tuo account", - "activateDescription": "Un'e-mail di attivazione è stata inviata al tuo indirizzo e-mail, visita il collegamento nell'e-mail per completare la registrazione.", - "continue": "Avanti", - "logout": "Esci", - "loggedOut": "Ora sei disconnesso.", - "clickToRefresh": "Clicca per aggiornare" - }, - "navbar": { - "myFiles": "Miei Files", - "myShare": "Condiviso", - "remoteDownload": "Download remoto", - "connect": "Connetti", - "taskQueue": "Coda attività", - "setting": "Impostazioni", - "videos": "Video", - "photos": "Foto", - "music": "Musica", - "documents": "Documenti", - "addATag": "Aggiungi un tag...", - "addTagDialog": { - "selectFolder": "Seleziona una Cartella", - "fileSelector": "Selettore File", - "folderLink": "Scorciatoia Cartella", - "tagName": "Nome Tag", - "matchPattern": "Modello(i) di corrispondenza del nome file", - "matchPatternDescription": "Puoi usare <0>* come carattere jolly. Per esempio, <1>*.png restituisce le immagini in formato png. Le regole su più righe funzioneranno con una relazione \"o\" tra loro.", - "icon": "Icona:", - "color": "Colore:", - "folderPath": "Percorso della cartella" - }, - "storage": "Archiviazione", - "storageDetail": "{{used}} di {{total}} utilizzato", - "notLoginIn": "Accesso non eseguito", - "visitor": "Anonimo", - "objectsSelected": "{{num}} oggetti selezionati", - "searchPlaceholder": "Cerca...", - "searchInFiles": "Search <0>{{name}} in my files", - "searchInFolders": "Cerca <0>{{name}} nella cartella corrente", - "searchInShares": "Cerca <0>{{name}} nelle condivisioni di altri utenti", - "backToHomepage": "Torna alla home page", - "toDarkMode": "Passa al tema scuro", - "toLightMode": "Passa al tema chiaro", - "myProfile": "Il mio profilo", - "dashboard": "Cruscotto", - "exceedQuota": "La tua capacità utilizzata ha superato la quota, elimina i file extra." - }, - "fileManager": { - "open": "Apri", - "openParentFolder": "Apri la cartella", - "download": "Scarica", - "batchDownload": "Scarica in batch", - "share": "Condividi", - "rename": "Rinomina", - "move": "Sposta", - "delete": "Elimina", - "moreActions": "Altre azioni...", - "refresh": "Aggiorna", - "compress": "Comprimi", - "newFolder": "Nuova Cartella", - "newFile": "Nuovo file", - "showFullPath": "Mostra percorso completo", - "listView": "Visualizzazione elenco", - "gridViewSmall": "Vista griglia (nessuna anteprima)", - "gridViewLarge": "Vista griglia", - "paginationSize": "Impaginazione", - "paginationOption": "{{option}} / pagina", - "noPagination": "Nessuna impaginazione", - "sortMethod": "Ordina per", - "sortMethods": { - "A-Z": "A-Z", - "Z-A": "Z-A", - "oldestUploaded": "Caricato da tempo", - "newestUploaded": "Ultimo caricato", - "oldestModified": "Il più vecchio modificato", - "newestModified": "Ultima modifica", - "smallest": "Più piccolo", - "largest": "Più grande" - }, - "shareCreateBy": "Creato da {{nick}}", - "name": "Nome", - "size": "Dimensione", - "lastModified": "Ultima modifica", - "currentFolder": "Cartella corrente", - "backToParentFolder": "Torna alla cartella superiore", - "folders": "Cartelle", - "files": "File", - "listError": ":( Impossibile elencare i file", - "dropFileHere": "Trascina e rilascia il file qui", - "orClickUploadButton": "Oppure fai clic sul pulsante \"Carica file\" in basso a destra per aggiungere un file", - "nothingFound": "Non è stato trovato nulla", - "uploadFiles": "Carica file", - "uploadFolder": "Carica cartella", - "newRemoteDownloads": "Nuovo download remoto", - "enter": "Entra", - "getSourceLink": "Ottieni collegamento sorgente", - "getSourceLinkInBatch": "Ottieni collegamenti sorgente", - "createRemoteDownloadForTorrent": "Nuovo download remoto", - "decompress": "Decomprimi", - "createShareLink": "Condividi", - "viewDetails": "Visualizza dettagli", - "copy": "Copia", - "bytes": " ({{bytes}} Bytes)", - "storagePolicy": "Policy archiviazione", - "inheritedFromParent": "Ereditato dal genitore", - "childFolders": "Cartelle secondarie", - "childFiles": "File secondari", - "childCount": "{{num}}", - "parentFolder": "Cartella superiore", - "rootFolder": "Cartella principale", - "modifiedAt": "Modificato alle", - "createdAt": "Creato alle", - "statisticAt": "Statistica su <1>", - "musicPlayer": "Lettore musicale", - "closeAndStop": "Chiudi e ferma", - "playInBackground": "Riproduci in background", - "copyTo": "Copia in", - "copyToDst": "Copia in <0>{{dst}}", - "errorReadFileContent": "Impossibile leggere il contenuto del file: {{msg}}", - "wordWrap": "A capo automatico", - "pdfLoadingError": "Impossibile caricare il PDF: {{msg}}", - "subtitleSwitchTo": "Sottotitolo cambiato in: {{subtitle}}", - "noSubtitleAvailable": "Nessun file di sottotitoli disponibile nella cartella video (supportato: ASS/SRT/VTT)", - "subtitle": "Sottotitoli", - "playlist": "Playlist", - "openInExternalPlayer": "Apri nel lettore esterno", - "searchResult": "Risultati della ricerca", - "preparingBathDownload": "Preparazione download batch...", - "preparingDownload": "Preparazione per il download...", - "browserBatchDownload": "Archiviazione lato browser", - "browserBatchDownloadDescription": "Scaricati e assemblati dal browser in tempo reale, non tutti gli ambienti sono supportati.", - "serverBatchDownload": "Archiviazione lato server dei trasferimenti", - "serverBatchDownloadDescription": "Archiviato dal server e inviato al client per il download immediato.", - "selectArchiveMethod": "Seleziona metodo di archiviazione", - "batchDownloadStarted": "Il download batch è iniziato, per favore non chiudere questa scheda", - "batchDownloadError": "Impossibile scaricare: {{msg}}", - "userDenied": "Utente respinto.", - "directoryDownloadReplace": "Sovrascrivi", - "directoryDownloadReplaceDescription": "{{num}} oggetti inclusi {{duplicates}} verranno sovrascritti.", - "directoryDownloadSkip": "Salta", - "directoryDownloadSkipDescription": "{{num}} oggetti inclusi {{duplicates}} verranno ignorati.", - "selectDirectoryDuplicationMethod": "Come gestire i file duplicati?", - "directoryDownloadStarted": "Download avviato, per favore non chiudere questa scheda.", - "directoryDownloadFinished": "Download terminato, nessun download fallito.", - "directoryDownloadFinishedWithError": "Download terminato, oggetto {{failed}} non riuscito.", - "directoryDownloadPermissionError": "Autorizzazione negata, consenti la lettura e la scrittura dei file locali." - }, - "modals": { - "processing": "In lavorazione...", - "duplicatedObjectName": "Nome dell'oggetto duplicato.", - "duplicatedFolderName": "Nome della cartella duplicato.", - "taskCreated": "Attività creata.", - "taskCreateFailed": "{{failed}} impossibile creare la(e) attività: {{details}}.", - "linkCopied": "Link copiato.", - "getSourceLinkTitle": "Ottieni il collegamento alla fonte", - "sourceLink": "Collegamento alla fonte", - "folderName": "Nome cartella", - "create": "Crea", - "fileName": "Nome file", - "renameDescription": "Inserisci il nuovo nome per <0>{{name}} :", - "newName": "Nuovo nome", - "moveToTitle": "Sposta in", - "moveToDescription": "Sposta in <0>{{name}}", - "saveToTitle": "Salva in", - "saveToTitleDescription": "Salva in <0>{{name}}", - "deleteTitle": "Elimina oggetti", - "deleteOneDescription": "Sei sicuro di eliminare <0>{{name}} ?", - "deleteMultipleDescription": "Sei sicuro di rimuovere questi {{num}} oggetti?", - "newRemoteDownloadTitle": "Nuova attività di download remoto", - "remoteDownloadURL": "Scarica URL destinazione", - "remoteDownloadURLDescription": "Incolla l'URL di download, un URL per riga, supporta il collegamento HTTP(s)/FTP/Magnete", - "remoteDownloadDst": "Scarica in", - "createTask": "Crea attività", - "downloadTo": "Scarica in <0>{{name}}", - "decompressTo": "Decomprimi in", - "decompressToDst": "Decomprimi in <0>{{name}}", - "defaultEncoding": "Predefinito", - "chineseMajorEncoding": "", - "selectEncoding": "Seleziona la codifica per i caratteri non UTF8", - "noEncodingSelected": "Nessun metodo di codifica selezionato", - "listingFiles": "Elenco dei file...", - "listingFileError": "Impossibile elencare i file: {{message}}", - "generatingSourceLinks": "Generazione dei link alla fonte...", - "noFileCanGenerateSourceLink": "Non esiste alcun file che possa essere utilizzato per generare il link all'origine", - "sourceBatchSizeExceeded": "Il gruppo utenti corrente può generare collegamenti alla sorgente per un massimo di {{limit}} files alla volta.", - "zipFileName": "Nome file ZIP", - "shareLinkShareContent": "Ho condiviso con te: {{name}} Link: {{link}}", - "shareLinkPasswordInfo": "Password: {{password}}", - "createShareLink": "Crea link di condivisione", - "usePasswordProtection": "Utilizza la protezione tramite password", - "sharePassword": "Condividi password", - "randomlyGenerate": "Casuale", - "expireAutomatically": "Scadenza automatica", - "downloadLimitOptions": "{{num}} downloads", - "or": "O dopo", - "5minutes": "5 minuti", - "1hour": "1 ora", - "1day": "1 giorno", - "7days": "7 giorni", - "30days": "30 giorni", - "custom": "Personalizza", - "seconds": "secondi", - "downloads": "downloads", - "downloadSuffix": "", - "allowPreview": "Abilita anteprima", - "allowPreviewDescription": "Consentire l'anteprima del contenuto del file dal link di condivisione", - "shareLink": "Link condivisione", - "sendLink": "Invia il link", - "directoryDownloadReplaceNotifiction": "Sovrascrivi {{name}}", - "directoryDownloadSkipNotifiction": "{{name}} Saltato", - "directoryDownloadTitle": "Download", - "directoryDownloadStarted": "Inizia a scaricare {{name}}", - "directoryDownloadFinished": "Download terminato", - "directoryDownloadError": "Errore: {{msg}}", - "directoryDownloadErrorNotification": "Si è verificato un errore durante il download {{name}}: {{msg}}", - "directoryDownloadAutoscroll": "Scorrimento automatico", - "directoryDownloadCancelled": "Download annullato", - "advanceOptions": "Opzioni avanzate", - "forceDelete": "Forza l'eliminazione ", - "forceDeleteDes": "Forza l'eliminazione dei record dei file, indipendentemente dal fatto che il file fisico sia stato eliminato correttamente.", - "unlinkOnly": "Scollega soltanto", - "unlinkOnlyDes": "Elimina solo i record dei file, i file fisici non verranno eliminati." - }, - "uploader": { - "fileNotMatchError": "Il file selezionato non corrisponde al file originale.", - "unknownError": "Si è verificato un errore sconosciuto: {{msg}}", - "taskListEmpty": "Nessuna attività di caricamento.", - "hideTaskList": "Nascondi l'elenco", - "uploadTasks": "Attività di caricamento", - "moreActions": "Altre azioni", - "addNewFiles": "Aggiungi nuovi file", - "toggleTaskList": "Espandi/Comprimi l'elenco", - "pendingInQueue": "In attesa in coda...", - "preparing": "Preparazione...", - "processing": "Elaborazione...", - "progressDescription": "{{uploaded}} caricati, {{total}} totale - {{percentage}}%", - "progressDescriptionFull": "{{uploaded}} caricati, {{total}} totale - {{percentage}}% ({{speed}})", - "progressDescriptionPlaceHolder": " - caricati", - "uploadedTo": "Caricati in ", - "rootFolder": "Cartella principale", - "unknownStatus": "Sconosciuto", - "resumed": "Ripreso", - "resumable": "Ripristinabile", - "retry": "Riprova", - "deleteTask": "Elimina attività", - "cancelAndDelete": "Annulla ed elimina", - "selectAndResume": "Seleziona lo stesso file e riprendi il caricamento", - "fileName": "Nome: ", - "fileSize": "Dimensione: ", - "sessionExpiredIn": "Scade <0>", - "chunkDescription": "({{total}} pacchetti, {{size}} ciascuno)", - "noChunks": "(Nessun pacchetto)", - "destination": "Destinazione: ", - "uploadSession": "Sessione caricamento: ", - "errorDetails": "Dettagli errore: ", - "uploadSessionCleaned": "Tutte le sessioni di caricamento sono state cancellate.", - "hideCompletedTooltip": "Nascondi le attività completate, non riuscite e annullate.", - "hideCompleted": "Nascondi le attività completate", - "addTimeAscTooltip": "Le attività aggiunte per prime vengono classificate per prime.", - "addTimeAsc":"Dal più vecchio al più recente", - "addTimeDescTooltip": "Le velocità di caricamento delle attività vengono visualizzate come velocità istantanea.", - "addTimeDesc": "Dal più recente al più vecchio", - "showInstantSpeedTooltip": "Le velocità di caricamento delle attività vengono visualizzate come velocità istantanea.", - "showInstantSpeed": "Velocità istantanea", - "showAvgSpeedTooltip": "Le velocità di caricamento delle attività vengono visualizzate come velocità medie.", - "showAvgSpeed": "Velocità media", - "cleanAllSessionTooltip": "Cancella tutte le sessioni di caricamento in sospeso sul lato server.", - "cleanAllSession": "Cancella tutte le sessioni di caricamento", - "cleanCompletedTooltip": "Cancella le attività completate, non riuscite e annullate", - "cleanCompleted": "Cancella le attività completate", - "retryFailedTasks": "Ritenta tutte le attività fallite", - "retryFailedTasksTooltip": "Riprova tutte le attività non riuscite nella coda corrente", - "setConcurrentTooltip": "Imposta il numero massimo di attività di caricamento che possono essere svolte simultaneamente.", - "setConcurrent": "Imposta limite attività simultanee", - "sizeExceedLimitError": "La dimensione del file supera i limiti dei criteri di archiviazione. (Maximum: {{max}})", - "suffixNotAllowedError": "La policy di archiviazione non supporta il caricamento di file con questa estensione. (Supportato:{{supported}})", - "createUploadSessionError": "Impossibile creare la sessione di caricamento", - "deleteUploadSessionError": "Impossibile eliminare la sessione di caricamento", - "requestError": "Richiesta non riuscita: {{msg}} ({{url}}).", - "chunkUploadError": "Impossibile caricare il pacchetto [{{index}}].", - "conflictError": "L'attività di caricamento dei file con lo stesso nome è già in fase di elaborazione.", - "chunkUploadErrorWithMsg": "Caricamento del pacchetto non riuscito: {{msg}}", - "chunkUploadErrorWithRetryAfter": "(Riprova tra {{retryAfter}}i)", - "emptyFileError": "Il caricamento di file vuoti su OneDrive non è supportato, crea file vuoti tramite il pulsante Crea file.", - "finishUploadError": "Impossibile completare il caricamento del file.", - "finishUploadErrorWithMsg": "Impossibile completare il caricamento del file: {{msg}}", - "ossFinishUploadError": "Impossibile completare il caricamento del file: {{msg}} ({{code}})", - "cosUploadFailed": "Caricamento fallito: {{msg}} ({{code}})", - "upyunUploadFailed": "Caricamento fallito: {{msg}}", - "parseResponseError": "Impossibile analizzare la risposta: {{msg}} ({{content}})", - "concurrentTaskNumber": "Limite attività corrente", - "dropFileHere": "Rilascia il file da caricare" - }, - "share": { - "expireInXDays": "Scade tra $t(share.days, {\"count\": {{num}} })", - "days":"{{count}} giorno", - "days_other":"{{count}} giorni", - "expireInXHours":"Scade tra $t(share.hours, {\"count\": {{num}} })", - "hours":"un'ora", - "hours_other":"{{count}} ore", - "createdBy": "Creato da <0>{{nick}}", - "sharedBy": "<0>{{nick}} ha condiviso $t(share.files, {\"count\": {{num}} }) con te.", - "files":"1 file", - "files_other":"{{count}} files", - "statistics": "$t(share.views, {\"count\": {{views}} }) • $t(share.downloads, {\"count\": {{downloads}} }) • {{time}}", - "views":"{{count}} visualizzazione", - "views_other":"{{count}} visualizzazioni", - "downloads":"{{count}} download", - "downloads_other":"{{count}} downloads", - "privateShareTitle": "Condivisione privata da {{nick}}", - "enterPassword": "Inserisci la password di condivisione", - "continue": "Continua", - "shareCanceled": "La condivisione è annullata.", - "listLoadingError": "Caricamento fallito.", - "sharedFiles": "File condivisi", - "createdAtDesc": "Data (Decrescente)", - "createdAtAsc": "Data (Crescente)", - "downloadsDesc": "Numero di download (Decrescente)", - "downloadsAsc":"Numero di download (Crescente)", - "viewsDesc":"Numero di visualizzazioni (Decrescente)", - "viewsAsc":"Numero di visualizzazioni (Crescente)", - "noRecords": "Nessun file condiviso.", - "sourceNotFound": "[La fonte non esiste]", - "expired": "Scaduto", - "changeToPublic": "Rendilo pubblico", - "changeToPrivate": "Rendilo privato", - "viewPassword": "Visualizza password", - "disablePreview": "Disabilita anteprima", - "enablePreview": "Abilita anteprima", - "cancelShare": "Annulla condivisione", - "sharePassword": "Condivi la password", - "readmeError": "Impossibile caricare il README: {{msg}}", - "enterKeywords": "Inserisci le parole chiave di ricerca.", - "searchResult": "Risultati di ricerca", - "sharedAt": "Condiviso a <0>", - "pleaseLogin": "Per favore accedi prima.", - "cannotShare": "Impossibile visualizzare l'anteprima di questo file.", - "preview": "Anteprima", - "incorrectPassword": "Password non corretta.", - "shareNotExist": "Collegamento di condivisione non valido o scaduto." - }, - "download": { - "failedToLoad": "Caricamento fallito.", - "active": "Attivo", - "finished": "Finito", - "activeEmpty": "Nessuna attività di download in corso.", - "finishedEmpty": "Nessuna attività di download completata.", - "loadMore": "Caricarne di più", - "taskFileDeleted": "File eliminato.", - "unknownTaskName": "[Sconosciuto]", - "taskCanceled": "Attività di download annullata, lo stato verrà aggiornato in seguito", - "operationSubmitted": "Operazione inviata, lo stato verrà aggiornato in seguito", - "deleteThisFile": "Elimina questo file", - "openDstFolder": "Apri la cartella di destinazione", - "selectDownloadingFile": "Seleziona i file da scaricare", - "cancelTask": "Annulla", - "updatedAt": "Aggiornato alle: ", - "uploaded": "Caricato: ", - "uploadSpeed": "Velocità di caricamento: ", - "InfoHash": "InfoHash: ", - "seederCount": "Seeders:", - "seeding": "Seeding: ", - "downloadNode": "Nodo: ", - "isSeeding": "Sì", - "notSeeding": "No", - "chunkSize": "Dimensione pacchetto:", - "chunkNumbers": "Pacchetto:", - "taskDeleted": "Attività eliminata.", - "transferFailed": "Impossibile trasferire i file.", - "downloadFailed": "Scaricamento fallito: {{msg}}", - "canceledStatus": "Annullato", - "finishedStatus": "Finito", - "pending": "Terminato, trasferimento in attesa in coda", - "transferring": "Finito, trasferimento in corso", - "deleteRecord": "Elimina record", - "createdAt": "Creato alle: " - }, - "setting": { - "avatarUpdated": "L'avatar è stato aggiornato e avrà effetto dopo l'aggiornamento.", - "nickChanged": "Il soprannome è cambiato e avrà effetto dopo l'aggiornamento.", - "settingSaved": "Impostazione salvata.", - "themeColorChanged": "Il colore del tema è cambiato.", - "profile": "Profilo", - "avatar": "Avatar", - "uid": "UID", - "nickname": "Soprannome", - "group": "Gruppo", - "regTime": "Data di iscrizione", - "privacyAndSecurity": "Privacy e sicurezza", - "profilePage": "Profilo pubblico", - "accountPassword": "Password", - "2fa": "Autenticazione 2FA", - "enabled": "Abilitata", - "disabled": "Disabilitata", - "appearance": "Aspetto", - "themeColor": "Colore del tema", - "darkMode": "Modalità scura", - "syncWithSystem": "Sincronizzato con il sistema", - "fileList": "Elenco file", - "timeZone": "Fuso orario", - "webdavServer": "Server", - "userName": "Username", - "manageAccount": "Gestisci gli account", - "uploadImage": "Carica da file", - "useGravatar": "Usa Gravatar", - "changeNick": "Cambia soprannome", - "originalPassword": "Password Originale", - "enable2FA": "Abilita l'autenticazione 2FA", - "disable2FA": "Disabilita l'autenticazione 2FA", - "2faDescription": "Utilizza qualsiasi app mobile 2FA o software di gestione password che supporti 2FA per scansionare il codice QR a sinistra per aggiungere questo sito. Dopo la scansione, inserisci il codice di verifica a 6 cifre fornito dall'app 2FA per abilitare 2FA.", - "inputCurrent2FACode": "Inserisci l'attuale codice di verifica 2FA.", - "timeZoneCode": "Codice fuso orario IANA", - "authenticatorRemoved": "Autenticatore rimosso.", - "authenticatorAdded": "Autenticatore aggiunto.", - "browserNotSupported": "Non supportato dal browser o dall'ambiente corrente.", - "removedAuthenticator": "Rimuovi l'autenticatore", - "removedAuthenticatorConfirm": "Sei sicuro di rimuovere questo autenticatore?", - "addNewAuthenticator": "Aggiungi un autenticatore", - "hardwareAuthenticator": "Autenticatore hardware", - "copied": "Copiato negli appunti.", - "pleaseManuallyCopy": "Il browser corrente non supporta, copia manualmente.", - "webdavAccounts": "Accounts WebDAV", - "webdavHint": "Server WebDAV: {{url}}; Nome utente: {{name}} ; La password è la password dell'account creato di seguito.", - "annotation": "Annotazione", - "rootFolder": "Cartella radice relativa", - "createdAt": "Creato alle", - "action": "Azione", - "readonlyOn": "Attiva la sola lettura", - "readonlyOff": "Disattiva la sola lettura", - "useProxyOn": "Attiva il proxy inverso", - "useProxyOff": "Disattiva il proxy inverso", - "delete": "Eliminare", - "listEmpty": "Nessuna registrazione.", - "createNewAccount": "Crea un nuovo account", - "taskType": "Tipo di attività", - "taskStatus": "Stato", - "lastProgress": "Ultimi progressi", - "errorDetails": "Dettagli errore", - "queueing": "In coda", - "processing": "In lavorazione", - "failed": "Fallito", - "canceled": "Annullato", - "finished": "Finito", - "fileTransfer": "Trasferimento di file", - "fileRecycle": "Riciclare file", - "importFiles": "Importa file esterni", - "transferProgress": "{{num}} file completati", - "waiting": "In sospeso", - "compressing": "Compressione", - "decompressing": "Decompressione", - "downloading": "Download in corso", - "transferring": "Trasferimento in corso", - "indexing": "Indicizzazione", - "listing": "Inserimento", - "allShares": "Condiviso", - "trendingShares": "Di tendenza", - "totalShares": "Condivisioni create", - "fileName": "Nome file", - "shareDate": "Condiviso alle", - "downloadNumber": "Download", - "viewNumber": "Visualizzazioni", - "language": "Lingua", - "iOSApp": "App iOS", - "connectByiOS": "Connettiti a <0>{{title}} tramite dispositivi iOS.", - "downloadOurApp": "Scarica la nostra APP per iOS:", - "fillInEndpoint": "Scansiona il codice QR sotto con la nostra app (NON utilizzare altre app per scansionare):", - "loginApp": "Puoi iniziare a utilizzare l'app iOS ora. Se riscontri problemi con il QR Code, puoi anche provare a inserire manualmente nome utente e password per accedere.", - "aboutCloudreve": "A proposito di Cloudreve", - "githubRepo": "Repository GitHub", - "homepage": "Homepage" - } -} diff --git a/public/locales/it-IT/common.json b/public/locales/it-IT/common.json deleted file mode 100644 index 8fb2262..0000000 --- a/public/locales/it-IT/common.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "pageNotFound": "Pagina non trovata", - "unknownError": "Errore sconosciuto", - "errLoadingSiteConfig": "Impossibile caricare la configurazione del sito: ", - "newVersionRefresh": "Una nuova versione della pagina corrente è disponibile e pronta per essere aggiornata.", - "errorDetails": "Dettagli dell'errore", - "renderError": "Si è verificato un errore nel rendering della pagina, provare a ricaricarla.", - "ok": "OK", - "cancel": "Annulla", - "select": "Seleziona", - "copyToClipboard": "Copia", - "close": "Chiudi", - "intlDateTime": "{{val, datetime}}", - "timeAgoLocaleCode": "it_IT", - "forEditorLocaleCode": "it", - "artPlayerLocaleCode": "it", - "errors": { - "401": "Effettuare il login.", - "403": "Non è permesso eseguire questa azione.", - "404": "Risorsa non trovata.", - "409": "Conflitto. ({{message}})", - "40001": "Parametri di input non validi ({{message}}).", - "40002": "Caricamento fallito.", - "40003": "Non è stato possibile creare la cartella.", - "40004": "Un oggetto con lo stesso nome già esiste.", - "40005": "Firma scaduta.", - "40006": "Tipo di policy non supportato.", - "40007": "Il gruppo attuale non ha il permesso di eseguire tale azione.", - "40011": "La sessione di caricamento non esiste o è scaduta.", - "40012": "Indice chunk non valido. ({{message}})", - "40013": "Lunghezza del contenuto non valida. ({{message}})", - "40014": "Superata la dimensione limite del batch per ottenere il link di origine.", - "40015": "Superata la dimensione limite del batch aria2.", - "40016": "Percorso non trovato.", - "40017": "Questo account è stato bloccato.", - "40018": "Questo account non è attivo.", - "40019": "Questa funzione non è abilitata.", - "40020": "Password o indirizzo e-mail errati.", - "40021": "Utente non trovato.", - "40022": "Codice di verifica non corretto.", - "40023": "Sessione di accesso non esistente.", - "40024": "Impossibile inizializzare WebAuthn.", - "40025": "Autenticazione fallita.", - "40026": "Il codice CAPTCHA non è corretto.", - "40027": "Verifica fallita, aggiornare la pagina e riprovare.", - "40028": "Consegna dell'e-mail non riuscita.", - "40029": "Questo link non è valido.", - "40030": "Questo link è scaduto.", - "40032": "Questa e-mail è già in uso.", - "40033": "Questo account non è attivato, l'e-mail di attivazione è stata reinviata.", - "40034": "Questo utente non può essere attivato.", - "40035": "Policy di archiviazione non trovata.", - "40039": "Gruppo non trovato.", - "40044": "File non trovato.", - "40045": "Impossibile elencare gli oggetti nella cartella indicata.", - "40047": "Fallita l'inizializzazione del filesystem.", - "40048": "Fallita la creazione dell'attività", - "40049": "La dimensione del file supera il limite.", - "40050": "Tipo di file non consentito.", - "40051": "Quota di archiviazione insufficiente.", - "40052": "Nome oggetto non valido, rimuovere i caratteri speciali.", - "40053": "Impossibile eseguire tale azione sulla cartella principale", - "40054": "Un file con lo stesso nome è già stato caricato in questa cartella, pulire le sessioni di caricamento.", - "40055": "Metadati del file incongruenti.", - "40056": "Tipo di file compresso non supportato.", - "40057": "Il criterio di archiviazione disponibile è cambiato, aggiornare l'elenco dei file e aggiungere nuovamente questa attività.", - "40058": "Questa condivisione non esiste o è già scaduta.", - "40069": "Password non corretta.", - "40070": "Questa azione non supporta l'anteprima.", - "40071": "Firma non valida.", - "50001": "Operazione di database non riuscita. ({{message}})", - "50002": "Fallita la firma dell'URL o della richiesta. ({{message}})", - "50004": "Operazione di I/O fallita. ({{message}})", - "50005": "Errore interno.", - "50010": "Il nodo desiderato non è disponibile.", - "50011": "Impossibile interrogare i metadati del file." - } -} \ No newline at end of file diff --git a/public/locales/it-IT/dashboard.json b/public/locales/it-IT/dashboard.json deleted file mode 100644 index 3af7437..0000000 --- a/public/locales/it-IT/dashboard.json +++ /dev/null @@ -1,827 +0,0 @@ -{ - "errors":{ - "40036": "La policy di archiviazione predefinita non può essere eliminata.", - "40037": "{{message}} file stanno utilizzando questa policy, per favore elimina prima quei file.", - "40038": "{{message}} gruppo(i) sta utilizzando questa policy, per favore scollega prima quei gruppi.", - "40040": "Impossibile eseguire tale azione sul gruppo di sistema.", - "40041": "{{message}} utenti sono ancora in questo gruppo, per favore elimina o scollega prima quegli utenti.", - "40042": "Non posso cambiare il gruppo del gruppo utenti del sistema.", - "40043": "Impossibile eseguire tale azione sull'utente predefinito.", - "40046": "Impossibile eseguire tale azione sul nodo master.", - "40060": "Il nodo slave non può inviare una richiesta di callback al master, controlla le impostazioni del nodo master: Base - Informazioni sul sito - URL del sito, assicurati che il nodo slave possa accedere a questo URL. ({{message}})", - "40061": "Versione Cloudreve non corrispondente. ({{message}})", - "50008": "Impossibile aggiornare l'impostazione. ({{message}})", - "50009": "Impossibile aggiungere la policy CORS." - }, - "nav": { - "summary": "Riepilogo", - "settings": "Impostazioni", - "basicSetting": "Base", - "publicAccess": "Accesso pubblico", - "email": "E-mail", - "transportation": "Trasmissione", - "appearance": "Aspetto", - "image": "Immagini", - "captcha": "Captcha", - "storagePolicy": "Policy d'archiviazione", - "nodes": "Nodi", - "groups": "Gruppi", - "users": "Utenti", - "files": "File", - "shares": "Condivisioni", - "tasks": "Compiti", - "remoteDownload": "Download remoto", - "generalTasks": "Generale", - "title": "Dashboard", - "dashboard": "Dashboard Cloudreve" - }, - "summary": { - "newsletterError": "Impossibile caricare la newsletter.", - "confirmSiteURLTitle": "Conferma l'URL del sito", - "siteURLNotSet": "Non hai ancora impostato l'URL del sito, vuoi impostarlo sull'attuale {{current}} ?", - "siteURLNotMatch": "L'URL del sito che hai impostato non corrisponde a quello corrente, vuoi impostarlo su {{current}} ?", - "siteURLDescription": "Questa impostazione è molto importante, assicurati che corrisponda all'URL effettivo del tuo sito. Puoi modificare questa impostazione in Impostazioni - Base.", - "ignore": "Ignora", - "changeIt": "Cambialo", - "trend": "Tendenza", - "summary": "Riepilogo", - "totalUsers": "Utenti", - "totalFiles": "File", - "publicShares": "Condivisioni pubbliche", - "privateShares": "Condivisioni private", - "homepage": "Homepage", - "documents": "Documenti", - "forum": "Forum", - "forumLink": "https://github.com/cloudreve/Cloudreve/discussions", - "telegramGroup": "Gruppo Telegram", - "telegramGroupLink": "https://t.me/cloudreve_global", - "buyPro": "Aggiornamento a Pro", - "publishedAt": "pubblicato alle <0>", - "newsTag": "annunci" - }, - "settings": { - "saved": "Impostazioni salvate.", - "save": "Salva", - "basicInformation": "Informazioni base", - "mainTitle": "Titolo principale", - "mainTitleDes": "Titolo principale del sito web.", - "subTitle": "Sottotitolo", - "subTitleDes": "Sottotitolo del sito web.", - "siteDescription": "Descrizione sito", - "siteDescriptionDes": "Descrizione del sito web, che potrà essere visualizzata nella pagina di riepilogo condivisa.", - "siteURL": "URL sito", - "siteURLDes": "Molto importante, assicurati che sia coerente con la situazione reale. Quando si utilizza la politica di archiviazione cloud e la piattaforma di pagamento, inserire l'indirizzo a cui è possibile accedere tramite WAN.", - "customFooterHTML": "HTML del piè di pagina personalizzato", - "customFooterHTMLDes": "Codice HTML personalizzato inserito in fondo alla pagina.", - "pwa": "Applicazione Web progressiva (PWA)", - "smallIcon": "Icona piccola", - "smallIconDes": "URL della piccola icona con ico come estensione", - "mediumIcon": "Icona media", - "mediumIconDes": "URL dell'icona media, preferisci la dimensione 192x192, formato png.", - "largeIcon": "Icona grande", - "largeIconDes": "URL dell'icona media, preferisci la dimensione 512x512, formato png. Questa icona verrà visualizzata anche quando si cambia account nell'app iOS.", - "displayMode": "Modalità di visualizzazione", - "displayModeDes": "La modalità di visualizzazione di un'applicazione PWA dopo l'installazione.", - "themeColor": "Colore Tema", - "themeColorDes": "Valore del colore CSS che influisce sul colore della barra di stato nella schermata di avvio della PWA, sulla barra di stato nella pagina dei contenuti e sulla barra degli indirizzi.", - "backgroundColor": "Colore di sfondo", - "backgroundColorDes": "Valore del colore CSS.", - "hint": "Suggerimento", - "webauthnNoHttps": "L'autenticazione Web richiede che il tuo sito web sia abilitato per HTTPS e conferma che in Impostazioni - Base - Anche l'URL del sito utilizza HTTPS.", - "accountManagement": "Account", - "allowNewRegistrations": "Accetta nuove iscrizioni", - "allowNewRegistrationsDes": "Dopo la disattivazione, nessun nuovo utente può essere registrato, a meno che non venga aggiunto manualmente dagli amministratori.", - "emailActivation": "Attivazione e-mail", - "emailActivationDes": "Dopo l'abilitazione, i nuovi utenti devono fare clic sul collegamento di attivazione nell'e-mail per completare le registrazioni. Assicurati che le impostazioni di consegna dell'e-mail siano corrette, altrimenti l'e-mail di attivazione non verrà consegnata.", - "captchaForSignup": "Captcha per le iscrizioni", - "captchaForSignupDes": "Se abilitare il captcha per le iscrizioni.", - "captchaForLogin": "Captcha per gli accessi", - "captchaForLoginDes": "Se abilitare il captcha per gli accessi.", - "captchaForReset": "Captcha per reimpostare la password", - "captchaForResetDes": "Abilitare il captcha per reimpostare la password.", - "webauthnDes": "Consentire agli utenti di accedere utilizzando un autenticatore hardware, il sito Web deve abilitare HTTPS affinché funzioni.", - "webauthn": "Autenticatore hardware", - "defaultGroup": "Gruppo predefinito", - "defaultGroupDes": "Il gruppo utenti iniziale dopo la registrazione dell'utente.", - "testMailSent": "L'e-mail di prova viene inviata.", - "testSMTPSettings": "Verifica le impostazioni SMTP", - "testSMTPTooltip": "Prima di inviare un'e-mail di prova, salvare le impostazioni SMTP modificate; i risultati della consegna dell'e-mail non verranno restituiti immediatamente, se non ricevi l'e-mail per un lungo periodo, controlla il registro degli errori generato da Cloudreve nel terminale.", - "recipient": "Destinatario", - "send": "Invia", - "smtp": "SMTP", - "senderName": "Nome del mittente", - "senderNameDes": "Nome del mittente visualizzato nell'e-mail.", - "senderAddress": "Indirizzo del mittente", - "senderAddressDes": "Indirizzo e-mail del mittente.", - "smtpServer": "Server SMTP", - "smtpServerDes": "Indirizzo del server SMTP, senza numero di porta.", - "smtpPort": "Porta SMTP", - "smtpPortDes": "Porta del server SMTP.", - "smtpUsername": "Nome utente SMTP", - "smtpUsernameDes": "Nome utente SMTP, generalmente coincide con l'indirizzo del mittente.", - "smtpPassword": "Password SMTP", - "smtpPasswordDes": "Password della casella di posta del mittente.", - "replyToAddress": "Indirizzo di risposta", - "replyToAddressDes": "La casella di posta utilizzata per ricevere e-mail di risposta quando gli utenti rispondono alle e-mail inviate dal sistema.", - "enforceSSL": "Applica la connessione SSL", - "enforceSSLDes": "Indica se applicare una connessione crittografata SSL. Se non puoi inviare e-mail, puoi disattivarlo e Cloudreve proverà a utilizzare STARTTLS e deciderà se utilizzare connessioni crittografate.", - "smtpTTL": "Connessione SMTP TTL (secondi)", - "smtpTTLDes": "Le connessioni SMTP stabilite durante il periodo TTL verranno riutilizzate dalle nuove richieste di recapito della posta.", - "emailTemplates": "Modelli di posta elettronica", - "activateNewUser": "Attiva nuovo utente", - "activateNewUserDes": "Modello per l'e-mail di attivazione dopo la registrazione del nuovo utente.", - "resetPassword": "Resetta la password", - "resetPasswordDes": "Modello per il resetta password.", - "sendTestEmail": "Invia email di prova", - "transportation": "Trasmissione", - "workerNum": "Numero del lavoratore", - "workerNumDes": "Il numero massimo di attività che devono essere eseguite in parallelo dalla coda delle attività del nodo master. Per avere effetto è necessario riavviare Cloudreve.", - "transitParallelNum": "Numero di trasferimenti in parallelo", - "transitParallelNumDes": "Numero massimo di co-processi paralleli per attività di trasferimento.", - "tempFolder": "Cartella temporanea", - "tempFolderDes": "Utilizzata per archiviare file temporanei generati da attività come decompressione, compressione, ecc.", - "textEditMaxSize": "Dimensione massima dei file di documenti modificabili", - "textEditMaxSizeDes": "La dimensione massima di un documento che può essere modificato online, i file oltre questa dimensione non possono essere modificati online. Questa impostazione si applica al testo semplice, al codice e ai documenti Office (WOPI).", - "failedChunkRetry": "Numero massimo di tentativi con errore del blocco", - "failedChunkRetryDes": "Numero massimo di tentativi dopo un blocco non riuscito, solo per caricamenti o trasferimenti lato server.", - "cacheChunks": "Blocco Cache per i tentativi", - "cacheChunksDes": "Se abilitato, i caricamenti di blocchi in streaming memorizzeranno nella cache i dati dei blocchi in una directory temporanea per riprovare dopo caricamenti non riusciti.\n Se disabilitato, i caricamenti di blocchi in streaming non occupano spazio aggiuntivo sul disco rigido, ma l'intero caricamento fallirà immediatamente dopo un singolo errore di blocco.", - "resetConnection": "Reimposta la connessione dopo un caricamento non riuscito", - "resetConnectionDes": "Se abilitato, il server forzerà il ripristino della connessione se la verifica del caricamento fallisce.", - "expirationDuration": "Durata della scadenza (secondi)", - "batchDownload": "Scaricamento in batch", - "downloadSession": "Sessione di Download", - "previewURL": "URL Anteprima", - "docPreviewURL": "URL anteprima del documento", - "uploadSession": "Sessione di caricamento", - "uploadSessionDes": "Per i criteri di archiviazione supportati, l'utente può riprendere i caricamenti entro la scadenza della sessione di caricamento. Il valore massimo può variare nei fornitori di archiviazione di terze parti.", - "downloadSessionForShared": "Scarica sessione in condivisioni", - "downloadSessionForSharedDes": "I download ripetuti di file condivisi entro questo periodo di tempo stabilito non verranno conteggiati nel numero totale di download.", - "onedriveMonitorInterval": "Intervallo di monitoraggio del caricamento di OneDrive", - "onedriveMonitorIntervalDes": "A intervalli prestabiliti, Cloudreve richiederà a OneDrive di verificare i caricamenti dei client per assicurarsi che siano sotto controllo.", - "onedriveCallbackTolerance": "Timeout del callback di OneDrive", - "onedriveCallbackToleranceDes": "Tempo massimo di attesa per il callback dopo che il client OneDrive ha terminato il caricamento, se lo supera il caricamento verrà considerato non riuscito.", - "onedriveDownloadURLCache": "Cache dei download di OneDrive", - "onedriveDownloadURLCacheDes": "Cloudreve può memorizzare nella cache il risultato dopo aver ottenuto l'URL di download del file per ridurre la frequenza delle richieste API hot.", - "slaveAPIExpiration": "Timeout dell'API slave (secondi)", - "slaveAPIExpirationDes": "Tempo di timeout affinché l'API master attenda le risposte alle richieste delle API slave.", - "heartbeatInterval": "Intervallo di heartbeat del nodo (secondi)", - "heartbeatIntervalDes": "L'intervallo con il quale il nodo master invia heartbeat ai nodi slave.", - "heartbeatFailThreshold": "Soglia tentativi di errore heartbeat", - "heartbeatFailThresholdDes": "Il numero massimo di tentativi che il master può effettuare dopo aver inviato un heartbeat a uno slave che non riesce. Dopo tutti i tentativi falliti, il nodo entrerà in modalità di ripristino.", - "heartbeatRecoverModeInterval": "Intervallo di heartbeat in modalità di recupero (secondi)", - "heartbeatRecoverModeIntervalDes": "Intervallo tra i tentativi del master di riconnettersi a un nodo dopo che il nodo è stato contrassegnato come modalità di ripristino.", - "slaveTransitExpiration": "Timeout trasferimento slave (secondi)", - "slaveTransitExpirationDes": "Tempo massimo che può essere utilizzato da uno slave per eseguire un'attività di trasferimento file.", - "nodesCommunication": "Comunicazione del nodo", - "cannotDeleteDefaultTheme": "Impossibile eliminare il tema predefinito.", - "keepAtLeastOneTheme": "Si prega di selezionare almeno un tema.", - "duplicatedThemePrimaryColor": "Colore primario duplicato.", - "themes": "Temi", - "colors": "Colori", - "themeConfig": "Configurazioni", - "actions": "Azioni", - "wrongFormat": "Formato errato.", - "createNewTheme": "Crea un nuovo tema", - "themeConfigDoc": "https://v4.mui.com/customization/default-theme/", - "themeConfigDes": "È possibile fare riferimento alle configurazioni complete disponibili all'indirizzo <0>Default Theme - Material-UI.", - "defaultTheme": "Tema Predefinito", - "defaultThemeDes": "Il tema predefinito da utilizzare quando l'utente non ne specifica uno preferito.", - "appearance": "Aspetto", - "personalFileListView": "Visualizzazione predefinita per l'elenco dei file personali", - "personalFileListViewDes": "La visualizzazione predefinita da utilizzare quando l'utente non specifica quella preferita.", - "sharedFileListView": "Visualizzazione predefinita per l'elenco dei file condivisi", - "sharedFileListViewDes": "La visualizzazione predefinita da utilizzare quando l'utente non specifica quella preferita.", - "primaryColor": "Colore primario", - "primaryColorText": "Testo sul colore primario", - "secondaryColor": "Colore secondario", - "secondaryColorText": "Testo sul colore secondario", - "avatar": "Avatar", - "gravatarServer": "Server Gravatar", - "gravatarServerDes": "URL del Gravatar del server mirror.", - "avatarFilePath": "Percorso del file dell'avatar", - "avatarFilePathDes": "Percorso per salvare i file avatar dell'utente.", - "avatarSize": "Dimensione massima del file avatar", - "avatarSizeDes": "Dimensione massima dei file avatar che gli utenti possono caricare.", - "smallAvatarSize": "Larghezza dell'avatar piccola", - "mediumAvatarSize": "Larghezza dell'avatar media", - "largeAvatarSize": "Larghezza dell'avatar grande", - "filePreview": "Anteprima file", - "officePreviewService": "Servizio anteprima di Office", - "officePreviewServiceDes": "Puoi utilizzare le seguenti variabili magiche:", - "officePreviewServiceSrcDes": "URL File", - "officePreviewServiceSrcB64Des": " URL del file con codifica Base64", - "officePreviewServiceName": "Nome File", - "thumbnails": "Miniature", - "thumbnailDoc": "Per ulteriori informazioni sulla miniatura, vedere il <0>document.", - "thumbnailDocLink":"https://docs.cloudreve.org/v/en/use/thumbnails", - "thumbnailBasic": "Di base", - "generators": "Generatori", - "thumbMaxSize": "Dimensione massima del file originale", - "thumbMaxSizeDes": "La dimensione massima del file originale per cui è possibile generare le miniature. Le miniature non verranno generate se i file superano questa dimensione.", - "generatorProxyWarning": "Per impostazione predefinita, i criteri di archiviazione non locali utilizzeranno solo il generatore \"Nativo nel criterio di archiviazione\". Puoi estendere la funzionalità delle miniature dei criteri di archiviazione di terze parti abilitando la funzione \"Generator proxy\".", - "policyBuiltin": "Politica di archiviazione nativa", - "policyBuiltinDes": "Utilizza l'API nativa del provider di archiviazione per elaborare le miniature. Per i criteri locali e S3, questo generatore non è disponibile e eseguirà automaticamente il fallback su altri generatori. Per altre policy di archiviazione, fare riferimento alla documentazione Cloudreve per i formati di immagine supportati.", - "cloudreveBuiltin":"Cloudreve integrato", - "cloudreveBuiltinDes": "Solo le immagini nei formati PNG, JPEG e GIF sono supportate utilizzando le funzionalità di elaborazione delle immagini integrate di Cloudreve.", - "libreOffice": "LibreOffice", - "libreOfficeDes": "Usa LibreOffice per generare miniature per i documenti Office. Questo generatore dipende da qualsiasi altro generatore di miniature di immagini (integrato in Cloudreve o VIPS).", - "vips": "VIPS", - "vipsDes": "Utilizza libvips per elaborare le miniature delle immagini, supportare più formati di immagine e consumare meno risorse.", - "thumbDependencyWarning": "I generatori di LibreOffice dipendono dai generatori integrati di Cloudreve o VIPS, abilita uno dei due.", - "ffmpeg": "FFmpeg", - "ffmpegDes": "Usa FFmpeg per generare le miniature dei video.", - "libRaw": "LibRaw", - "libRawDes": "Utilizzo di LibRaw per elaborare le immagini Raw", - "executable": "Eseguibile", - "executableDes": "L'indirizzo o il comando dell'eseguibile del generatore di terze parti.", - "executableTest": "Prova", - "executableTestSuccess": "Il generatore funziona, versione: {{version}}", - "generatorExts": "Estensioni disponibili", - "generatorExtsDes": "Elenco delle estensioni di file disponibili per questo generatore, utilizza la virgola per separarne più di una.", - "ffmpegSeek": "Posizione di acquisizione della miniatura", - "ffmpegSeekDes": "Definisci il tempo di intercettazione della miniatura, si consiglia di scegliere un valore inferiore per accelerare il processo di generazione. Se la lunghezza effettiva del video viene superata, la generazione della miniatura fallirà.", - "generatorProxy": "Proxy del generatore", - "enableThumbProxy": "Utilizza proxy generatore", - "proxyPolicyList": "Politica di archiviazione abilitata", - "proxyPolicyListDes": "Multi-selezionabile. Se abilitato, i file la cui politica di archiviazione non supporta la generazione nativa, le relative miniature saranno generate tramite proxy da Cloudreve.", - "thumbWidth": "Larghezza", - "thumbHeight": "Altezza", - "thumbSuffix": "Suffisso file", - "thumbConcurrent": "Conteggio corrente", - "thumbConcurrentDes": "-1 significa automatico.", - "thumbFormat": "Formato immagine", - "thumbFormatDes": "Disponibile: png/jpg", - "thumbQuality": "Qualità", - "thumbQualityDes": "Percentuale della qualità di compressione, valida solo per la codifica jpg.", - "thumbGC": "Esegui GC dopo la generazione della miniatura", - "captcha": "Captcha", - "captchaType": "Tipo di Captcha", - "plainCaptcha": "Semplice", - "reCaptchaV2": "reCAPTCHA V2", - "tencentCloudCaptcha": "Tencent Cloud Captcha", - "captchaProvider": "Fornitore del servizio captcha.", - "plainCaptchaTitle": "Captcha semplice", - "captchaWidth": "Larghezza", - "captchaHeight": "Altezza", - "captchaLength": "Lunghezza", - "captchaMode": "Modalità", - "captchaModeNumber": "Numeri", - "captchaModeLetter": "Lettere", - "captchaModeMath": "Matematica", - "captchaModeNumberLetter": "Numeri + lettere", - "captchaElement": "Elementi all'interno dell'immagine captcha.", - "complexOfNoiseText": "Complesso di testo rumore", - "complexOfNoiseDot": "Complesso di punti rumore", - "showHollowLine": "Mostra linee vuote", - "showNoiseDot": "Mostra punti rumore", - "showNoiseText": "Mostra testo rumore", - "showSlimeLine": "Mostra linee di slime", - "showSineLine": "Mostra le linee sinusoidali", - "siteKey": "CHIAVE Sito", - "siteKeyDes": "Puoi trovarlo nella <0>pagina Gestione app.", - "siteSecret": "Segreto", - "siteSecretDes": "Puoi trovarlo nella <0>pagina Gestione app.", - "secretID": "ID Segreto", - "secretIDDes": "Puoi trovarlo nella <0>pagina di gestione degli accessi.", - "secretKey": "Chiave Segreta", - "secretKeyDes": "Puoi trovarlo nella <0>pagina di gestione degli accessi.", - "tCaptchaAppID": "APPID", - "tCaptchaAppIDDes": "Puoi trovarlo nella <0>pagina di gestione Captcha.", - "tCaptchaSecretKey": "Chiave segreta dell'app", - "tCaptchaSecretKeyDes": "Puoi trovarlo nella <0>pagina di gestione Captcha.", - "staticResourceCache": "Cache pubblica delle risorse statiche", - "staticResourceCacheDes": "Durata massima della cache per le risorse statiche accessibili al pubblico (ad es. collegamento all'origine della policy locale, collegamento per il download).", - "wopiClient": "WOPI Client", - "wopiClientDes": "Estendi le funzionalità di anteprima online e modifica dei documenti di Cloudreve interfacciandosi con i sistemi di elaborazione dei documenti online che supportano il protocollo WOPI. Per ulteriori informazioni, fare riferimento alla <0>Documentazione ufficiale.", - "wopiDocLink": "https://docs.cloudreve.org/v/en/use/wopi", - "enableWopi": "Abilita WOPI", - "wopiEndpoint": "Endpoint di WOPI Discovery", - "wopiEndpointDes": "URL dell'endpoint dell'API WOPI Discovery.", - "wopiSessionTtl": "Modifica sessione TTL (secondi)", - "wopiSessionTtlDes": "L'utente apre una sessione di modifica di documenti online con una data di scadenza, oltre la quale la sessione non può continuare a salvare nuove modifiche." - }, - "policy": { - "sharp": "#", - "name": "Nome", - "type": "Tipo", - "childFiles": "File secondari", - "totalSize": "Dimensione totale", - "actions": "Azioni", - "authSuccess": "Autorizzazione concessa.", - "policyDeleted": "Criterio eliminato", - "newStoragePolicy": "Nuova politica di archiviazione", - "all": "Tutto", - "local": "Locale", - "remote": "Nodo remoto", - "qiniu": "Qiniu", - "upyun": "Upyun", - "oss": "Alibaba Cloud OSS", - "cos": "Tencent Cloud COS", - "onedrive": "OneDrive", - "s3": "AWS S3", - "refresh": "Aggiorna", - "delete": "Elimina", - "edit": "Modifica", - "editInProMode": "Modifica in modalità professionale", - "editInWizardMode": "Modifica in modalità guidata", - "selectAStorageProvider": "Seleziona un provider di archiviazione", - "comparesStoragePolicies": "Confronta le politiche di archiviazione", - "comparesStoragePoliciesLink": "https://docs.cloudreve.org/v/en/use/policy/compare", - "storagePathStep": "Percorso di archiviazione", - "sourceLinkStep": "Collegamenti sorgente", - "uploadSettingStep": "Caricamento in corso", - "finishStep": "Fine", - "policyAdded": "Politica di archiviazione aggiunta.", - "policySaved": "Politica di archiviazione salvata.", - "editLocalStoragePolicy": "Modifica politica di archiviazione locale", - "addLocalStoragePolicy": "Aggiungi criterio di archiviazione locale", - "optional": "Facoltativo", - "pathMagicVarDes": "Inserisci il percorso fisico della cartella in cui desideri archiviare i file. È supportato il percorso assoluto o relativo (relativo all'eseguibile Cloudreve). Puoi utilizzare variabili magiche nel percorso, che verranno automaticamente sostituite con i valori corrispondenti quando il file viene caricato; vedi <0>Elenco delle variabili magiche del percorso per le variabili magiche disponibili.", - "pathOfFolderToStoreFiles": "Percorso della cartella", - "filePathMagicVarDes": "Vuoi rinominare i file fisici caricati? La ridenominazione qui non influenzerà il nome del file finale presentato all'utente. Le variabili Magic possono essere utilizzate anche per i nomi dei file, vedi <0>Elenco delle variabili Magic per i nomi dei file per le variabili magiche disponibili.", - "autoRenameStoredFile": "Abilita la ridenominazione automatica", - "keepOriginalFileName": "Utilizza il nome file originale", - "renameRule": "Regola di rinomina", - "next": "Avanti", - "enableGettingPermanentSourceLink": "Consentire all'utente di ottenere un collegamento permanente all'origine del file?", - "enableGettingPermanentSourceLinkDes": "Se abilitato, gli utenti possono ottenere un collegamento diretto al contenuto del file, da utilizzare nell'applicazione Image Bed o per uso personale. Potrebbe anche essere necessario abilitare questa funzione nelle impostazioni del gruppo utenti per farlo disponibile per gli utenti.", - "allowed": "Abilita", - "forbidden": "Disabilita", - "useCDN": "Vuoi utilizzare CDN per il download e i collegamenti sorgente?", - "useCDNDes": "Se abilitato, la parte del dominio dell'URL che l'utente utilizza per accedere ai file verrà sostituita con il dominio CDN.", - "use": "Abilita", - "notUse": "Disabilita", - "cdnDomain": "Seleziona un protocollo e inserisci il dominio CDN:", - "cdnPrefix": "Dominio CDN", - "back": "Indietro", - "limitFileSize": "Vuoi limitare la dimensione massima di un singolo file che può essere caricato?", - "limit": "Sì", - "notLimit": "No", - "enterSizeLimit": "Inserisci la dimensione massima del file:", - "maxSizeOfSingleFile": "Dimensione massima del singolo file", - "limitFileExt": "Vuoi limitare le estensioni dei file?", - "enterFileExt": "Inserisci le estensioni dei file che possono essere caricate, separate da virgole e punto e virgola:", - "extList": "Elenco estensioni file", - "chunkSizeLabel": "Specifica la dimensione del blocco per i caricamenti ripristinabili. Un valore pari a 0 significa che non vengono utilizzati caricamenti ripristinabili.", - "chunkSizeDes": "Dopo aver abilitato il caricamento ripristinabile, i file caricati dagli utenti verranno suddivisi in blocchi e caricati sul lato server uno per uno. Dopo che il caricamento viene interrotto, gli utenti possono scegliere di continuare il caricamento dall'ultimo blocco caricato.", - "chunkSize": "Dimensione del blocco", - "nameThePolicy": "Ultimo passaggio, dai un nome alla politica di archiviazione:", - "policyName": "Nome della politica di archiviazione", - "finish": "Fine", - "furtherActions": "Per utilizzare questa politica di archiviazione, vai alla pagina di impostazione del gruppo utenti e associa questa politica di archiviazione al gruppo utenti appropriato.", - "backToList": "Torna all'elenco dei criteri di archiviazione", - "magicVar": { - "fileNameMagicVar": "Variabili magiche del nome file", - "pathMagicVar": "Variabili magiche del percorso", - "variable": "Variabile", - "description": "Descrizione", - "example": "Esempio", - "16digitsRandomString": "Stringa casuale di 16 cifre", - "8digitsRandomString": "Stringa casuale di 8 cifre", - "secondTimestamp": "Timestamp", - "nanoTimestamp": "Nano timestamp", - "uid": "ID utente", - "originalFileName": "Nome del file originale", - "originFileNameNoext": "Nome del file originale senza estensione", - "extension": "File extension name", - "uuidV4": "Nome dell'estensione del file", - "date": "Data", - "dateAndTime": "Data e ora", - "year": "Anno", - "month": "Mese", - "day": "Giorno", - "hour": "Ora", - "minute": "Minuto", - "second": "Secondo", - "userUploadPath": "Percorso di caricamento" - }, - "storageNode": "Nodo di archiviazione", - "communicationOK": "Comunicazione riuscita.", - "editRemoteStoragePolicy": "Modifica politica di archiviazione remota", - "addRemoteStoragePolicy": "Aggiungi politica di archiviazione remota", - "remoteDescription": "La politica di archiviazione remota ti consente di utilizzare un server che esegue anche Cloudreve come nodo di archiviazione slave e il traffico di upload e download degli utenti viene trasmesso direttamente su HTTP.", - "remoteCopyBinaryDescription": "Copia l'eseguibile Cloudreve con la stessa versione del master sul server che desideri utilizzare come nodo di archiviazione slave.", - "remoteSecretDescription": "Quello che segue è il segreto dello schiavo generato casualmente, di solito non è necessario modificarlo. Se hai esigenze di personalizzazione, puoi inserire il tuo segreto nel campo seguente.", - "remoteSecret": "Segreto del nodo slave", - "modifyRemoteConfig": "Modifica il file di configurazione Cloudreve sul nodo slave.", - "addRemoteConfigDes": " Crea un nuovo file <0>conf.ini nella stessa directory del Cloudreve slave, compila la configurazione dello slave e avvia/riavvia il Cloudreve slave. Di seguito è riportato un esempio di configurazione per il tuo slave Cloudreve, dove la sezione segreta è precompilata per te come generata nel passaggio precedente.", - "remoteConfigDifference": "Il formato del file di configurazione lato slave è più o meno lo stesso del lato master, con le seguenti differenze:", - "remoteConfigDifference1": "Il campo <1>mode nella sezione <0>System deve essere modificato in <2>slave.", - "remoteConfigDifference2": "Devi specificare il campo <1>Secret nella sezione <0>Slave, il cui valore è il segreto compilato o generato nel passaggio 2.", - "remoteConfigDifference3": "TLa configurazione cross-origin, ovvero il contenuto del campo <0>CORS, deve essere abilitata, come descritto nell'esempio sopra o nella documentazione ufficiale. Se la configurazione non è corretta, gli utenti non saranno in grado di caricare file sul nodo slave tramite il browser web.", - "inputRemoteAddress": "Inserisci l'indirizzo del nodo slave.", - "inputRemoteAddressDes": "Se HTTPS è abilitato sul master, anche lo slave deve abilitarlo e inserire l'indirizzo con il protocollo HTTPS di seguito.", - "remoteAddress": "Indirizzo del nodo slave", - "testCommunicationDes": "Dopo aver completato i passaggi precedenti, puoi verificare se la comunicazione funziona facendo clic sul pulsante Prova in basso.", - "testCommunication": "Testa la comunicazione con slave", - "pathMagicVarDesRemote": "Immetti il percorso fisico della cartella in cui desideri archiviare i file. È supportato il percorso assoluto o relativo (relativo all'eseguibile Cloudreve slave). Puoi utilizzare variabili magiche nel percorso, che verranno automaticamente sostituite con le corrispondenti valori quando il file viene caricato; vedere <0>Elenco delle variabili magiche del percorso per le variabili magiche disponibili.", - "storageBucket": "Bucket di archiviazione", - "editQiniuStoragePolicy": "Modifica la politica di archiviazione Qiniu", - "addQiniuStoragePolicy": "Aggiungi politica di archiviazione Qiniu", - "wanSiteURLDes": "Prima di utilizzare questa politica, assicurati che l'indirizzo inserito in Impostazioni di base - Informazioni sul sito - URL del sito corrisponda all'indirizzo effettivo e che <0>sia possibile accedervi correttamente tramite WAN.", - "createQiniuBucket": "Vai alla <0>dashboard Qiniu per creare un bucket di archiviazione.。", - "enterQiniuBucket": "Inserisci il \"nome del bucket\" appena creato:", - "qiniuBucketName": "Nome del Bucket", - "bucketTypeDes": "Seleziona il tipo di Bucket appena creato. Ti consigliamo di selezionare \"Bucket privato\" per una maggiore sicurezza.", - "privateBucket": "Bucket privato", - "publicBucket": "Bucket pubblico", - "bucketCDNDes": "Compila il nome del dominio CDN-accelerated che hai associato al Bucket di archiviazione.", - "bucketCDNDomain": "Dominio CDN", - "qiniuCredentialDes": "Vai a Centro personale - Gestione credenziali nella dashboard di Qiniu e compila gli AK, SK ottenuti.", - "ak": "AK", - "sk": "SK", - "cannotEnableForPrivateBucket": "Se questa funzionalità è abilitata per il Bucket privato, è necessario abilitare \"Utilizza collegamento origine reindirizzato\" per i gruppi di utenti.", - "limitMimeType": "Vuoi limitare i MimeType dei file che possono essere caricati?", - "mimeTypeDes": "Inserisci il MimeType dei file consentiti e separa più MimeType con una virgola. Qiniu rileverà il contenuto del file per determinare il MimeType e consentirà il caricamento se il MimeType è presente nel tuo elenco.", - "mimeTypeList": "Elenco MimeType", - "chunkSizeLabelQiniu": "Specifica la dimensione del blocco per i caricamenti ripristinabili. L'intervallo consentito è 1 MB - 1 GB.", - "createPlaceholderDes": "Vuoi creare un file segnaposto e detrarre la capacità dell'utente quando gli utenti iniziano a caricare? Se abilitato, Cloudreve impedirà agli utenti di avviare in modo dannoso più richieste di caricamento ma non di completare il caricamento.", - "createPlaceholder": "Crea file segnaposto", - "notCreatePlaceholder": "Non creare", - "corsSettingStep": "Politica CORS", - "corsPolicyAdded": "La Politica CORS è stata aggiunta correttamente.", - "editOSSStoragePolicy": "Modifica la politica di archiviazione Alibaba Cloud OSS", - "addOSSStoragePolicy": "Aggiungi la politica di archiviazione Alibaba Cloud OSS", - "createOSSBucketDes": "Vai a <0>OSS Dashboard per creare un Bucket . Attenzione: puoi utilizzare solo Bucket con SKU di <1>Archiviazione standard o <2>Archiviazione a bassa frequenza, <3>L'archiviazione non è supportata.", - "ossBucketNameDes": "Inserisci il <0>nome del Bucket specificato:", - "bucketName": "Nome Bucket", - "publicReadBucket": "Lettura pubblica", - "ossEndpointDes": "Vai alla pagina di riepilogo del Bucket, inserisci l'<2>Endpoint nella sezione <1>Accesso esterno, nella pagina <0>Dominio di accesso.", - "endpoint": "EndPoint", - "endpointDomainOnly": "Formato errato, inserisci semplicemente il nome host del dominio.", - "ossLANEndpointDes": "Se il tuo Cloudreve è distribuito su servizi Alibaba Cloud che si trovano nella stessa zona di disponibilità del Bucket OSS, puoi inoltre specificare un endpoint intranet, Cloudreve proverà a utilizzare questo endpoint sul lato server per ridurre i costi del traffico . Vuoi utilizzare l'endpoint intranet OSS?", - "intranetEndPoint": "Endpoint Intranet", - "ossCDNDes": "Vuoi utilizzare Alibaba Cloud CDN per velocizzare l'accesso ai file?", - "createOSSCDNDes": "Vai su <0>Alibaba Cloud CDN Dashboard per creare un dominio CDN, l'origine del CDN dovrebbe essere il tuo Bucket OSS. Inserisci il dominio CDN e seleziona se desideri utilizzare HTTPS:", - "ossAKDes": "Ottieni la tua chiave di accesso nella pagina <0>Gestione delle informazioni sulla sicurezza, inserisci la chiave di accesso qui sotto:", - "shouldNotContainSpace": "Non può contenere spazi.", - "nameThePolicyFirst": "Nomina la politica di archiviazione:", - "chunkSizeLabelOSS": "Specifica la dimensione del blocco per i caricamenti ripristinabili. L'intervallo consentito è 100 KB - 5 GB.", - "ossCORSDes": "Questa politica di archiviazione richiede una politica CORS per abilitare il caricamento dal browser. Cloudreve può configurarla automaticamente per te oppure puoi impostarla manualmente seguendo i passaggi nella documentazione. Se hai già impostato la politica CORS per questo Bucket, questo passaggio può essere saltato.", - "letCloudreveHelpMe": "Lascia che Cloudreve lo imposti per me", - "skip": "Salta", - "editUpyunStoragePolicy": "Modifica la politica di archiviazione Upyun", - "addUpyunStoragePolicy": "Aggiungi la politica di archiviazione Upyun", - "createUpyunBucketDes": "Vai alla <0>Dashboard Upyun per creare un servizio di archiviazione.", - "storageServiceNameDes": "Inserisci il nome del tuo servizio di archiviazione:", - "storageServiceName": "Nome del servizio", - "operatorNameDes": "Crea un operatore per questo servizio, autorizza gli operatori con permessi di lettura, scrittura ed eliminazione, inserisci le informazioni dell'operatore di seguito.", - "operatorName": "Nome dell'operatore", - "operatorPassword": "Password operatore", - "upyunCDNDes": "Compila il dominio associato al servizio di archiviazione e scegli se utilizzare HTTPS.", - "upyunOptionalDes": "Puoi saltare questo passaggio e mantenerlo come predefinito, ma ti consigliamo vivamente di seguire le istruzioni riportate di seguito.", - "upyunTokenDes": "Vai al pannello Configurazione funzionalità del servizio di archiviazione creato, vai alla scheda Configurazione accesso, abilita Token Anti-Hotlinking e imposta un segreto.", - "tokenEnabled": "Abilita token anti-hotlink", - "tokenDisabled": "Non utilizzare Token Anti-Hotlinking", - "upyunTokenSecretDes": "Inserisci il segreto del Token Anti-Hotlinking.", - "upyunTokenSecret": "Segreto del Token Anti-Hotlink", - "cannotEnableForTokenProtectedBucket": "Questa funzionalità non è supportata se abiliti Token Anti-Hotlinking.", - "callbackFunctionStep": "Serverless callback", - "callbackFunctionAdded": "La funzione di callback è stata aggiunta.", - "editCOSStoragePolicy": "Modifica politica di archiviazione COS", - "addCOSStoragePolicy": "Aggiungi politica di archiviazione COS", - "createCOSBucketDes": "Vai alla <0>Dashboard COS per creare un Bucket di archiviazione.", - "cosBucketNameDes": "Vai alla pagina delle impostazioni di base del Bucket creato, inserisci il <0>nome del Bucket di seguito:", - "cosBucketFormatError": "Il formato del nome del Bucket non è corretto, un esempio: ccc-1252109809", - "cosBucketTypeDes": "Di seguito puoi selezionare il tipo di controllo dell'accesso per il Bucket che hai creato. Ti consigliamo di selezionare <0>Lettura/Scrittura privata per una maggiore sicurezza, il Bucket privato non dispone di \"Ottieni collegamento origine\" caratteristica.", - "cosPrivateRW": "Lettura/scrittura privata", - "cosPublicRW": "Lettura pubblica e scrittura privata", - "cosAccessDomainDes": "Vai alla configurazione di base del Bucket creato e compila il <1>Dominio di accesso indicato nella sezione <0>Informazioni di base.", - "accessDomain": "Dominio di accesso", - "cosCDNDes": "Vuoi utilizzare Tencent Cloud CDN per velocizzare l'accesso ai file?", - "cosCDNDomainDes": "Vai alla <0>Console di gestione CDN di Tencent Cloud per creare un dominio di accelerazione CDN e impostare il sito di origine sul Bucket COS appena creato. Inserisci il nome del dominio CDN di seguito e seleziona se utilizzarlo HTTPS.", - "cosCredentialDes": "Ottieni una coppia di chiavi di accesso dalla pagina <0>Chiavi di accesso di Tencent Cloud e compilale di seguito. Assicurati che la coppia di chiavi abbia l'autorizzazione di accesso ai servizi COS e SCF.", - "secretId": "Id segreto", - "secretKey": "Chiave segreta", - "cosCallbackDes": "Il trasferimento diretto lato client del Bucket di archiviazione COS richiede l'uso del prodotto <0>Cloud Functions di Tencent Cloud per garantire callback di caricamento controllati. Questo passaggio può essere saltato se intendi utilizzare questa politica di archiviazione per te stesso o assegnarla a un gruppo di utenti attendibili. Se è per uso pubblico, assicurati di creare funzioni cloud di callback.", - "cosCallbackCreate": "Cloudreve può provare a creare automaticamente la funzione cloud di callback per te, seleziona la regione del Bucket COS e continua. Potrebbero essere necessari alcuni secondi per la creazione, quindi sii paziente. Assicurati che il tuo account Tencent Cloud abbia il servizio di funzione cloud abilitato prima di crearlo.", - "cosBucketRegion": "Regione del Bucket", - "ap-beijing": "ap-beijing", - "ap-chengdu": "ap-chengdu", - "ap-guangzhou": "ap-guangzhou", - "ap-guangzhou-open": "ap-guangzhou-open", - "ap-hongkong": "ap-hongkong", - "ap-mumbai": "ap-mumbai", - "ap-shanghai": "ap-shanghai", - "na-siliconvalley": "na-siliconvalley", - "na-toronto": "na-toronto", - "applicationRegistration": "Registrazione dell'applicazione", - "grantAccess": "Concedi l'accesso", - "warning": "Avvertimento", - "odHttpsWarning": "Devi abilitare HTTPS per utilizzare i criteri di archiviazione di OneDrive/SharePoint; dopo averlo abilitato, assicurati di modificare Impostazioni - Base - Informazioni sul sito - URL del sito.", - "editOdStoragePolicy": "Modifica politica di archiviazione OneDrive/SharePoint", - "addOdStoragePolicy": "Aggiungi politica di archiviazione OneDrive/SharePoint", - "creatAadAppDes": "Vai a <0>Dashboard di Azure Active Directory (cloud mondiale) o <1>Dashboard di Azure Active Directory (cloud cinese a 21 V), dopo aver effettuato l'accesso, vai a <2> Pannello di amministrazione di Azure Active Directory, puoi facoltativamente utilizzare un account diverso da quello utilizzato per archiviare i file per accedere.", - "createAadAppDes2": "Vai al menu <0>Registrazioni app a sinistra e fai clic sul pulsante <1>Nuova registrazione.", - "createAadAppDes3": "Compila il modulo di registrazione dell'applicazione. Assicurati che <0>Tipi di account supportati sia selezionato come <1>\tAccount in qualsiasi directory organizzativa (qualsiasi directory Azure AD - Multitenant) e account Microsoft personali (ad es. Skype, Xbox); <2>URI di reindirizzamento (facoltativo) è selezionato come <3>Web e compila <4>{{url}} per altro campi, lascialo come predefinito.", - "aadAppIDDes": "Una volta creato, vai alla pagina <0>Panoramica in Gestione applicazioni, copia l'<1>ID applicazione (client) e compila i seguenti campi:", - "aadAppID": "ID applicazione (client)", - "addAppSecretDes": "Vai al menu <0>Certificati e segreti sul lato sinistro, fai clic sul pulsante <1>Nuovo segreto client e seleziona <3>Mai per <2>Scade Dopo aver finito di creare il segreto client, inserisci il valore del segreto client di seguito:", - "aadAppSecret": "Segreto del client", - "aadAccountCloudDes": "Seleziona il tipo di account Microsoft 365:", - "multiTenant": "Cloud pubblico mondiale", - "gallatin": "Cloud cinese 21V", - "sharePointDes": "Vuoi archiviare file in SharePoint?", - "saveToSharePoint": "Archivia file su SharePoint", - "saveToOneDrive": "Memorizza i file su OneDrive predefinito", - "spSiteURL": "URL del sito SharePoint", - "odReverseProxyURLDes": "Vuoi utilizzare un server proxy inverso personalizzato per il download dei file?", - "odReverseProxyURL": "URL del server proxy inverso", - "chunkSizeLabelOd": "Specifica la dimensione del blocco per i caricamenti ripristinabili. OneDrive richiede che sia un multiplo intero di 320 KiB (327.680 byte).", - "limitOdTPSDes": "Vuoi aggiungere un limite alla frequenza delle richieste API OneDrive lato server?", - "tps": "Limite TPS", - "tpsDes": "Limita questa politica di archiviazione al numero massimo di richieste API inviate a OneDrive al secondo. Le richieste che superano questa frequenza saranno limitate. Quando più nodi Cloudreve trasferiscono file, ciascuno utilizza il proprio Bucket di token, quindi ridimensiona questo numero deve essere abbassato in modo appropriato in questa condizione.", - "tpsBurst": "Carichi TPS", - "tpsBurstDes": "Quando inattivo, Cloudreve può riservare un numero specificato di slot per futuri carichi di traffico.", - "odOauthDes": "Tuttavia, dovrai fare clic sul pulsante in basso e autorizzare l'accesso all'account Microsoft per completare l'inizializzazione prima di poterlo utilizzare. Puoi autorizzare nuovamente in seguito nella pagina dell'elenco delle politica di archiviazione.", - "gotoAuthPage": "Vai alla pagina di autorizzazione", - "s3SelfHostWarning": "Le politiche di storage di AWS S3 sono attualmente sicure solo per uso personale o per gruppi di utenti attendibili.", - "editS3StoragePolicy": "Modifica politica di archiviazione AWS S3", - "addS3StoragePolicy": "Aggiungi politica di archiviazione AWS S3", - "s3BucketDes": "Vai alla dashboard AWS S3 per creare un Bucket, inserisci il <0>nome del Bucket appena creato:", - "publicAccessDisabled": "Lettura pubblica disabilitata", - "publicAccessEnabled": "Lettura pubblica abilitata", - "s3EndpointDes": "(Facoltativo) Specificare l'EndPoint (nodo geografico) del Bucket di archiviazione nel formato URL completo, ad esempio <0>https://Bucket.region.example.com. Lasciandolo vuoto verrà utilizzato l'endpoint predefinito generato dal sistema.", - "selectRegionDes": "Seleziona la regione in cui si trova il Bucket di archiviazione o inserisci manualmente il codice regione.", - "enterAccessCredentials": "Ottieni una coppia di credenziali di accesso e inseriscile di seguito:", - "accessKey": "Chiave di accesso", - "chunkSizeLabelS3": "Specifica la dimensione del blocco per i caricamenti ripristinabili. L'intervallo consentito è 5 MB - 5 GB.", - "editPolicy": "Modifica politica di archiviazione", - "setting":"Nome impostazione", - "value": "Valore", - "description": "Descrizione", - "id": "ID", - "policyID": "ID della politica di archiviazione.", - "policyType": "Tipo della politica di archiviazione.", - "server": "Server", - "policyEndpoint": "Endpoint del nodo di archiviazione.", - "bucketID": "Identificatore del Bucket di archiviazione.", - "yes": "Sì", - "no": "No", - "privateBucketDes": "Il Bucket di archiviazione è privato.", - "resourceRootURL": "URL root della risorsa file", - "resourceRootURLDes": "Prefisso dell'URL generato per l'anteprima e il download.", - "akDes": "Chiave di accesso / Aggiorna Token", - "maxSizeBytes": "Dimensione massima del singolo file (byte)", - "maxSizeBytesDes": "La dimensione massima del file può essere caricata, 0 significa nessun limite.", - "autoRename": "Rinomina automaticamente", - "autoRenameDes": "Rinominare automaticamente i file.", - "storagePath": "Percorso di archiviazione", - "storagePathDes": "Percorso fisico dei file caricati.", - "fileName": "Nome file", - "fileNameDes": "Nome fisico dei file caricati.", - "allowGetSourceLink": "Consenti di ottenere il collegamento sorgente", - "allowGetSourceLinkDes": "Consentire l'ottenimento dei collegamenti di origine. Tieni presente che alcuni tipi di politiche di archiviazione non sono supportati e, anche se sono attivati qui, i collegamenti di origine ottenuti non saranno validi.", - "upyunToken": "Token anti-hotlink Upyun", - "upyunOnly": "Disponibile solo per la politica Upyun.", - "allowedFileExtension": "Estensioni di file consentite", - "emptyIsNoLimit": "Vuoto significa nessun limite.", - "allowedMimetype": "MimeType consentito", - "qiniuOnly": "ADisponibile solo per la politica Qiniu.", - "odRedirectURL": "URL di reindirizzamento di OneDrive", - "noModificationNeeded": "In genere non è necessaria alcuna modifica.", - "odReverseProxy": "Server proxy inverso OneDrive", - "odOnly": "Disponibile solo per le politiche di OneDrive.", - "odDriverID": "ID driver OneDrive/SharePoint", - "odDriverIDDes": "Disponibile solo per le politiche OneDrive, vuoto significa utilizzare il driver OneDrive predefinito.", - "s3Region": "Regione Amazon S3", - "s3Only": "Disponibile solo per la politica AWS S3.", - "lanEndpoint": "EndPoint Intranet.", - "ossOnly": "Disponibile solo per la politica OSS.", - "chunkSizeBytes": "Dimensione del blocco (byte)", - "chunkSizeBytesDes": "Dimensione del blocco per caricamenti ripristinabili. Supportato solo nella politica di archiviazione parziale.", - "placeHolderWithSize": "Utilizza il segnaposto prima del caricamento", - "placeHolderWithSizeDes": "Creare un file segnaposto prima del caricamento. Supportato solo nella politica di archiviazione parziale.", - "saveChanges": "Salva modifiche", - "s3EndpointPathStyle": "Seleziona il formato dell'indirizzo dell'endpoint S3 o, se non sai cosa selezionare, lascia semplicemente l'impostazione predefinita. Alcune politiche di archiviazione compatibili con S3 di terze parti potrebbero richiedere che questa opzione funzioni. Quando attivata , forzeremo l'uso di indirizzi in formato simile a un percorso, come <0>http://s3.amazonaws.com/BUCKET/KEY..", - "usePathEndpoint": "Forza stile percorso", - "useHostnameEndpoint": "Utilizza il nome host se possibile", - "thumbExt": "Estensioni che supportano le miniature", - "thumbExtDes": "Lascia vuoto per indicare che viene utilizzato il set predefinito di politiche di archiviazione. Non valido per le politiche di archiviazione S3 locali." - }, - "node": { - "#": "#", - "name": "Nome", - "status": "Stato", - "features": "Funzioni abilitate", - "action": "Azioni", - "remoteDownload": "Download remoto", - "nodeDisabled": "Il nodo è disabilitato.", - "nodeEnabled": "Il nodo è abilitato.", - "nodeDeleted": "Il nodo è stato eliminato.", - "disabled": "Disabilitato", - "online": "Online", - "offline": "Offline", - "addNewNode": "Nuovo nodo", - "refresh": "Aggiorna", - "enableNode": "Abilita nodo", - "disableNode": "Disabilita nodo", - "edit": "Modifica", - "delete": "Elimina", - "slaveNodeDes": "È possibile aggiungere un server che esegue anche Cloudreve come nodo slave. Un nodo slave può condividere il carico di determinate attività asincrone (come i download remoti) per il master. Fare riferimento alla seguente procedura guidata per distribuire e configurare un nodo slave. <0> Se hai già distribuito una politica di archiviazione del nodo remoto sul server di destinazione, puoi saltare alcuni passaggi in questa pagina e inserire semplicemente il segreto dello slave e l'indirizzo del server qui, mantenendoli uguali in politica di archiviazione remota. Nelle versioni successive, la configurazione relativa alla politica di archiviazione remota verrà unita qui.", - "overwriteDes": "; Le seguenti impostazioni sono opzionali e corrispondono ai parametri rilevanti del nodo master, <0>; che possono essere applicati al nodo slave tramite il file di configurazione, regolarli in base a <0 >; la situazione attuale. La modifica delle seguenti impostazioni richiede il riavvio del nodo slave per avere effetto.", - "workerNumDes": "Numero massimo di attività da eseguire in parallelo nella coda delle attività.", - "parallelTransferDes": "Numero massimo di goroutine parallele durante il trasferimento di file nella coda delle attività", - "chunkRetriesDes": "Numero massimo di tentativi dopo il caricamento non riuscito di un blocco.", - "multipleMasterDes": "Un'istanza Cloudreve slave può interfacciarsi con più nodi master Cloudreve; aggiungi semplicemente questo nodo slave a tutti i nodi master e mantieni il segreto coerente.", - "ariaSuccess": "Connessione riuscita, versione Aria2: {{version}}", - "slave": "slave", - "master": "master", - "aria2Des": "La funzionalità di download remoto di Cloudreve è alimentata da <0>Aria2. Per utilizzarla, avvia Aria2 con lo stesso utente che esegue Cloudreve sul server del nodo di destinazione e abilita il servizio RPC nel file di configurazione di Aria2, <1>Aria2 deve condividere lo stesso file system del processo {{mode}} Cloudreve Per ulteriori informazioni e linee guida, fare riferimento alla sezione <2>Download offline della documentazione.", - "slaveTakeOverRemoteDownload": "Hai bisogno che questo nodo si occupi delle attività di download remoto?", - "masterTakeOverRemoteDownload": "È necessario che il master si occupi dell'attività di download remoto?", - "routeTaskSlave": "Dopo l'abilitazione, le richieste di download remoto degli utenti possono essere programmate su questo nodo per l'elaborazione.", - "routeTaskMaster": "Dopo l'abilitazione, le richieste di download remoto degli utenti possono essere programmate sul nodo master per l'elaborazione.", - "enable": "Abilita", - "disable": "Disabilita", - "slaveNodeTarget": "sul nodo slave che condivide lo stesso file system con lo slave Cloudreve", - "masterNodeTarget": "che condivide lo stesso file system con Cloudreve", - "aria2ConfigDes": "Avvia Aria2 {{target}} che condivide lo stesso file system con Cloudreve. Quando avvii Aria2, devi abilitare il servizio RPC nel suo file di configurazione e impostare il segreto RPC per un uso futuro. Di seguito è riportato un esempio di configurazione come riferimento.", - "enableRPCComment": "Abilita il servizio RPC", - "rpcPortComment": "Porta RPC da ascoltare", - "rpcSecretComment": "Segreto RPC, puoi cambiarlo da solo.", - "rpcConfigDes": "Si consiglia di avviare Aria2 prima del nodo Cloudreve nel processo di avvio di routine, in modo che il nodo Cloudreve possa iscriversi alle notifiche degli eventi su Aria2 e le modifiche allo stato di download siano gestite in modo più tempestivo. Naturalmente, se questo processo non è disponibile, il nodo Cloudreve monitorerà lo stato dell'attività anche tramite polling.", - "rpcServerDes": "Inserisci l'indirizzo del servizio RPC che {{mode}} Cloudreve utilizza per comunicare con Aria2. Può essere compilato come <0>http://127.0.0.1:6800/, dove il numero di porta <1>6800 è coerente con <2>rpc-listen-port nel file di configurazione sopra.", - "rpcServer": "RPC Server", - "rpcServerHelpDes": "L'indirizzo del server RPC contiene il numero di porta completo, ad esempio http://127.0.0.1:6800/, Lascia vuoto per indicare che il servizio Aria2 non è abilitato.", - "rpcTokenDes": "Segreto RPC, coerente con <0>rpc-secret nel file di configurazione di Aria2; lascia vuoto se non impostato.", - "aria2PathDes": "Compila il <0>percorso assoluto sul nodo che Aria2 utilizza come directory di download temporanea. Il processo Cloudreve sul nodo necessita di permessi di lettura, scrittura ed esecuzione su questa directory.", - "aria2SettingDes": "Compila di seguito alcune informazioni aggiuntive sui parametri Aria2, secondo le tue necessità.", - "refreshInterval": "Intervallo di aggiornamento dello stato (secondi)", - "refreshIntervalDes": "L'intervallo in cui Cloudreve richiede un aggiornamento dello stato dell'attività da Aria2.", - "rpcTimeout": "Timeout RPC (secondi)", - "rpcTimeoutDes": "Tempo di attesa massimo quando si chiamano i servizi RPC.", - "globalOptions": "Opzioni di lavoro globali", - "globalOptionsDes": "Impostazioni aggiuntive apportate durante la creazione di un processo di download, scritte in formato codificato JSON, puoi anche scrivere queste impostazioni nel file di configurazione di Aria2, consulta la documentazione ufficiale di Aria2 per le impostazioni disponibili.", - "testAria2Des": "Una volta completati questi passaggi, puoi fare clic sul pulsante Prova in basso per verificare se {{mode}} Cloudreve comunica correttamente con Aria2.", - "testAria2DesSlaveAddition": "Assicurati di aver eseguito e superato il \"Test di comunicazione con lo slave\" nella pagina precedente prima di eseguire il test.", - "testAria2": "Provo la comunicazione di Aria2", - "aria2DocURL": "https://docs.cloudreve.org/v/en/use/aria2", - "nameNode": "Inserisci il nome di questo nodo:", - "loadBalancerRankDes": "Specifica un peso di bilanciamento del carico per questo nodo, il valore è un numero intero. Alcune politiche di bilanciamento del carico peseranno i nodi in base a questo valore.", - "loadBalancerRank": "Peso di bilanciamento del carico", - "nodeSaved": "Nodo salvato con successo.", - "nodeSavedFutureAction": "Se aggiungi un nuovo nodo, dovrai anche abilitare manualmente il nodo nell'elenco dei nodi affinché funzioni correttamente.", - "backToNodeList": "Torna all'elenco dei nodi", - "communication": "Comunicazione", - "otherSettings": "Altre impostazioni", - "finish": "Fine", - "nodeAdded": "Nodo aggiunto con successo.", - "nodeSavedNow": "Nodo salvato con successo.", - "editNode": "Modifica nodo", - "addNode": "Aggiungi nodo" - }, - "group": { - "#": "#", - "name": "Nome", - "type": "Politica di archiviazione", - "count": "Utenti minorenni", - "size": "Quota spazio di archiviazione", - "action": "Azioni", - "deleted": "Gruppo eliminato.", - "new": "Nuovo gruppo", - "aria2FormatError": "Errore di formato delle opzioni Aria2.", - "atLeastOnePolicy": "È richiesta almeno una politica di archiviazione.", - "added": "Gruppo aggiunto con successo.", - "saved": "Gruppo salvato con successo.", - "editGroup": "Modifica {{group}}", - "nameOfGroup": "Nome", - "nameOfGroupDes": "Nome del gruppo.", - "storagePolicy": "Politica di archiviazione", - "storageDes": "Seleziona la politica di archiviazione utilizzata da questo gruppo.", - "initialStorageQuota": "Quota di archiviazione iniziale", - "initialStorageQuotaDes": "Lo spazio di archiviazione massimo può essere utilizzato da un singolo utente in questo gruppo.", - "downloadSpeedLimit": "Velocità massima di download", - "downloadSpeedLimitDes": "Compila 0 per indicare nessun limite. Quando la restrizione è attivata, la velocità massima di download sarà limitata quando gli utenti di questo gruppo scaricano tutti i file secondo la politica di archiviazione che supporta il limite di velocità.", - "bathSourceLinkLimit": "Dimensione massima dei collegamenti sorgente batch", - "bathSourceLinkLimitDes": "Per i file secondo la politica di archiviazione supportata, il numero massimo di file consentiti agli utenti per ottenere collegamenti di origine in un singolo batch, inserire 0 significa che non è consentita la generazione batch di collegamenti di origine.", - "allowCreateShareLink": "Condividi file", - "allowCreateShareLinkDes": "Se disabilitato, gli utenti non potranno creare collegamenti di condivisione.", - "allowDownloadShare": "Scarica file condivisi", - "allowDownloadShareDes": "Se disabilitato, l'utente non potrà scaricare file condivisi.", - "allowWabDAV": "WebDAV", - "allowWabDAVDes": "Se disabilitato, gli utenti non potranno connettersi allo spazio di archiviazione tramite il protocollo WebDAV", - "allowWabDAVProxy": "Proxy WebDAV", - "allowWabDAVProxyDes": "Se abilitato, gli utenti possono configurare WebDAV per eseguire il proxy del traffico durante il download dei file", - "disableMultipleDownload": "Disabilita richieste di download multiple", - "disableMultipleDownloadDes": "Valido solo per le politiche di archiviazione locale. Se disabilitato, gli utenti non possono utilizzare lo strumento di download multi-thread.", - "allowRemoteDownload": "Download remoto", - "allowRemoteDownloadDes": "Consentire agli utenti di creare attività di download remoto", - "aria2Options": "Opzioni di lavoro Aria2", - "aria2OptionsDes": "I parametri aggiuntivi che questo gruppo utente porta con sé durante la creazione di attività di download remoto. Le opzioni sono scritte in formato codificato JSON, puoi anche scrivere queste impostazioni nel file di configurazione di Aria2, consulta la documentazione ufficiale per i parametri disponibili.", - "aria2BatchSize": "Dimensione massima delle attività batch Aria2", - "aria2BatchSizeDes": "Il numero di attività di download remoto simultanee consentite all'utente. Compila 0 o lascia vuoto per non indicare alcun limite.", - "serverSideBatchDownload": "Download batch lato server", - "serverSideBatchDownloadDes": "Consentire agli utenti di selezionare più file per utilizzare il download batch di inoltro lato server, dopo la disattivazione, gli utenti possono comunque utilizzare la funzionalità di download batch basata su browser puro.", - "compressTask": "Attività di compressione/decompressione", - "compressTaskDes": "Consentire all'utente di creare l'attività di compressione/decompressione", - "compressSize": "Dimensione massima del file da comprimere", - "compressSizeDes": "La dimensione totale massima del file dei processi di compressione che possono essere creati dall'utente. Inserisci 0 per indicare nessun limite.", - "decompressSize": "Dimensione massima del file da decomprimere", - "decompressSizeDes": "La dimensione totale massima del file dei processi di decompressione che può essere creato dall'utente. Inserisci 0 per indicare nessun limite.", - "redirectedSource": "Utilizza il collegamento sorgente reindirizzato", - "redirectedSourceDes": "Se abilitato, il collegamento sorgente al file ottenuto dall'utente verrà reindirizzato da Cloudreve con un collegamento più breve. Quando disabilitato, il collegamento sorgente al file ottenuto dall'utente diventa l'URL originale del file. Alcune politiche producono collegamenti di origine non reindirizzati che non rimangono persistenti, vedere <0>Confronto delle politiche di archiviazione.", - "advanceDelete": "Consenti opzioni avanzate di eliminazione file", - "advanceDeleteDes": "Una volta abilitati, gli utenti possono scegliere se forzare l'eliminazione e se scollegare solo i collegamenti fisici durante l'eliminazione dei file. Queste opzioni sono simili alla dashboard di amministrazione durante l'eliminazione dei file." - }, - "user": { - "deleted": "Utente eliminato.", - "new": "Nuovo utente", - "filter": "Filtro", - "selectedObjects": "{{num}} oggetti selezionati.", - "nick": "Soprannome", - "email": "E-mail", - "group": "Gruppo", - "status": "Stato", - "usedStorage": "Archiviazione utilizzata", - "active": "Attivo", - "notActivated": "Inattivo", - "banned": "Bloccato", - "bannedBySys": "Bandito dal sistema", - "toggleBan": "Blocca/Sblocca", - "filterCondition": "Condizioni del filtro", - "all": "Tutto", - "userStatus": "Stato dell'utente", - "searchNickUserName": "Cerca soprannome/nome utente", - "apply": "Applica", - "added": "Utente aggiunto.", - "saved": "Utente salvato.", - "editUser": "Modifica {{nick}}", - "password": "Password", - "passwordDes": "Lasciare vuoto significa nessuna modifica.", - "groupDes": "Gruppo a cui appartiene l'utente.", - "2FASecret": "Segreto 2FA", - "2FASecretDes": "Segreto dell'autenticatore 2FA, lascia vuoto per disabilitare 2FA." - }, - "file": { - "name": "Nome file", - "deleteAsync": "L'attività di eliminazione verrà eseguita in background.", - "import": "Importa file esterni", - "forceDelete": "Forza l'eliminazione", - "size": "Dimensione", - "uploader": "Caricatore", - "createdAt": "Creato il", - "uploading": "Caricamento in corso", - "unknownUploader": "Sconosciuto", - "uploaderID": "ID caricatore", - "searchFileName": "Cerca nome file", - "storagePolicy": "Politica di archiviazione", - "selectTargetUser": "Seleziona utente di destinazione", - "importTaskCreated": "Attività di importazione creata, puoi visualizzarne lo stato in Attività - Generale.", - "manuallyPathOnly": "La politica di archiviazione selezionata supporta solo l'immissione manuale del percorso.", - "selectFolder": "Seleziona cartella", - "importExternalFolder": "Importa cartelle esterne", - "importExternalFolderDes": "Puoi importare file e strutture di cartelle esistenti dalla tua politica di archiviazione in Cloudreve. L'operazione di importazione non occuperà spazio di archiviazione fisico aggiuntivo, ma sottrarrà comunque la quota di spazio di archiviazione utilizzata dall'utente come di consueto. L'importazione verrà interrotta quando la quota non è sufficiente.", - "storagePolicyDes": "Seleziona la policy di archiviazione in cui sono attualmente archiviati i file da importare.", - "targetUser": "Utente di destinazione", - "targetUserDes": "Seleziona il file system dell'utente in cui vuoi importare i file, puoi cercare gli utenti per soprannome o email.", - "srcFolderPath": "Percorso della cartella di origine", - "select": "Seleziona", - "selectSrcDes": "Il percorso della cartella da importare sul lato storage.", - "dstFolderPath": "Percorso della cartella di destinazione", - "dstFolderPathDes": "Percorso nel file system dell'utente per contenere tutti i file importati.", - "recursivelyImport": "Importa ricorsivamente", - "recursivelyImportDes": "Importare ricorsivamente tutte le sottocartelle della cartella.", - "createImportTask": "Crea attività di importazione", - "unlink": "Scollega (mantieni il file fisico)" - }, - "share": { - "deleted": "Condivisione eliminata.", - "objectName": "Nome oggetto", - "views": "Viste", - "downloads": "Downloads", - "price": "Prezzo", - "autoExpire": "Scadenza automatica", - "owner": "Proprietario", - "createdAt": "Creato il", - "public": "Pubblico", - "private": "Privato", - "afterNDownloads":"Dopo {{num}} download.", - "none": "Nessuno", - "srcType": "Tipo di oggetto sorgente", - "folder": "Cartella", - "file": "File" - }, - "task": { - "taskDeleted": "Attività eliminata.", - "howToConfigAria2": "Come configurare il download remoto?", - "srcURL": "URL di origine", - "node": "Nodo distribuito", - "createdBy": "Creato da", - "ready": "Pronto", - "downloading": "Scaricamento in corso", - "paused": "In pausa", - "seeding": "Seeding", - "error": "Errore", - "finished": "Finito", - "canceled": "Annullato/Interrotto", - "unknown": "Sconosciuto", - "aria2Des": "Il download remoto di Cloudreve supporta una modalità decentralizzata master-slave. Puoi configurare più nodi slave Cloudreve che possono essere utilizzati per gestire attività di download remoto, distribuendo la pressione sul nodo master. Naturalmente, puoi anche configurare per gestire download remoti solo sul nodo master, che è il modo più semplice.", - "masterAria2Des": "Se hai bisogno di abilitare solo i download remoti sul nodo master, <0>fai clic qui per modificare il nodo master.", - "slaveAria2Des": "Se desideri distribuire attività di download remoto su nodi slave, <0>fai clic qui per aggiungere e configurare un nuovo nodo.", - "editGroupDes": "Quando aggiungi più nodi che possono essere utilizzati per i download remoti, il nodo master invierà richieste di download remoto a questi nodi a turno per l'elaborazione. Potrebbe anche essere necessario <0>andare qui per abilitare autorizzazioni di download remoto per i gruppi corrispondenti.", - "lastProgress": "Ultimo progresso", - "errorMsg": "Messaggio di errore" - } -} diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index 6121535..1e0364a 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -1,5 +1,20 @@ { "login": { + "lastStep": "最后一步", + "siginToYourAccount": "登录你的账号", + "createNewAccount": "创建新账号", + "enterPassword": "请输入密码", + "enterPasswordHint": "请输入账号 {{email}} 对应的密码", + "paswordlessHint": "账号 {{email}} 为无密码账户,请选择下列方式认证:", + "noAccountSignupNow": "还没有账号?<0>立即注册", + "haveAccountSignInNow": "已有账号?<0>立即登录", + "privacyPolicy": "隐私政策", + "termOfUse": "使用条款", + "signupHint": "你输入的账户 {{email}} 不存在,是否立即注册?", + "accountNotFoundHint": "你输入的账户 {{email}} 不存在。", + "or": "或者", + "selectAccountToUse": "选择要使用的账号", + "useOtherAccount": "使用其他账号", "email": "电子邮箱", "password": "密码", "captcha": "验证码", @@ -7,9 +22,9 @@ "signIn": "登录", "signUp": "注册", "signUpAccount": "注册账号", - "useFIDO2": "使用外部验证器登录", + "useFIDO2": "使用通行密钥器登录", "usePassword": "使用密码登录", - "forgetPassword": "忘记密码", + "forgetPassword": "忘记密码?", "2FA": "二步验证", "input2FACode": "请输入 6 位二步验证代码", "passwordNotMatch": "两次密码输入不一致", @@ -26,23 +41,47 @@ "success": "登录成功", "signUpSuccess": "注册成功", "activateSuccess": "激活成功", - "accountActivated": "您的账号已被成功激活。", + "accountActivated": "您的账号已被成功激活", "title": "登录 {{title}}", "sinUpTitle": "注册 {{title}}", "activateTitle": "邮件激活", "activateDescription": "一封激活邮件已经发送至您的邮箱,请访问邮件中的链接以继续完成注册。", "continue": "下一步", + "back": "上一步", "logout": "退出登录", "loggedOut": "您已退出登录", "clickToRefresh": "点击刷新验证码" }, "navbar": { + "notBefore": "不早于", + "notAfter": "不晚于", + "minimum": "最小", + "maximum": "最大", + "fileSize": "文件大小", + "searchBase": "搜索路径", + "searchInBase": "搜索 <0>", + "conditionDuplicate": "条件已存在", + "fileType": "文件类型", + "addCondition": "添加条件", + "notNameOpOr": "需包含全部关键词", + "caseFolding": "忽略大小写", + "keywords": "关键字", + "fileNameKeywordsHelp": "输入后按回车键添加关键字", + "advancedSearch": "高级搜索", + "searchFilesTitle": "搜索文件", + "searchIn": "搜索 <0>{{keywords}}", + "recentlyViewed": "最近浏览", + "searchFiles": "搜索文件...", + "showMore": "更多", "myFiles": "我的文件", + "hisFiles": "他的文件", + "trash": "回收站", + "sharedWithMe": "与我共享", "myShare": "我的分享", "remoteDownload": "离线下载", - "connect": "连接", - "taskQueue": "任务队列", - "setting": "个人设置", + "connect": "连接与挂载", + "taskQueue": "后台任务", + "setting": "设置", "videos": "视频", "photos": "图片", "music": "音乐", @@ -64,39 +103,173 @@ "notLoginIn": "未登录", "visitor": "游客", "objectsSelected": "{{num}} 个对象", - "searchPlaceholder": "搜索...", - "searchInFiles": "在我的文件中搜索 <0>{{name}}", - "searchInFolders": "在当前目录中搜索 <0>{{name}}", - "searchInShares": "在全站分享中搜索 <0>{{name}}", + "searchPlaceholder": "按下 <0>/ 开始搜索", "backToHomepage": "返回主页", - "toDarkMode": "切换到深色模式", - "toLightMode": "切换到浅色模式", + "darkModeSwitch": "设置黑暗模式", + "toDarkMode": "黑暗", + "toLightMode": "浅色", "myProfile": "个人主页", - "dashboard": "管理面板", - "exceedQuota": "您的已用容量已超过容量配额,请尽快删除多余文件" + "dashboard": "管理面板" }, "fileManager": { + "shareWithMeEmpty": "没有找到别人的分享", + "shareWithMeEmptyDes": "如需要在此看到别人的分享,请在访问别人分享链接时,在右上角将快捷方式保存到你的文件中的任意位置。", + "selectAll": "全选", + "selectNone": "取消选择", + "invertSelection": "反选", + "imageSize": "图片尺寸", + "focalLength": "焦距", + "columnExisted": "列已存在", + "metadataColumn": "元数据 ({{metadata}})", + "column": "列", + "listColumnSetting": "列设置", + "addColumn": "添加列", + "failedLoadPreview": "预览加载失败", + "recursiveLimitReached": "搜索深度达到上限", + "recursiveLimitReachedDes": "系统已停止搜索更深层的目录,请尝试缩小搜索目录的范围。", + "searchConditions": "{{num}} 个条件", + "createDate": "创建日期", + "updatedDate": "修改日期", + "cameraMake": "相机制造商", + "cameraModel": "相机型号", + "lensModel": "镜头型号", + "lensMake": "镜头制造商", + "metadataKey": "键", + "metadataValue": "值", + "metadata": "元数据", + "symbolicFile": "快捷方式", + "relocation": "转移存储策略", + "downloadingFile": "正在下载 “{{name}}”, 请不要关闭本页面...", + "mountOwner": "只有当前目录的所有者可以挂载策略", + "uploading": "上传中", + "noActionsCanBeDone": "没有可以进行的操作", + "newFileName": "新文件.{{ext}}", + "newDocumentType": "{{display_name}} (.{{ext}})", + "text": "文本", + "diagram": "图表", + "whiteboard": "白板", + "selectApplications": "选择应用...", + "newlyCreatedFolder": "新建文件夹", + "expandAllApp": "展开所有应用", + "epubViewer": "ePub 阅读器", + "googledocs": "Google Docs 在线阅读器", + "m365viewer": "Microsoft Office 在线阅读器", + "pdfViewer": "PDF 阅读器", + "viewerFileSizeWarning": "打开的文件大小 ({{file_size}}) 超过了 {{app}} 的限制 ({{max}}),可能无法正常工作。", + "testSubtitleStyle": "测试字幕样式 AaBbCc", + "color": "颜色", + "fontSize": "字体大小", + "disableSubtitle": "禁用字幕", + "noSubtitle": "没有在当前目录下找到 ASS/SRT/VTT 字幕文件。", + "subtitleStyles": "字幕样式", + "subtitles": "字幕", + "markdownEditor": "Markdown 编辑器", + "saveSuccess": "在 {{time}} 保存成功", + "drawioLng": "zh", + "charset": "编码", + "textType": "文本类型", + "fileSaved": "文件已保存", + "failedToLoadFile": "文件加载失败: {{msg}}", + "monacoEditor": "Monaco 代码编辑器", + "preparingOpenFile": "正在准备打开文件...", + "openWithDescription": "选择一个应用打开 .{{ext}} 文件。", + "openWith": "打开方式", + "readOnly": "只读", + "save": "保存", + "noMoreImages": "当前页面无可浏览图像", + "imageViewer": "图片查看器", + "logFileDeleteShare": "删除分享链接", + "logFileEditShare": "编辑分享链接", + "deleteShareWarning": "确定要删除此分享链接吗?", + "edit": "编辑", + "editAndReactivate": "编辑并重新激活", + "yes": "是", + "no": "否", + "permanentValid": "永久有效", + "manageShares": "管理分享链接", + "deleteVersionWarning": "确定要删除此版本吗?此操作无法撤销。", + "setAsCurrent": "设为当前版本", + "current": "[当前版本]", + "createdBy": "创建者", + "manageVersions": "管理版本", + "livePhoto": "Live Photo", + "version": "版本", + "actions": "操作", + "versionEntity": "文件数据和历史版本", + "data": "数据", + "owned": "拥有此文件", + "ownedSymbolic": "拥有此快捷方式", + "expires": "过期时间", + "originalLocation": "原始位置", + "descendant": "子对象", + "folderChildren": "{{files}} 个文件,{{folders}} 个文件夹", + "moreThan": "大于 {{text}}", + "calculate": "计算", + "unset": "未设置", + "folder": "文件夹", + "file": "文件", + "symbolicLink": "快捷方式 ({{srcType}})", + "type": "类型", + "storageUsed": "占用空间", + "location": "位置", + "basicInfo": "基本信息", + "format": "格式", + "duration": "时长", + "artist": "艺术家", + "album": "专辑", + "title": "标题", + "resolution": "分辨率", + "takenAt": "拍摄时间", + "software": "软件", + "copyright": "作者", + "exposureBias": "曝光补偿", + "flash": "闪光灯", + "copyToClipboard": "复制到剪切板", + "searchSomething": "搜索 \"{{text}}\"...", + "iso": "ISO", + "exposureValue": "{{num}} 秒", + "exposure": "曝光", + "aperture": "光圈", + "mediaInfo": "媒体信息", + "details": "详情", + "activity": "活动", + "goToSharedLink": "转到分享链接", + "saveShortcut": "保存分享为快捷方式", + "customizeIcon": "自定义图标", + "tags": "标签", + "apply": "应用", + "customizeColor": "自定义颜色", + "folderColor": "文件夹颜色", + "restore": "还原", + "unpin": "取消固定", + "youDontHaveReadPermissionToThisFile": "你没有权限读取此内容", + "sharedWithOthers": "与他人分享", + "new": "新建", "open": "打开", - "openParentFolder": "打开所在目录", + "openParentFolder": "转到所在目录", "download": "下载", "batchDownload": "打包下载", "share": "分享", "rename": "重命名", + "organize": "整理", + "pin": "固定到侧边栏", + "pinAlias": "展示别名", + "optional": "可选", "move": "移动", "delete": "删除", "moreActions": "更多操作", "refresh": "刷新", - "compress": "压缩", + "createArchive": "创建压缩文件", "newFolder": "创建文件夹", "newFile": "创建文件", "showFullPath": "显示路径", "listView": "列表", - "gridViewSmall": "小图标", - "gridViewLarge": "大图标", + "gridView": "网格", + "galleryView": "画廊", "paginationSize": "分页大小", "paginationOption": "{{option}} / 页", "noPagination": "不分页", - "sortMethod": "排序方式", + "sortMethod": "排序", "sortMethods": { "A-Z": "A-Z", "Z-A": "Z-A", @@ -115,24 +288,22 @@ "backToParentFolder": "上级目录", "folders": "文件夹", "files": "文件", - "listError": ":( 请求时出现错误", + "listError": "请求时出现错误", "dropFileHere": "拖拽文件至此", - "orClickUploadButton": "或点击右下方“上传文件”按钮添加文件", + "orClickUploadButton": "或点击左上方“创建”按钮添加文件", "nothingFound": "什么都没有找到", "uploadFiles": "上传文件", "uploadFolder": "上传目录", "newRemoteDownloads": "离线下载", "enter": "进入", - "getSourceLink": "获取外链", - "getSourceLinkInBatch": "批量获取外链", + "getSourceLink": "获取直链", "createRemoteDownloadForTorrent": "创建离线下载任务", - "decompress": "解压缩", + "extractArchive": "解压缩", "createShareLink": "创建分享链接", "viewDetails": "详细信息", "copy": "复制", "bytes": " ({{bytes}} 字节)", "storagePolicy": "存储策略", - "inheritedFromParent": "跟随父目录", "childFolders": "包含目录", "childFiles": "包含文件", "childCount": "{{num}} 个", @@ -140,12 +311,14 @@ "rootFolder": "根目录", "modifiedAt": "修改于", "createdAt": "创建于", - "statisticAt": "统计于 <1>", - "musicPlayer": "音频播放", + "statisticAt": "统计于", + "musicPlayer": "音频播放器", "closeAndStop": "退出播放", "playInBackground": "后台播放", "copyTo": "复制到", - "copyToDst": "复制到 <0>{{dst}}", + "copyToDst": "复制到 <0>", + "moveTo": "移动到", + "moveToDst": "移动到 <0>", "errorReadFileContent": "无法读取文件内容:{{msg}}", "wordWrap": "自动换行", "pdfLoadingError": "PDF 加载失败:{{msg}}", @@ -156,71 +329,140 @@ "openInExternalPlayer": "用外部播放器打开", "searchResult": "搜索结果", "preparingBathDownload": "正在准备打包下载...", - "preparingDownload": "获取下载地址...", + "preparingDownload": "正在准备下载...", + "browserDownload": "浏览器端下载到本地目录", + "browserDownloadDescription": "由浏览器逐一下载文件结构到你指定到本地目录。", "browserBatchDownload": "浏览器端打包", - "browserBatchDownloadDescription": "由浏览器实时下载并打包,并非所有环境都支持。", + "browserBatchDownloadDescription": "由浏览器实时下载并打包为 Zip 文件,无法下载大于 4GB 的数据。", "serverBatchDownload": "服务端中转打包", - "serverBatchDownloadDescription": "由服务端中转打包并实时发送到客户端下载。", - "selectArchiveMethod": "选择打包下载方式", - "batchDownloadStarted": "打包下载已开始,请不要关闭此标签页", + "serverBatchDownloadDescription": "由服务端中转打包为 Zip 文件并实时发送到客户端下载,不支持分享快捷方式。", + "selectArchiveMethod": "选择批量下载方式", + "batchDownloadStarted": "打包下载已开始,请不要关闭此页面...", "batchDownloadError": "打包遇到错误:{{msg}}", "userDenied": "用户拒绝", - "directoryDownloadReplace": "替换对象", - "directoryDownloadReplaceDescription": "将会替换 {{duplicates}} 等共 {{num}} 个对象。", - "directoryDownloadSkip": "跳过对象", - "directoryDownloadSkipDescription": "将会跳过 {{duplicates}} 等共 {{num}} 个对象。", - "selectDirectoryDuplicationMethod": "重复对象处理方式", + "directoryDownloadReplace": "替换此文件", + "directoryDownloadReplaceDescription": "将会覆盖本地的 “{{name}}”", + "directoryDownloadSkip": "跳过此文件", + "directoryDownloadSkipDescription": "将会跳过下载 “{{name}}”", + "selectDirectoryDuplicationMethod": "文件重名", + "directoryDownloadReplaceAll": "替换此文件和后续所有重名文件", + "directoryDownloadReplaceAllDescription": "将会覆盖本地的 “{{name}}”,并记住选择", + "directoryDownloadSkipAll": "跳过此文件和后续所有重名文件", + "directoryDownloadSkipAllDescription": "将会跳过下载 “{{name}}”,并记住选择", "directoryDownloadStarted": "下载已开始,请不要关闭此标签页", "directoryDownloadFinished": "下载完成,无失败对象", "directoryDownloadFinishedWithError": "下载完成, 失败 {{failed}} 个对象", - "directoryDownloadPermissionError": "无权限操作,请允许读写本地文件" + "directoryDownloadPermissionError": "无权限操作,请允许读写本地文件", + "back": "后退", + "view": "视图", + "layout": "布局", + "thumbnails": "缩略图", + "on": "开启", + "off": "关闭" }, "modals": { - "processing": "处理中...", + "showFileName": "显示文件名", + "archiveFile": "压缩文件", + "cancelDownload": "取消下载", + "always": "始终", + "justOnce": "仅一次", + "quality": "质量", + "saveAsOtherFormat": "另存为其他格式", + "conflictDes1": "文件版本发生冲突,可能的原因是:", + "conflictDes2": "<0>该文件在你打开后被从它处更新了新版本。<1>如果你另存为了新文件名或新位置,可能已有同名文件存在。", + "saveAs": "另存为", + "versionConflict": "版本冲突", + "overwrite": "覆盖", + "editShareLink": "编辑分享链接", + "clearPermissions": "清除权限设置", + "shortcutCreated": "快捷方式已创建", + "createShortcut": "创建快捷方式", + "createShortcutTo": "在 <0> 创建快捷方式", + "targetExisted": "目标已存在", + "users": "用户", + "groups": "用户组", + "resetToDefault": "重置为默认", + "duplicateTag": "标签 \"{{tag}}\" 已存在", + "colorForTag": "自定义新标签颜色", + "enterForNewTag": "按回车键添加新标签", + "manageTags": "管理标签", + "onlyOwner": "只有文件所有者可以强制解锁此文件", + "forceUnlock": "强制解锁", + "forceUnlockAll": "强制解锁全部", + "forceUnlockDes": "强制解锁可能会导致文件状态异常,推荐优先等待文件被主动释放。确定要继续解锁吗?", + "webdav": "WebDAV", + "soft-delete": "移至回收站", + "updateMetadata": "更新元数据", + "upload": "上传", + "moveCopy": "移动或复制", + "view": "查看", + "cannotPerformAction": "不支持移动或复制到此处", + "cannotMoveCopyToChild": "无法移动或复制到子目录", + "copySuccess": "成功复制 {{num}} 个文件", + "moveSuccess": "成功移动 {{num}} 个文件", + "unknownParent": "未知父目录", + "unknownParentDes": "被占用的目录是共享目录的父目录,它不属于你所有", + "lockConflictTitle": "文件被占用", + "lockConflictDescription": "操作无法完成,因为下列文件正在被使用,请稍后重试。 如果你是文件所有者,并且确定文件没有被使用,你可以强制解锁文件并重试。", + "application": "应用", + "errorDetailsTitle": "错误详情", + "processingMoving": "正在移动文件...", + "processingCopying": "正在复制文件...", + "processingRestoring": "正在恢复文件...", + "fileRestored": "已恢复 {{num}} 个文件至原位", "duplicatedObjectName": "新名称与已有文件重复", - "duplicatedFolderName": "文件夹名称重复", + "newNameLengthError": "文件名长度必须在 1~255 个字符之间", + "newNameCharacterError": "文件名不能包含以下字符:\\ / : * ? \" < > |", + "newNameDotError": "文件名不能为 \".\" 或 \"..\"", "taskCreated": "任务已创建", "taskCreateFailed": "{{failed}} 个任务创建失败:{{details}}", "linkCopied": "链接已复制", - "getSourceLinkTitle": "获取文件外链", - "sourceLink": "文件外链", + "getSourceLinkTitle": "获取文件直链", + "sourceLink": "文件直链", "folderName": "文件夹名称", "create": "创建", - "fileName": "文件名称", + "fileName": "文件名", "renameDescription": "输入 <0>{{name}} 的新名称:", "newName": "新名称", - "moveToTitle": "移动至", "moveToDescription": "移动至 <0>{{name}}", "saveToTitle": "保存至", "saveToTitleDescription": "保存至 <0>{{name}}", "deleteTitle": "删除对象", - "deleteOneDescription": "确定要删除 <0>{{name}} 吗?", - "deleteMultipleDescription": "确定要删除这 {{num}} 个对象吗?", + "deleteOneDescription": "确定要将 <0>{{name}} 移至回收站吗?", + "deleteMultipleDescription": "确定要将这 {{num}} 个对象移至回收站吗?", + "deleteOneDescriptionHard": "确定要永久删除 <0>{{name}} 吗?", + "trashRetention": "回收站中的文件会在 <0>{{num}} 后自动删除。", + "deleteMultipleDescriptionHard": "确定要永久删除这 {{num}} 个对象吗?", "newRemoteDownloadTitle": "新建离线下载任务", "remoteDownloadURL": "下载链接", - "remoteDownloadURLDescription": "输入文件下载地址,一行一个,支持 HTTP(s) / FTP / 磁力链", + "remoteDownloadURLDescription": "输入文件下载地址,一行一个", "remoteDownloadDst": "下载至", + "processNode": "处理节点", + "remoteDownloadNodeAuto": "自动分配", "createTask": "创建任务", - "downloadTo": "下载至 <0>{{name}}", + "downloadToDst": "下载至 <0>{{name}}", + "downloadTo": "下载至", "decompressTo": "解压缩至", "decompressToDst": "解压缩至 <0>{{name}}", "defaultEncoding": "缺省", "chineseMajorEncoding": "简体中文常见编码", - "selectEncoding": "选择 ZIP 文件特殊字符编码", + "selectEncoding": "ZIP 文件编码", "noEncodingSelected": "未选择编码方式", "listingFiles": "列取文件中...", "listingFileError": "列取文件时出错:{{message}}", "generatingSourceLinks": "生成外链中...", "noFileCanGenerateSourceLink": "没有可以生成外链的文件", "sourceBatchSizeExceeded": "当前用户组最大可同时为 {{limit}} 个文件生成外链", - "zipFileName": "ZIP 文件名", + "zipFileName": "压缩文件名", "shareLinkShareContent": "我向你分享了:{{name}} 链接:{{link}}", "shareLinkPasswordInfo": " 密码: {{password}}", "createShareLink": "创建分享链接", - "usePasswordProtection": "使用密码保护", + "privateShare": "隐藏分享", + "privateShareDes": "勾选后,其他人无法在你的个人主页看到此分享链接。", + "expireAfterDownload": "下载后自动过期", "sharePassword": "分享密码", "randomlyGenerate": "随机生成", - "expireAutomatically": "自动过期", + "expireAutomatically": "超时自动过期", "downloadLimitOptions": "{{num}} 次下载", "or": "或者", "5minutes": "5 分钟", @@ -229,29 +471,38 @@ "7days": "7 天", "30days": "30 天", "custom": "自定义", - "seconds": "秒", + "minutes": "分钟", "downloads": "次下载", - "downloadSuffix": "后过期", + "expirePrefix": "", + "expireSuffix": "后过期", "allowPreview": "允许预览", "allowPreviewDescription": "是否允许在分享页面预览文件内容", "shareLink": "分享链接", "sendLink": "发送链接", "directoryDownloadReplaceNotifiction": "已覆盖 {{name}}", "directoryDownloadSkipNotifiction": "已跳过 {{name}}", - "directoryDownloadTitle": "下载", - "directoryDownloadStarted": "开始下载 {{name}}", - "directoryDownloadFinished": "下载完成", + "directoryDownloadTitle": "批量下载日志", + "directoryDownloadStarted": "开始下载 “{{name}}”", + "directoryDownloadFinished": "下载完成 “{{name}}”", "directoryDownloadError": "遇到错误:{{msg}}", "directoryDownloadErrorNotification": "下载 {{name}} 遇到错误:{{msg}}", "directoryDownloadAutoscroll": "自动滚动", "directoryDownloadCancelled": "已取消下载", "advanceOptions": "高级选项", - "forceDelete": "强制删除文件", - "forceDeleteDes": "强制删除文件记录,无论物理文件是否被成功删除", - "unlinkOnly": "仅解除链接", + "skipSoftDelete": "彻底删除文件", + "skipSoftDeleteDes": "跳过回收站,直接删除文件", + "unlinkOnly": "保留物理文件", "unlinkOnlyDes": "仅删除文件记录,物理文件不会被删除" }, "uploader": { + "fileCopyName": "副本_", + "overwriteTooltip": "文件重名时覆盖已有文件,只针对新添加的任务有效", + "rename": "使用新文件名重试", + "overwrite": "覆盖已有文件", + "pasteFilesHere": "将文件粘贴到此处", + "clipboardDefaultFileName": "剪贴板 {{date}}.png", + "uploadFromClipboard": "从剪贴板上传", + "uploadList": "上传列表", "fileNotMatchError": "所选择文件与原始文件不符", "unknownError": "出现未知错误:{{msg}}", "taskListEmpty": "没有上传任务", @@ -266,7 +517,7 @@ "progressDescription": "已上传 {{uploaded}} , 共 {{total}} - {{percentage}}%", "progressDescriptionFull": "{{speed}} 已上传 {{uploaded}} , 共 {{total}} - {{percentage}}%", "progressDescriptionPlaceHolder": "已上传 - ", - "uploadedTo": "已上传至 ", + "uploaded": "已上传", "rootFolder": "根目录", "unknownStatus": "未知", "resumed": "断点续传", @@ -277,10 +528,11 @@ "selectAndResume": "选取同样文件并恢复上传", "fileName": "文件名:", "fileSize": "文件大小:", - "sessionExpiredIn": "<0> 过期", + "sessionExpiredIn": "<0>过期", "chunkDescription": "({{total}} 个分片, 每个分片 {{size}})", "noChunks": "(无分片)", - "destination": "存储路径:", + "destination": "存放位置:", + "storagePolicy": "存储策略:", "uploadSession": "上传会话:", "errorDetails": "错误信息:", "uploadSessionCleaned": "上传会话已清除", @@ -322,33 +574,28 @@ "dropFileHere": "松开鼠标开始上传" }, "share": { - "expireInXDays": "{{num}} 天后到期", - "days":"{{count}} day", - "days_other":"{{count}} days", - "expireInXHours": "{{num}} 小时后到期", - "hours":"an hour", - "hours_other":"{{count}} hours", - "createdBy": "此分享由 <0>{{nick}} 创建", + "statistics": "统计", + "expireAt": "<0>过期", + "expireAfterDownloads": "{{downloads}} 次下载后过期", + "somebodyShare": "{{name}} 的分享", + "expiredLink": "已失效的分享", "sharedBy": "<0>{{nick}} 向您分享了 {{num}} 个文件", - "files":"1 file", - "files_other":"{{count}} files", - "statistics": "{{views}} 次浏览 • {{downloads}} 次下载 • {{time}}", - "views":"{{count}} view", - "views_other":"{{count}} views", - "downloads":"{{count}} download", - "downloads_other":"{{count}} downloads", + "files": "1 file", + "files_other": "{{count}} files", + "statisticsViews": "{{views}} 次浏览", + "statisticsDownloads": "{{downloads}} 次下载 ", + "views": "{{count}} view", + "views_other": "{{count}} views", + "downloads": "{{count}} download", + "downloads_other": "{{count}} downloads", "privateShareTitle": "{{nick}} 的加密分享", - "enterPassword": "输入分享密码", + "enterPassword": "分享密码", "continue": "继续", - "shareCanceled": "分享已取消", + "shareCanceled": "分享链接已删除", "listLoadingError": "加载失败", "sharedFiles": "我的分享", - "createdAtDesc": "创建日期由晚到早", - "createdAtAsc": "创建日期由早到晚", - "downloadsDesc": "下载次数由大到小", - "downloadsAsc": "下载次数由小到大", - "viewsDesc": "浏览次数由大到小", - "viewsAsc": "浏览次数由小到大", + "createdAtDesc": "最新", + "createdAtAsc": "最早", "noRecords": "没有分享记录.", "sourceNotFound": "[原始对象不存在]", "expired": "已失效", @@ -367,9 +614,12 @@ "cannotShare": "此文件无法预览", "preview": "预览", "incorrectPassword": "密码不正确", - "shareNotExist": "分享不存在或已过期" + "shareNotExist": "分享不存在或已过期", + "copyLinkToClipboard": "复制链接到剪切板" }, "download": { + "cancelTaskConfirm": "确定要取消此任务吗?", + "saveChanges": "保存更改", "failedToLoad": "加载失败", "active": "进行中", "finished": "已完成", @@ -385,28 +635,95 @@ "selectDownloadingFile": "选择要下载的文件", "cancelTask": "取消任务", "updatedAt": "更新于:", - "uploaded": "上传大小:", - "uploadSpeed": "上传速度:", - "InfoHash": "InfoHash:", + "uploaded": "上传大小", + "uploadSpeed": "上传速度", + "InfoHash": "InfoHash", "seederCount": "做种者:", "seeding": "做种中:", "downloadNode": "节点:", "isSeeding": "是", "notSeeding": "否", "chunkSize": "分片大小:", - "chunkNumbers": "分片数量:", + "chunkNumbers": "分片数量", "taskDeleted": "删除成功", "transferFailed": "文件转存失败", "downloadFailed": "下载出错:{{msg}}", "canceledStatus": "已取消", "finishedStatus": "已完成", "pending": "已完成,转存排队中", - "transferring": "已完成,转存中", + "transferring": "转存中", "deleteRecord": "删除记录", "createdAt": "创建日期:" }, "setting": { - "avatarUpdated": "头像已更新,刷新后生效", + "noAuthenticator": "添加通行密钥以使用人脸、指纹或 USB 密钥登录账号", + "neverUsed": "从未使用过", + "usedAt": "上次使用于 <0>", + "passkeyName": "{os} 上的 {browser}", + "versionRetentionMax": "最大版本数量,0 表示无限制", + "versionRetentionEnabledExt": "启用的文件扩展名", + "versionRetentionEnabledExtDes": "按回车键添加,留空时会对所有文件启用", + "enableVersionRetention": "启用版本保留", + "enableVersionRetentionDes": "启用后,对于符合条件的文件,系统会保留其的历史版本", + "versionRetention": "版本保留", + "languageDes": "设置应用展示语言和首选邮件语言", + "timezoneDes": "设置展示时区,默认跟随系统时区", + "nickNameDes": "用于公开展示的名字,可使用真实姓名或昵称", + "cropAvatar": "裁剪头像", + "preference": "偏好", + "accountCreatedAt": "创建于 <0>", + "shoeQr": "显示", + "deviceNothing": "当前用户组不支持 WebDAV", + "connectionInfo": "连接信息", + "proxyTooltip": "服务端代理所有文件下载请求", + "readonlyTooltip": "用户只能通过此账号读取文件", + "rootFolderIn": "选择 <0>", + "createWebDavAccount": "创建 WebDAV 账号", + "editWebDavAccount": "编辑 {{name}}", + "seeding": "做种中", + "awaitSeeding": "等待做种", + "awaitSeedingDes": "等待下载任务做种完成。", + "downloadTransferDes": "将文件转存到目的地。", + "downloadDes": "下载指定的文件。", + "retryErrorHistory": "历史重试错误", + "retryCount": "重试次数", + "resumeAt": "下次恢复执行", + "executeDuration": "执行净耗时", + "input": "输入", + "output": "输出", + "suspended": " (已挂起)", + "updatedAt": "更新于", + "taskDetails": "任务详情", + "partialSuccessWarning": "有 {{num}} 个对象处理失败,已将其跳过。", + "sendTask": "发送任务", + "sendTaskDes": "将任务发送到处理节点。", + "downloaded": "已下载", + "extractedFiles": "已解压文件数量", + "extractedFilesSize": "已解压文件大小", + "extractingFiles": "解压文件", + "extractingFilesDes": "将所有文件解压到指定目录。", + "downloadingZip": "获取压缩文件", + "downloadingZipDes": "将压缩文件下载到临时工作区。", + "progressNotAvailable": "进度信息尚未可用", + "uploadedSize": "转存文件", + "archivedFiles": "已处理文件数量", + "transferredFiles": "已转存文件数量", + "archivedFilesSize": "已处理文件大小", + "createArchiveFinishing": "提交新增文件更改。", + "indexForArchiveDes": "检索所有待压缩文件。", + "prepare": "准备", + "preparingWorkspaceDes": "准备临时工作区。", + "compressFiles": "创建压缩文件", + "compressFilesDes": "将文件压缩到临时工作区。", + "uploadArchiveFileDes": "将压缩文件转存到目的地。", + "uploadWorker": "上传线程 #{{num}}", + "queueToStart": "排队开始", + "indexingFiles": "检索文件", + "indexingFilesDes": "检索所有待转移文件,并将其锁定。", + "transferring": "转存", + "committingChanges": "提交更改", + "autoRefresh": "自动刷新", + "avatarUpdated": "头像已更新,最新头像展示可能有延迟", "nickChanged": "昵称已更改,刷新后生效", "settingSaved": "设置已保存", "themeColorChanged": "主题配色已更换", @@ -416,7 +733,7 @@ "nickname": "昵称", "group": "用户组", "regTime": "注册时间", - "privacyAndSecurity": "安全隐私", + "security": "密码和安全", "profilePage": "个人主页", "accountPassword": "登录密码", "2fa": "二步验证", @@ -425,7 +742,7 @@ "appearance": "个性化", "themeColor": "主题配色", "darkMode": "黑暗模式", - "syncWithSystem": "跟随系统", + "syncWithSystem": "系统", "fileList": "文件列表", "timeZone": "时区", "webdavServer": "连接地址", @@ -437,16 +754,16 @@ "originalPassword": "原密码", "enable2FA": "启用二步验证", "disable2FA": "关闭二步验证", - "2faDescription": "请使用任意二步验证APP或者支持二步验证的密码管理软件扫描左侧二维码添加本站。扫描完成后请填写二步验证APP给出的6位验证码以开启二步验证。", - "inputCurrent2FACode": "请验证当前二步验证代码。", + "2faDescription": "请使用任意二步验证 APP 或者支持二步验证的密码管理软件扫描二维码添加本站。扫描完成后请填写二步验证 APP 给出的 6 位验证码以开启二步验证。", + "inputCurrent2FACode": "输入当前二步验证 APP 给出的 6 位验证码:", "timeZoneCode": "IANA 时区名称标识", "authenticatorRemoved": "凭证已删除", "authenticatorAdded": "验证器已添加", "browserNotSupported": "当前浏览器或环境不支持", "removedAuthenticator": "删除凭证", "removedAuthenticatorConfirm": "确定要吊销这个凭证吗?", - "addNewAuthenticator": "添加新验证器", - "hardwareAuthenticator": "外部认证器", + "addNewAuthenticator": "添加新凭证", + "hardwareAuthenticator": "通行密钥", "copied": "已复制到剪切板", "pleaseManuallyCopy": "当前浏览器不支持,请手动复制", "webdavAccounts": "WebDAV 账号管理", @@ -455,16 +772,17 @@ "rootFolder": "相对根目录", "createdAt": "创建日期", "action": "操作", - "readonlyOn": "开启只读", - "readonlyOff": "关闭只读", - "useProxyOn": "开启反代", - "useProxyOff": "关闭反代", + "readonlyOn": "只读", + "readonlyOff": "读写", + "proxy": "反向代理", + "none": "无", + "proxied": "已代理", "delete": "删除", "listEmpty": "没有记录", "createNewAccount": "创建新账号", "taskType": "任务类型", "taskStatus": "状态", - "lastProgress": "最后进度", + "taskProgress": "任务进度", "errorDetails": "错误信息", "queueing": "排队中", "processing": "处理中", @@ -479,7 +797,6 @@ "compressing": "压缩中", "decompressing": "解压缩中", "downloading": "下载中", - "transferring": "转存中", "indexing": "索引中", "listing": "插入中", "allShares": "全部分享", @@ -490,13 +807,19 @@ "downloadNumber": "下载次数", "viewNumber": "浏览次数", "language": "语言", - "iOSApp": "iOS 客户端", - "connectByiOS": "通过 iOS 设备连接到 <0>{{title}}", - "downloadOurApp": "下载并安装我们的 iOS 应用:", - "fillInEndpoint": "使用我们的 iOS 应用扫描下方二维码(其他扫码应用无效):", - "loginApp": "完成绑定,你可以开始使用 iOS 客户端了。如果扫码绑定遇到问题,你也可以尝试手动输入用户名和密码登录。", - "aboutCloudreve": "关于 Cloudreve", - "githubRepo": "GitHub 仓库", - "homepage": "主页" + "iOSApp": "iOS/iPadOS 客户端", + "connectByiOS": "通过 iOS/iPadOS 设备连接到 <0>{{title}}", + "downloadOurApp": "下载并安装我们的应用:", + "fillInEndpoint": "使用应用扫描下方二维码(其他扫码应用无效):", + "loginApp": "完成绑定,你可以开始使用客户端了。如果扫码绑定遇到问题,你也可以尝试手动输入用户名和密码登录。", + "relocateFileTo": "将 <0>{{more}} 的存储策略转移至 {{policy}}", + "extractFileTo": "将 <0>{{more}} 解压缩至 <1>", + "createArchiveTo": "将 <0>{{more}} 打包至 <1>" + }, + "vas": { + "points": "积分", + "quota": "容量配额", + "used": "已使用 - {{size}}", + "total": "总容量 - {{size}}" } -} +} \ No newline at end of file diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index faa1a54..0452017 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -3,20 +3,34 @@ "unknownError": "未知错误", "errLoadingSiteConfig": "无法加载站点配置:", "newVersionRefresh": "当前页面有新版本可用,准备刷新。", - "errorDetails": "错误详情", + "errorDetails": "详情", "renderError": "页面渲染出现错误,请尝试刷新此页面。", "ok": "确定", "cancel": "取消", "select": "选择", "copyToClipboard": "复制", "close": "关闭", + "dismiss": "关闭", "intlDateTime": "{{val, datetime}}", + "seconds": "s 秒", + "minutes": "m 分 s 秒", + "hours": "H 小时 m 分", + "days": "{{d}} 天", "timeAgoLocaleCode": "zh_CN", "forEditorLocaleCode": "zh-CN", "artPlayerLocaleCode": "zh-cn", + "requestID": "请求 ID: {{id}}", + "object": "对象", + "error": "错误", + "areYouSure": "确认", + "incorrectSizeInput": "不符合尺寸限制", + "of": "共", + "rowsPerPage": "每页行数", + "custom": "自定义", + "enter": "输入", "errors": { "401": "请先登录", - "403": "此操作被禁止", + "403": "你没有权限执行此操作", "404": "资源不存在", "409": "发生冲突 ({{message}})", "40001": "输入参数有误 ({{message}})", @@ -35,7 +49,7 @@ "40017": "该账号已被封禁", "40018": "该账号未激活", "40019": "此功能未启用", - "40020": "用户邮箱或密码错误", + "40020": "凭证无效或过期", "40021": "用户不存在", "40022": "验证代码不正确", "40023": "登录会话不存在", @@ -68,10 +82,16 @@ "40069": "密码不正确", "40070": "此分享无法预览", "40071": "签名无效", + "40073": "文件被占用", + "40074": "所选文件数量超出限制", + "40079": "超出最大遍历文件数限制,请缩小操作范围", + "40081": "操作未完全成功", + "40082": "只有文件所有者可以执行此操作", + "40080": "用户邮箱或密码错误", "50001": "数据库操作失败 ({{message}})", "50002": "URL 或请求签名失败 ({{message}})", "50004": "I/O 操作失败 ({{message}})", - "50005": "內部错误 ({{message}})", + "50005": "内部错误 ({{message}})", "50010": "目标节点不可用", "50011": "文件元信息查询失败" } diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 58504da..e8e7950 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -1,7 +1,7 @@ { - "errors":{ + "errors": { "40036": "默认存储策略无法删除", - "40037": "有 {{message}} 个文件仍在使用此存储策略,请先删除这些文件", + "40037": "有文件 Blob 仍在使用此存储策略,请先删除这些文件 Blob", "40038": "有 {{message}} 个用户组绑定了此存储策略,请先解除绑定", "40040": "无法对系统用户组执行此操作", "40041": "有 {{message}} 位用户仍属于此用户组,请先删除这些用户或者更改用户组", @@ -10,6 +10,7 @@ "40046": "无法对主机节点执行此操作", "40060": "从机无法向主机发送回调请求,请检查主机端 参数设置 - 站点信息 - 站点URL设置,并确保从机可以连接到此地址 ({{message}})", "40061": "Cloudreve 版本不一致 ({{message}})", + "40086": "节点正在被以下存储策略使用:{{message}}", "50008": "设置项更新失败 ({{message}})", "50009": "跨域策略添加失败" }, @@ -17,251 +18,398 @@ "summary": "面板首页", "settings": "参数设置", "basicSetting": "站点信息", - "publicAccess": "注册与登录", "email": "邮件", "transportation": "传输与通信", "appearance": "外观", "image": "图像与预览", "captcha": "验证码", "storagePolicy": "存储策略", - "nodes": "离线下载节点", + "nodes": "节点", "groups": "用户组", "users": "用户", "files": "文件", + "entities": "文件 Blob", "shares": "分享", - "tasks": "持久任务", + "tasks": "后台任务", "remoteDownload": "离线下载", "generalTasks": "常规任务", "title": "仪表盘", - "dashboard": "Cloudreve 仪表盘" + "dashboard": "Cloudreve 仪表盘", + "userSession": "用户会话", + "fileSystem": "文件系统", + "mediaProcessing": "媒体处理", + "queue": "队列", + "events": "事件", + "server": "服务器" }, "summary": { - "newsletterError": "Cloudreve 公告加载失败", + "generatedAt": "生成于 <0>", "confirmSiteURLTitle": "确定站点URL设置", - "siteURLNotSet": "您尚未设定站点URL,是否要将其设定为当前的 {{current}} ?", - "siteURLNotMatch": "您设置的站点URL与当前实际不一致,是否要将其设定为当前的 {{current}} ?", - "siteURLDescription": "此设置非常重要,请确保其与您站点的实际地址一致。你可以在 参数设置 - 站点信息 中更改此设置。", + "siteURLNotMatch": "你设置的站点 URL 并未包含当前的 {{current}},是否要更改设置?", + "setAsPrimary": "设置为主要站点 URL", + "setAsPrimaryDes": "将 {{current}} 设置为主要站点 URL,用于与外部服务通信和接受回调,请使用能被公网访问的 URL。", + "setAsSecondary": "添加到备选站点 URL", + "setAsSecondaryDes": "将 {{current}} 添加到备选站点 URL,Cloudreve 会根据用户实际访问的 URL 自动选择是否使用。", + "siteURLDescription": "此设置非常重要,请确保其与你站点的实际地址一致。你可以在 参数设置 - 站点信息 中更改此设置。", "ignore": "忽略", "changeIt": "更改", "trend": "趋势", "summary": "总计", "totalUsers": "注册用户", - "totalFiles": "文件总数", - "publicShares": "公开分享总数", - "privateShares": "私密分享总数", + "totalFiles": "文件", + "shareLinks": "分享链接", + "totalBlobs": "文件 Blob", "homepage": "主页", "documents": "文档", "forum": "讨论社区", "forumLink": "https://forum.cloudreve.org", - "telegramGroup": "Telegram 群组", - "telegramGroupLink": "https://t.me/cloudreve_official", - "buyPro": "购买捐助版", + "discordCommunity": "Discord 社群", + "buyPro": "升级到 Pro", "publishedAt": "发表于 <0>", - "newsTag": "notice" + "newsTag": "notice", + "licenseExpireAt": "授权有效期", + "permanentLicense": "永久", + "offlineLicenseExpireAy": "离线授权过期日期", + "offlineLicenseDes": "在连接网络的情况下,Cloudreve 会在过期前自动更新离线授权", + "licensedDomains": "授权的域名", + "renew": "更新离线授权", + "manageLicense": "管理授权", + "volPurchase": "客户端 VOL 授权需要单独在 <0>授权管理面板 购买。VOL 授权允许你的用户免费使用 <1>Cloudreve iOS 客户端 连接到你的站点,无需用户再付费订阅 iOS 客户端。购买授权后请点击下方更新授权。", + "iosVol": "iOS 客户端批量授权 (VOL)", + "refreshSuccessfully": "刷新成功", + "manualRefresh": "手动刷新离线授权", + "manualRefreshDes": "自动刷新离线授权失败,请尝试登录 <0>授权管理面板 获取最新的离线授权,将其粘贴在下方。" + }, + "queue": { + "queueName_io_intense": "IO 密集型", + "queueName_io_intenseDes": "用于处理大量 IO 操作的队列,包括: 转移存储策略、解压缩、压缩。", + "queueName_media_meta": "媒体元数据提取", + "queueName_media_metaDes": "用于提取媒体文件的元数据。", + "queueName_recycle": "Blob 回收", + "queueName_recycleDes": "用于删除过期的文件 Blob。", + "queueName_thumb": "缩略图生成", + "queueName_thumbDes": "用于为文件生成缩略图。", + "queueName_remote_download": "离线下载", + "queueName_remote_downloadDes": "用于处理离线下载任务。", + "failed": "失败 ({{count}})", + "success": "成功 ({{count}})", + "suspending": "挂起 ({{count}})", + "busyWorker": "处理中 ({{count}})", + "submited": "已提交 ({{count}})", + "editQueueSettings": "编辑队列设置 - {{name}}", + "workerNum": "工作线程数", + "workerNumDes": "任务队列最多并行执行的任务数。", + "maxExecution": "最大执行时间", + "maxExecutionDes": "任务最大执行时间(秒),超过此时间任务将被终止。", + "backoffFactor": "退避因子", + "backoffFactorDes": "任务重试时间间隔的增长因子。", + "backoffMaxDuration": "最大退避时间", + "backoffMaxDurationDes": "任务重试的最大退避时间(秒)。", + "maxRetry": "最大重试次数", + "maxRetryDes": "任务失败后的最大重试次数。", + "retryDelay": "重试延迟", + "retryDelayDes": "任务重试的初始延迟时间(秒)。" }, "settings": { + "resetUrl": "重置链接", + "exceedToleranceDays": "设置的封禁宽容天数", + "activateUrl": "激活链接", + "domainNotLicensed": "域名未授权", + "domainNotLicensedDes": "你设置的站点 URL 中包含未授权的域名,请在 <0>授权管理面板 中添加此子域名,然后点击下方按钮更新授权后重试。", + "showSettings": "显示设置", + "perPage": "每页 {{num}} 条", + "noNodes": "没有可用的节点。", + "extractMediaMeta": "媒体信息提取", + "extractMediaMetaDes": "提取媒体文件的元数据以用于展示和搜索。默认情况下,非本机存储策略只会使用“存储策略原生”方式提取。你可以在存储策略设置页面开启“提取器代理”功能扩展第三方存储策略的缩略图能力。", + "exif": "EXIF", + "exifDes": "从图片文件中提取 EXIF 元数据以用于展示和搜索。", + "music": "音乐元数据", + "musicDes": "从音乐文件中提取元数据,包括标题、艺术家、专辑等信息。", + "ffprobe": "FFprobe", + "ffprobeDes": "使用 FFprobe 从视频和音频文件中提取元数据。", + "maxSizeLocal": "最大文件大小(本地存储)", + "maxSizeLocalDes": "当文件存储在本地存储策略时,允许提取元数据的最大文件大小,填写为 0 时不限制。", + "maxSizeRemote": "最大文件大小(远程存储)", + "maxSizeRemoteDes": "当文件存储在第三方存储策略时,允许提取元数据的最大文件大小,填写为 0 时不限制。", + "exifBruteForce": "必要时使用暴力搜索", + "exifBruteForceDes": "启用后,如果在标准头部位置找不到 EXIF 数据,将扫描整个文件以查找 EXIF 数据。这可能会增加处理时间,但可以找到非标准位置的 EXIF 数据。", + "musicCover": "歌曲封面", + "musicCoverDes": "提取音频文件中的专辑封面, 支持 ID3 (v1, 2.2, 2.3, 2.4) 元数据容器。这一生成器依赖于任一其他图像生成器(Cloudreve 内置 或 VIPS)。", + "notAppliedToNativeGenerator": "{{prefix}}不适用于存储策略原生生成器。", + "fileBlobMargin": "文件 Blob 临时 URL 缓存冗余(秒)", + "fileBlobMarginDes": "当相同的文件 Blob 被多次请求时,如果最初的 URL 剩余有效期大于冗余时长,相同的 URL 会被复用。", + "fileBlobTimeout": "文件 Blob 临时 URL 有效期", + "fileBlobTimeoutDes": "限制用户打开或下载文件时,所获得的临时链接的有效期,只针对本机存储策略、WebDAV 或经 Cloudreve 代理的文件下载。", + "wopiSessionTimeout": "WOPI 会话有效期(秒)", + "wopiSessionTimeoutDes": "限制用户使用 WOPI 编辑文件时,单个会话的有效期,过期后用户需要重新从 Cloudreve 打开文件。", + "oauthRefresh": "OAuth 存储策略凭证刷新间隔", + "oauthRefreshDes": "设定多久刷新需要使用 OAuth 的存储策略(OneDrive)的凭证,可以避免长期未使用存储策略导致的凭证过期", + "transitParallelNum": "中转最大并行传输", + "transitParallelNumDes": "当单个服务端文件中转任务包含多个文件时,最大并行上传的数量。", + "failedChunkRetry": "分片错误最大重试", + "failedChunkRetryDes": "分片上传失败后重试的最大次数,只适用于服务端上传或中转。", + "cacheChunks": "缓存流式分片文件以用于重试", + "cacheChunksDes": "开启后,流式中转分片上传时会将分片数据缓存在系统临时目录,以便用于分片上传失败后的重试;\n 关闭后,流式中转分片上传不会额外占用硬盘空间,但分片上传失败后整个上传会立即失败。", + "folderPropsTimeout": "目录统计信息有效期(秒)", + "folderPropsTimeoutDes": "用户计算目录统计信息(大小,包含文件数量等)时,结果缓存的有效期。", + "slaveAPIExpiration": "从机 API 签名有效期(秒)", + "slaveAPIExpirationDes": "主机访问从机 API 时使用的签名有效期。", + "uploadSessionTimeout": "上传会话有效期 (秒)", + "uploadSessionDes": "在上传会话有效期内,对于支持的存储策略,用户可以断点续传未完成的任务。最大可设定的值受限于不同存储策略服务商的规则。", + "archiveTimeout": "服务端打包下载会话有效期 (秒)", + "advanceOptions": "高级设置", + "emojiOptions": "Emoji 选项", + "addCategorize": "添加分类", + "category": "分类", + "searchQuery": "文件分类查询", + "importWopi": "导入 WOPI 应用设置", + "wopiEndpoint": "WOPI Discovery Endpoint", + "wopiDes": "通过对接支持 WOPI 协议的在线文档处理系统,扩展 Cloudreve 的文档在线预览和编辑能力。请在此填写 WOPI 服务发现地址,比如 https://example.com/hosting/discovery", + "embeddedWebpageViewer": "嵌入网页式应用", + "wopiViewer": "WOPI 协议应用", + "ext": "扩展名", + "invalidWopiActionMapping": "WOPI Action 映射无效", + "woapiActionMapping": "WOPI Action 映射", + "drawioHost": "DrawIO 实例", + "drawioHostDes": "你可以填写自建实例的地址。", + "openInNew": "在新窗口直接打开", + "openInNewDes": "勾选后,会直接弹出新标签打开此应用。", + "maxSize": "最大文件大小", + "maxSizeDes": "此应用支持的最大文件大小,填写 0 表示不限制,超出大小时仍会尝试打开文件,但会警告用户。", + "srcEncodedVar": "经过 URL 编码后的文件 Blob 临时访问地址", + "srcVar": "文件 Blob 临时访问地址", + "nameEncodedVar": "经过 URL 编码后的文件名", + "versionEntityVar": "打开的文件版本 Blob ID,为空时表示打开的是最新版本。", + "fileIdVar": "文件 ID", + "userIdVar": "用户 ID,未登录时为空。", + "userDisplayNameVar": "经过 URL 编码后的用户昵称。", + "fileViewers": "文件浏览应用", + "addViewer": "添加应用", + "viewerGroupTitle": "应用分组 #{{index}}", + "viewerType": "类型", + "displayName": "名称", + "displayNameDes": "展示名称,支持 i18next 键值。", + "viewerEnabled": "启用", + "newFileAction": "新建文件映射", + "newFileActionDes": "添加映射后,用户点击“新建”按钮后会出现此应用的选项。", + "addNewFileAction": "添加映射", + "builtinViewerType": "内置应用", + "wopiViewerType": "WOPI", + "customViewerType": "自定义", + "nMapping": "{{num}} 个", + "editViewerTitle": "编辑 {{name}}", + "builtInIconUrlDes": "此内置应用有默认图标,图标地址留空时会使用默认图标。", + "viewerUrl": "应用 URL", + "viewerUrlDes": "自定义应用的 URL 地址,支持使用 <0>魔法变量。", + "addIcon": "添加图标", + "exts": "扩展名列表", + "icon": "图标", + "iconUrl": "图标地址", + "iconColor": "图标颜色", + "iconColorDark": "图标颜色(黑暗模式)", + "fileIcons": "文件图标", + "builtinIcon": "内置图标", + "mimeMapping": "MIME 类型映射", + "mimeMappingDes": "JSON 格式的 MIME 类型映射表,键为文件扩展名,值为 MIME 类型。Cloudreve 会根据文件扩展名和此设置判断文件 MIME 类型。", + "mapProvider": "地图提供商", + "mapProviderDes": "展示媒体位置信息时使用的地图提供商。", + "mapGoogle": "Google Maps", + "mapOpenStreetMap": "OpenStreetMap", + "tileType": "默认地图类型", + "tileTypeDes": "Google Maps 默认地图类型。", + "tileTypeTerrain": "地形", + "tileTypeSatellite": "卫星", + "tileTypeGeneral": "常规", + "maxPageSize": "最大分页大小", + "maxPageSizeDes": "限制用户可调整的每页最大文件数量。", + "maxRecursiveSearch": "最大递归搜索数量", + "maxRecursiveSearchDes": "用户搜索文件时,如果已搜索的文件数量超出此限制,搜索会停止并警告用户。", + "maxBatchSize": "最大批量操作数量", + "maxBatchSizeDes": "用户可批量操作的最大文件数量,只会统计顶层数量,子目录下的文件数量不会计入。", + "defaultPagination": "文件列表分页方式", + "cursorPagination": "游标分页", + "cursorPaginationDes": "用户滚动到底端后会自动加载更多文件,对于大量文件列表性能较好,但无法看到总页数。", + "offsetPagination": "传统分页", + "offsetPaginationDes": "页面底部会展示分页导航,用户可以看到总页数并跳转到某一页,在对于大量文件列表下性能较差。", + "defaultPaginationDes": "无论上面设置如何,用户在搜索时会强制使用游标分页。", + "publicResourceMaxAge": "静态资源缓存有效期 (秒)", + "publicResourceMaxAgeDes": "用于告知浏览器或 CDN 缓存静态资源的有效期,单位为秒。影响范围包括文件、缩略图和用户头像。", + "cronDes": "{{des}},此处需要填写正确的 <0>Cron 表达式。重启 Cloudreve 后生效。", + "entityCollectInterval": "文件 Blob 回收间隔", + "entityCollectIntervalDes": "设定多久扫描并删除过期的文件 Blob", + "trashBinInterval": "回收站扫描间隔", + "trashBinIntervalDes": "设定多久扫描并删除回收站中的过期文件", + "logtoName": "登录方式名称", + "logtoNameDes": "用于展示给用户的登录方式名称,默认为“SSO”,支持 i18next 键值。", + "logtoDirectSSO": "直达第三方登录", + "logtoDirectSSODes": "如果你想要跳过 Logto 登录屏幕,直接跳转到对接的第三方登录或 SSO,请在此填写第三方登录连接器的标识,详情请参考 <0>Logto 文档。", + "logtoEndpoint": "Logto 端点", + "logtoEndpointDes": "应用管理面板获取到的 Logto 端点地址,可以为自己部署的实例。", + "logtoKey": "应用密钥", + "logtoKeyDes": "应用管理页面创建的应用密钥。", + "logtoAppIDDes": "你所创建的应用 ID", + "logto": "Logto", + "logtoDes": "借由 <0>Logto, 你可以实现更多第三方平台的互联登录,比如 Apple、GitHub、Microsoft Entra ID、Google、短信 等。请在 Logto 管理面板创建一个 “传统网页应用”,并将 {{url}} 加入到 “重定向 URIs”中。", + "thirdPartySignIn": "第三方登录", + "logo": "LOGO", + "logoDes": "LOGO 图像的地址,用于在左上角展示;请分别提供黑暗模式和日间模式下不同的 LOGO。", + "dark": "黑暗模式", + "light": "日间模式", + "tosUrl": "使用条款链接", + "tosUrlDes": "用于在用户登录或注册页脚展示,留空不展示。", + "privacyUrl": "隐私政策链接", + "privacyUrlDes": "用于在用户登录或注册页脚展示,留空不展示。", + "addSecondary": "添加备选站点 URL", + "secondarySiteURL": "备选", + "secondaryDes": "你还可以添加其他备选站点 URL,Cloudreve 会根据用户实际访问的 URL 自动选择是否使用", + "primarySiteURL": "主要", + "primarySiteURLDes": "主要站点 URL 用于与外部服务通信和接受回调(比如:存储提供商),请使用能被公网访问的 URL。", + "revert": "撤销更改", "saved": "设置已更改", "save": "保存", "basicInformation": "基本信息", - "mainTitle": "主标题", - "mainTitleDes": "站点的主标题", - "subTitle": "副标题", - "subTitleDes": "站点的副标题", + "mainTitle": "站点名称", + "mainTitleDes": "站点的名称。", "siteDescription": "站点描述", - "siteDescriptionDes": "站点描述信息,可能会在分享页面摘要内展示", + "siteDescriptionDes": "站点描述信息,可能会在分享页面摘要内展示。", "siteURL": "站点 URL", - "siteURLDes": "非常重要,请确保与实际情况一致。使用云存储策略、支付平台时,请填入可以被外网访问的地址", "customFooterHTML": "页脚代码", - "customFooterHTMLDes": "在页面底部插入的自定义 HTML 代码", - "pwa": "渐进式应用 (PWA)", + "customFooterHTMLDes": "在页面底部插入的自定义 HTML 代码。", + "announcement": "站点公告", + "announcementDes": "展示给已登录用户的公告,留空不展示。当此项内容更改时,所有用户会重新看到公告。", + "supportHTML": "支持 HTML 代码", + "branding": "图标", "smallIcon": "小图标", - "smallIconDes": "扩展名为 ico 的小图标地址", + "smallIconDes": "扩展名为 ico 的小图标地址。", "mediumIcon": "中图标", - "mediumIconDes": "192x192 的中等图标地址,png 格式", + "mediumIconDes": "192x192 的中等图标地址,png 格式。", "largeIcon": "大图标", - "largeIconDes": "512x512 的大图标地址,png 格式。此图标还会被用于在 iOS 客户端切换站点时展示", + "largeIconDes": "512x512 的大图标地址,png 格式。此图标还会被用于在 iOS 客户端切换站点时展示。", "displayMode": "展示模式", - "displayModeDes": "PWA 应用添加后的展示模式", + "displayModeDes": "PWA 应用添加后的展示模式。", "themeColor": "主题色", - "themeColorDes": "CSS 色值,影响 PWA 启动画面上状态栏、内容页中状态栏、地址栏的颜色", + "themeColorDes": "CSS 色值,影响 PWA 启动画面上状态栏、内容页中状态栏、地址栏的颜色。", "backgroundColor": "背景色", "backgroundColorDes": "CSS 色值", "hint": "提示", - "webauthnNoHttps": "Web Authn 需要您的站点启用 HTTPS,并确认 参数设置 - 站点信息 - 站点URL 也使用了 HTTPS 后才能开启。", + "webauthnNoHttps": "Web Authn 需要你的站点启用 HTTPS,并确认 参数设置 - 站点信息 - 站点URL 也使用了 HTTPS 后才能开启。", "accountManagement": "注册与登录", "allowNewRegistrations": "允许新用户注册", - "allowNewRegistrationsDes": "关闭后,无法再通过前台注册新的用户", + "allowNewRegistrationsDes": "关闭后,无法再通过前台注册新的用户。", "emailActivation": "邮件激活", - "emailActivationDes": "开启后,新用户注册需要点击邮件中的激活链接才能完成。请确认邮件发送设置是否正确,否则激活邮件无法送达。", + "emailActivationDes": "开启后,新用户注册需要点击邮件中的激活链接才能完成。请确认 <0>邮件发信设置 是否正确,否则激活邮件无法送达。", "captchaForSignup": "注册验证码", - "captchaForSignupDes": "是否启用注册表单验证码", + "captchaForSignupDes": "是否启用注册表单验证码。", "captchaForLogin": "登录验证码", - "captchaForLoginDes": "是否启用登录表单验证码", + "captchaForLoginDes": "是否启用登录表单验证码。", "captchaForReset": "找回密码验证码", - "captchaForResetDes": "是否启用找回密码表单验证码", - "webauthnDes": "是否允许用户使用绑定的外部验证器登录,站点必须启用 HTTPS 才能使用。", - "webauthn": "外部验证器登录", + "captchaForResetDes": "是否启用找回密码表单验证码。", + "webauthnDes": "是否允许用户使用绑定的硬件认证设备登录,比如:人脸、指纹或 USB 密钥;站点必须启用 HTTPS 才能使用。", + "webauthn": "使用通行密钥登录", "defaultGroup": "默认用户组", - "defaultGroupDes": "用户注册后的初始用户组", + "defaultGroupDes": "用户注册后的初始用户组。", "testMailSent": "测试邮件已发送", "testSMTPSettings": "发件测试", - "testSMTPTooltip": "发送测试邮件前,请先保存已更改的邮件设置;邮件发送结果不会立即反馈,如果您长时间未收到测试邮件,请检查 Cloudreve 在终端输出的错误日志。", + "testSMTPTooltip": "Cloudreve 会使用你当前 SMTP 设置发送测试邮件,测试前无需保存设置。", "recipient": "收件人地址", "send": "发送", "smtp": "发信", "senderName": "发件人名", - "senderNameDes": "邮件中展示的发件人姓名", + "senderNameDes": "邮件中展示的发件人姓名。", "senderAddress": "发件人邮箱", - "senderAddressDes": "发件邮箱的地址", + "senderAddressDes": "发件邮箱的地址。", "smtpServer": "SMTP 服务器", - "smtpServerDes": "发件服务器地址,不含端口号", + "smtpServerDes": "发件服务器地址,不含端口号。", "smtpPort": "SMTP 端口", - "smtpPortDes": "发件服务器地址端口号", + "smtpPortDes": "发件服务器地址端口号。", "smtpUsername": "SMTP 用户名", - "smtpUsernameDes": "发信邮箱用户名,一般与邮箱地址相同", + "smtpUsernameDes": "发信邮箱用户名,一般与邮箱地址相同。", "smtpPassword": "SMTP 密码", "smtpPasswordDes": "发信邮箱密码", "replyToAddress": "回信邮箱", - "replyToAddressDes": "用户回复系统发送的邮件时,用于接收回信的邮箱", + "replyToAddressDes": "用户回复系统发送的邮件时,用于接收回信的邮箱。", "enforceSSL": "强制使用 SSL 连接", - "enforceSSLDes": "是否强制使用 SSL 加密连接。如果无法发送邮件,可关闭此项, Cloudreve 会尝试使用 STARTTLS 并决定是否使用加密连接", + "enforceSSLDes": "是否强制使用 SSL 加密连接。如果无法发送邮件,可关闭此项, Cloudreve 会尝试使用 STARTTLS 并决定是否使用加密连接。", "smtpTTL": "SMTP 连接有效期 (秒)", - "smtpTTLDes": "有效期内建立的 SMTP 连接会被新邮件发送请求复用", + "smtpTTLDes": "有效期内建立的 SMTP 连接会被新邮件发送请求复用。", "emailTemplates": "邮件模板", "activateNewUser": "新用户激活", - "activateNewUserDes": "新用户注册后激活邮件的模板", "resetPassword": "重置密码", - "resetPasswordDes": "密码重置邮件模板", "sendTestEmail": "发送测试邮件", "transportation": "传输", "workerNum": "Worker 数量", "workerNumDes": "主机节点任务队列最多并行执行的任务数,保存后需要重启 Cloudreve 生效", - "transitParallelNum": "中转并行传输", - "transitParallelNumDes": "任务队列中转任务传输时,最大并行协程数", "tempFolder": "临时目录", "tempFolderDes": "用于存放解压缩、压缩等任务产生的临时文件的目录路径", - "textEditMaxSize": "文档在线编辑最大尺寸", - "textEditMaxSizeDes": "文档文件可在线编辑的最大大小,超出此大小的文件无法在线编辑。此项设置适用于纯文本文件、代码文件、Office 文档 (WOPI)", - "failedChunkRetry": "分片错误重试", - "failedChunkRetryDes": "分片上传失败后重试的最大次数,只适用于服务端上传或中转", - "cacheChunks": "缓存流式分片文件以用于重试", - "cacheChunksDes": "开启后,流式中转分片上传时会将分片数据缓存在系统临时目录,以便用于分片上传失败后的重试;\n 关闭后,流式中转分片上传不会额外占用硬盘空间,但分片上传失败后整个上传会立即失败。", + "textEditMaxSize": "文档在线编辑最大大小", + "textEditMaxSizeDes": "文档文件可在线编辑的最大大小,超出此大小的文件无法在线编辑。此项设置适用于纯文本文件、代码文件、Office 文档 (WOPI)等 Web 在线编辑器。", "resetConnection": "上传校验失败时强制重置连接", "resetConnectionDes": "开启后,如果本次策略、头像等数据上传校验失败,服务器会强制重置连接", - "expirationDuration": "有效期 (秒)", "batchDownload": "打包下载", - "downloadSession": "下载会话", "previewURL": "预览链接", - "docPreviewURL": "Office 文档预览链接", - "uploadSession": "上传会话", - "uploadSessionDes": "在上传会话有效期内,对于支持的存储策略,用户可以断点续传未完成的任务。最大可设定的值受限于不同存储策略服务商的规则。", - "downloadSessionForShared": "分享下载会话", - "downloadSessionForSharedDes": "设定时间内重复下载分享文件,不会被记入总下载次数", - "onedriveMonitorInterval": "OneDrive 客户端上传监控间隔", - "onedriveMonitorIntervalDes": "每间隔所设定时间,Cloudreve 会向 OneDrive 请求检查客户端上传情况已确保客户端上传可控", - "onedriveCallbackTolerance": "OneDrive 回调等待", - "onedriveCallbackToleranceDes": "OneDrive 客户端上传完成后,等待回调的最大时间,如果超出会被认为上传失败", - "onedriveDownloadURLCache": "OneDrive 下载请求缓存", - "onedriveDownloadURLCacheDes": "OneDrive 获取文件下载 URL 后可将结果缓存,减轻热门文件下载API请求频率", - "slaveAPIExpiration": "从机API请求超时(秒)", - "slaveAPIExpirationDes": "主机等待从机API请求响应的超时时间", - "heartbeatInterval": "节点心跳间隔(秒)", - "heartbeatIntervalDes": "主机节点向从机节点发送心跳的间隔", - "heartbeatFailThreshold": "心跳失败重试阈值", - "heartbeatFailThresholdDes": "主机向从机发送心跳失败后,主机可最大重试的次数。重试失败后,节点会进入恢复模式", - "heartbeatRecoverModeInterval": "恢复模式心跳间隔(秒)", - "heartbeatRecoverModeIntervalDes": "节点因异常被主机标记为恢复模式后,主机尝试重新连接节点的间隔", - "slaveTransitExpiration": "从机中转超时(秒)", - "slaveTransitExpirationDes": "从机执行文件中转任务可消耗的最长时间", - "nodesCommunication": "节点通信", "cannotDeleteDefaultTheme": "不能删除默认配色", - "keepAtLeastOneTheme": "请至少保留一个配色方案", - "duplicatedThemePrimaryColor": "主色调不能与已有配色重复", - "themes": "主题配色", - "colors": "关键色", "themeConfig": "色彩配置", "actions": "操作", "wrongFormat": "格式不正确", - "createNewTheme": "新建配色方案", - "themeConfigDoc": "https://v4.mui.com/zh/customization/default-theme/", - "themeConfigDes": "完整的配置项可在 <0>默认主题 - Material-UI 查阅。", - "defaultTheme": "默认配色", - "defaultThemeDes": "用户未指定偏好配色时,站点默认使用的配色方案", - "appearance": "界面", - "personalFileListView": "个人文件列表默认样式", - "personalFileListViewDes": "用户未指定偏好样式时,个人文件页面列表默认样式", - "sharedFileListView": "目录分享页列表默认样式", - "sharedFileListViewDes": "用户未指定偏好样式时,目录分享页面的默认样式", - "primaryColor": "主色调", - "primaryColorText": "主色调文字", - "secondaryColor": "辅色调", - "secondaryColorText": "辅色调文字", "avatar": "头像", "gravatarServer": "Gravatar 服务器", "gravatarServerDes": "Gravatar 服务器地址,可选择使用国内镜像", "avatarFilePath": "头像存储路径", - "avatarFilePathDes": "用户上传自定义头像的存储路径", + "avatarFilePathDes": "用户上传自定义头像的存储路径,相对于 Cloudreve 数据目录。", "avatarSize": "头像文件大小限制", "avatarSizeDes": "用户可上传头像文件的最大大小", - "smallAvatarSize": "小头像尺寸", - "mediumAvatarSize": "中头像尺寸", - "largeAvatarSize": "大头像尺寸", + "avatarImageSize": "图像尺寸 (px)", + "avatarImageSizeDes": "用户所上传头像会被调整到给定的尺寸,单位为像素。", "filePreview": "文件预览", - "officePreviewService": "Office 文档预览服务", - "officePreviewServiceDes": "可使用以下替换变量:", - "officePreviewServiceSrcDes": "文件 URL", - "officePreviewServiceSrcB64Des": " Base64 编码后的文件 URL", - "officePreviewServiceName": "文件名", "thumbnails": "缩略图", "thumbnailDoc": "有关配置缩略图的更多信息,请参阅 <0>官方文档。", - "thumbnailDocLink":"https://docs.cloudreve.org/use/thumbnails", + "thumbnailDocLink": "https://docs.cloudreve.org/use/thumbnails", "thumbnailBasic": "基本设置", - "generators": "生成器", + "generators": "缩略图生成器", "thumbMaxSize": "最大原始文件尺寸", - "thumbMaxSizeDes": "可生成缩略图的最大原始文件的大小,超出此大小的文件不会生成缩略图", - "generatorProxyWarning": "默认情况下,非本机存储策略只会使用“存储策略原生”生成器。你可以通过开启“生成器代理”功能扩展第三方存储策略的缩略图能力。", + "thumbMaxSizeDes": "可生成缩略图的最大原始文件的大小,超出此大小的文件不会生成缩略图。", + "generatorProxyWarning": "默认情况下,非本机存储策略只会使用“存储策略原生”生成器。你可以在存储策略设置页面开启“生成器代理”功能扩展第三方存储策略的缩略图能力。", "policyBuiltin": "存储策略原生", - "policyBuiltinDes": "使用存储提供方原生的图像处理接口。对于本机和 S3 策略,这一生成器不可用,将会自动顺沿其他生成器。对于其他存储策略,支持的原始图像格式和大小限制请参考 Cloudreve 文档。", - "cloudreveBuiltin":"Cloudreve 内置", + "policyBuiltinDes": "使用存储提供方原生的图像处理接口。对于本机和 S3 策略,这一生成器不可用,将会自动顺沿其他生成器。对于其他存储策略,请前往存储策略设置页面设置允许的扩展名。", + "cloudreveBuiltin": "Cloudreve 内置", "cloudreveBuiltinDes": "使用 Cloudreve 内置的图像处理能力,仅支持 PNG、JPEG、GIF 格式的图片。", "libreOffice": "LibreOffice", "libreOfficeDes": "使用 LibreOffice 生成 Office 文档的缩略图。这一生成器依赖于任一其他图像生成器(Cloudreve 内置 或 VIPS)。", "vips": "VIPS", "vipsDes": "使用 libvips 处理缩略图图像,支持更多图像格式,资源消耗更低。", - "thumbDependencyWarning": "LibreOffice 生成器依赖于 Cloudreve 内置 或 VIPS 生成器,请开启其中任一生成器。", + "thumbDependencyWarning": "LibreOffice 或歌曲封面生成器依赖于 Cloudreve 内置 或 VIPS 生成器,请开启其中任一生成器。", "ffmpeg": "FFmpeg", "ffmpegDes": "使用 FFmpeg 生成视频缩略图。", - "libRaw": "LibRaw", - "libRawDes": "使用 LibRaw 处理 RAW 图像。", "executable": "可执行文件", - "executableDes": "第三方生成器可执行文件的地址或命令", + "executableDes": "第三方生成器可执行文件的路径或命令。", "executableTest": "测试", "executableTestSuccess": "生成器正常,版本:{{version}}", "generatorExts": "可用扩展名", - "generatorExtsDes": "此生成器可用的文件扩展名列表,多个请使用半角逗号 , 隔开", + "generatorExtsDes": "此生成器可用的文件扩展名列表,多个请使用半角逗号 , 隔开。", "ffmpegSeek": "缩略图截取位置", "ffmpegSeekDes": "定义缩略图截取的时间,推荐选择较小值以加速生成过程。如果超出视频实际长度,会导致缩略图截取失败", "generatorProxy": "生成器代理", "enableThumbProxy": "使用生成器代理", "proxyPolicyList": "启动代理的存储策略", "proxyPolicyListDes": "可多选。选中后,存储策略不支持原生生成缩略图的类型会由 Cloudreve 代理生成", - "thumbWidth": "缩略图宽度", - "thumbHeight": "缩略图高度", - "thumbSuffix": "缩略图文件后缀", - "thumbConcurrent": "缩略图生成并行数量", - "thumbConcurrentDes": "-1 表示自动决定", + "thumbWidth": "最大宽度", + "thumbHeight": "最大高度", + "thumbSuffix": "Blob 文件后缀", + "thumbSuffixDes": "生成的缩略图 Blob 相对于原始 Blob 增加的后缀,", "thumbFormat": "缩略图格式", "thumbFormatDes": "可选:png/jpg", "thumbQuality": "图像质量", - "thumbQualityDes": "压缩质量百分比,只针对 jpg 编码有效", + "thumbQualityDes": "压缩质量百分比,只针对 jpg 编码有效。", "thumbGC": "生成完成后立即回收内存", "captcha": "验证码", "captchaType": "验证码类型", - "plainCaptcha": "普通", + "captchaTypeDes": "选择验证码类型和验证码服务提供商。", + "plainCaptcha": "图形", "reCaptchaV2": "reCAPTCHA V2", - "tencentCloudCaptcha": "腾讯云验证码", + "turnstile": "Cloudflare Turnstile", + "turnstileSiteKey": "站点密钥", + "turnstileSiteKSecret": "密钥", "captchaProvider": "验证码类型", - "plainCaptchaTitle": "普通验证码", "captchaWidth": "宽度", "captchaHeight": "高度", "captchaLength": "长度", @@ -279,9 +427,9 @@ "showSlimeLine": "使用波浪线", "showSineLine": "使用正弦线", "siteKey": "Site KEY", - "siteKeyDes": "<0>应用管理页面 获取到的的 网站密钥", + "siteKeyDes": "<0>应用管理页面 获取到的的 网站密钥。", "siteSecret": "Secret", - "siteSecretDes": "<0>应用管理页面 获取到的的 秘钥", + "siteSecretDes": "<0>应用管理页面 获取到的的 秘钥。", "secretID": "SecretId", "secretIDDes": "<0>访问密钥页面 获取到的的 SecretId", "secretKey": "SecretKey", @@ -292,16 +440,335 @@ "tCaptchaSecretKeyDes": "<0>图形验证页面 获取到的的 App Secret Key", "staticResourceCache": "静态公共资源缓存", "staticResourceCacheDes": "公共可访问的静态资源(如:本机策略直链、文件下载链接)的缓存有效期", - "wopiClient": "WOPI 客户端", - "wopiClientDes": "通过对接支持 WOPI 协议的在线文档处理系统,扩展 Cloudreve 的文档在线预览和编辑能力。详情请参考 <0>官方文档。", - "wopiDocLink": "https://docs.cloudreve.org/use/wopi", - "enableWopi": "使用 WOPI", - "wopiEndpoint": "WOPI Discovery Endpoint", - "wopiEndpointDes": "WOPI 客户端发现 API 的端点地址", - "wopiSessionTtl": "编辑会话有效期(秒)", - "wopiSessionTtlDes": "用户打开在线编辑文档会话的有效期,超出此期限的会话无法继续保存新更改" + "creditSystem": "积分系统", + "creditAndVAS": "积分与增值服务", + "enableCredit": "启用积分系统", + "enableCreditDes": "启用积分系统,允许用户为分享链接设置价格。", + "creditPrice": "积分价格", + "creditPriceDes": "使用货币充值积分的价格(以最小货币单位计),填写 0 表示禁止充值积分。", + "shareScoreRate": "分享者佣金比例", + "shareScoreRateDes": "分享链接被购买时,分享者获得的积分百分比(1-100)", + "cronNotifyUser": "通知超额用户扫描间隔", + "cronNotifyUserDes": "扫描并发送邮件提醒超额用户,", + "cronBanUser": "用户封禁扫描间隔", + "cronBanUserDes": "扫描并封禁超出存储且超出缓冲期的用户", + "anonymousPurchase": "匿名购买", + "anonymousPurchaseDes": "允许未登录用户直接购买分享链接。", + "shopNavEnabled": "显示商店导航", + "shopNavEnabledDes": "在侧边栏导航中显示“商店”条目。", + "paymentSettings": "支付设置", + "currencyCode": "货币代码", + "currencyCodeDes": "三字母货币代码(如 USD、CNY、EUR)", + "currencySymbol": "货币符号", + "currencySymbolDes": "显示的货币符号(如 $、¥、€)", + "currencyUnit": "货币单位", + "currencyUnitDes": "最小货币单位(如美元/分为100)", + "paymentProviders": "支付提供商", + "providerName": "提供商名称,用于展示给用户。", + "providerType": "提供商类型", + "providerKey": "密钥", + "selectCurrency": "选择常用货币", + "addPaymentProvider": "添加支付提供商", + "stripeProvider": "Stripe", + "weixinProvider": "微信支付", + "customProvider": "自定义支付渠道", + "customProviderDes": "通过实现 Cloudreve 兼容付款接口来对接其他第三方支付平台,详情请参考 <0>官方文档。", + "providerKeyDes": "输入 Stripe 的 API 密钥。", + "storageProductSettings": "存储产品", + "storageProductsDes": "配置用户可以购买以扩展存储空间的产品。", + "addStorageProduct": "添加产品", + "editStorageProduct": "编辑产品", + "storageSize": "存储大小", + "storageSizeBytes": "此产品包含的存储大小。", + "duration": "时长", + "durationSeconds": "时长(秒,例如:2592000 表示 30 天)。", + "price": "价格", + "priceInUnits": "价格(以最小货币单位计)", + "priceInUnitsDes": "价格将显示为:", + "chipLabel": "标签(可选)", + "chipLabelHelp": "显示在产品名称旁边的短文本标签。", + "usePoints": "允许使用积分", + "points": "积分", + "pointsHelp": "购买此产品所需的积分数量。", + "pointsUnit": "积分", + "groupProductSettings": "用户组产品", + "groupProductsDes": "配置用户可以购买以加入特定用户组的产品。", + "addGroupProduct": "添加用户组产品", + "editGroupProduct": "编辑用户组产品", + "groupId": "用户组 ID", + "groupIdHelp": "购买此产品后升级到的用户组。", + "description": "描述", + "descriptionHelp": "输入特性或优势,每行一项", + "receiptEmailTemplate": "支付收据模板", + "receiptEmailTemplateDes": "当支付被确认时发送给用户的邮件模板。", + "activationEmailTemplate": "账户激活模板", + "activationEmailTemplateDes": "当用户激活账户时发送给用户的邮件模板。", + "quotaExceededEmailTemplate": "存储配额超出模板", + "quotaExceededEmailTemplateDes": "当用户超出存储配额时发送给用户的邮件模板。", + "resetPasswordEmailTemplate": "密码重置模板", + "resetPasswordEmailTemplateDes": "当用户请求重置密码时发送给用户的邮件模板。", + "addLanguage": "添加语言", + "languageCodeDes": "请选择要添加的语言。", + "emailSubject": "邮件主题", + "emailSubjectDes": "邮件的主题。", + "emailBody": "邮件内容", + "emailBodyDes": "邮件的内容。你可以使用 <0>魔法变量 来定制邮件内容。", + "orderTitle": "订单标题", + "themeOptions": "主题选项", + "themeOptionsDes": "为你的站点配置自定义主题选项。这些主题将可供用户在其偏好设置中选择。", + "primaryColor": "主色调", + "secondaryColor": "次色调", + "primaryColorDark": "主色调(暗色模式)", + "secondaryColorDark": "次色调(暗色模式)", + "addThemeOption": "添加主题选项", + "editThemeOption": "编辑主题选项", + "invalidThemeConfig": "无效的主题配置。请检查你的 JSON 语法。", + "themeConfiguration": "主题配置", + "themePreview": "主题预览", + "lightTheme": "亮色主题", + "darkTheme": "暗色主题", + "previewTitle": "预览标题", + "previewTextField": "输入字段", + "previewPrimary": "主色调", + "invalidThemePreview": "无效的主题配置,无法预览", + "duplicateThemeColor": "已存在使用此主色调的主题。请选择不同的颜色。", + "themeDes": "完整的可配置项请参考 <0>Material-UI Default theme viewer。", + "defaultTheme": "默认", + "auditLog": "事件", + "auditLogDes": "配置哪些事件应该被记录。某些事件可能会被系统用于提供额外功能,例如文件活动和登录活动。", + "systemEvents": "系统事件", + "systemEventsDes": "与系统操作和状态相关的事件。", + "userEvents": "用户事件", + "userEventsDes": "与用户账户、认证和配置文件更改相关的事件。", + "fileEvents": "文件事件", + "fileEventsDes": "与文件操作相关的事件,如上传、下载和修改。", + "shareEvents": "分享事件", + "shareEventsDes": "与文件分享和链接访问相关的事件。", + "versionEvents": "版本事件", + "versionEventsDes": "与文件版本管理相关的事件。", + "mediaEvents": "媒体事件", + "mediaEventsDes": "与媒体文件处理相关的事件,如缩略图生成。", + "filesystemEvents": "文件系统事件", + "filesystemEventsDes": "与文件系统操作相关的事件,如挂载和归档处理。", + "webdavEvents": "WebDAV 事件", + "webdavEventsDes": "与 WebDAV 账户管理和访问相关的事件。", + "paymentEvents": "支付事件", + "paymentEventsDes": "与支付交易和处理相关的事件。", + "emailEvents": "Email 事件", + "emailEventsDes": "与邮件发送和通知相关的事件。", + "toggleAll": "启用/禁用所有事件", + "toggleAllDes": "启用或禁用此类别中的所有事件。", + "event": { + "server_start": "服务器启动", + "user_signup": "用户注册", + "email_sent": "邮件发送", + "user_activated": "用户激活", + "user_login_failed": "登录失败", + "user_login": "用户登录", + "user_token_refresh": "令牌刷新", + "file_create": "文件创建", + "file_rename": "文件重命名", + "set_file_permission": "权限更改", + "entity_uploaded": "文件上传或更新", + "entity_downloaded": "文件下载", + "copy_from": "复制来源", + "copy_to": "复制到", + "move_to": "移动到", + "delete_file": "文件删除", + "move_to_trash": "移动到回收站", + "share": "分享创建", + "share_link_viewed": "分享链接查看", + "set_current_version": "设置当前版本", + "delete_version": "删除版本", + "thumb_generated": "缩略图生成", + "live_photo_uploaded": "上传 Live Photo", + "update_metadata": "元数据更新", + "edit_share": "分享编辑", + "delete_share": "分享删除", + "mount": "挂载", + "relocate": "转移存储策略", + "create_archive": "创建归档", + "extract_archive": "解压归档", + "webdav_login_failed": "WebDAV 登录失败", + "webdav_account_create": "WebDAV 账户创建", + "webdav_account_update": "WebDAV 账户更新", + "webdav_account_delete": "WebDAV 账户删除", + "payment_created": "支付创建", + "points_change": "积分更改", + "payment_paid": "支付完成", + "payment_fulfilled": "履行订单", + "payment_fulfill_failed": "履行订单失败", + "storage_added": "存储扩容", + "group_changed": "用户组更改", + "user_exceed_quota_notified": "超出配额通知", + "user_changed": "用户状态更改", + "get_direct_link": "获取直链", + "link_account": "链接外部账户", + "unlink_account": "取消链接外部账户", + "change_nick": "更改昵称", + "change_avatar": "更改头像", + "membership_unsubscribe": "取消订阅", + "change_password": "更改密码", + "enable_2fa": "启用 2FA", + "disable_2fa": "禁用 2FA", + "add_passkey": "添加通行密钥", + "remove_passkey": "移除通行密钥", + "redeem_gift_code": "兑换礼品码" + }, + "server": "服务器设置", + "tempPath": "临时路径", + "tempPathDes": "存储临时文件的目录,相对于 Cloudreve 数据目录。修改前请确保没有正在进行的队列任务。", + "siteID": "站点 ID", + "siteIDDes": "用于标识站点的唯一 ID,一般无需修改。", + "siteSecretKey": "主密钥", + "siteSecretKeyDes": "用于加密用户令牌、签名的主密钥。轮转后,所有用户令牌、签名都将失效。保存后重启 Cloudreve 生效。", + "rotateSecretKey": "轮转主密钥", + "hashidSalt": "HashID 盐值", + "hashidSaltDes": "用于生成 HashID 的盐值,请谨慎更改,更改后会导致现有的直链、分享链接等全部失效。", + "accessTokenTTL": "访问令牌 TTL", + "accessTokenTTLDes": "访问令牌的有效期,单位为秒。", + "refreshTokenTTL": "刷新令牌 TTL", + "refreshTokenTTLDes": "刷新令牌的有效期,单位为秒。影响用户登录状态的保持时间。", + "cronGarbageCollect": "垃圾回收扫描间隔", + "cronGarbageCollectDes": "设定多久扫描并回收临时文件和 KV 存储中的过期数据", + "startWithProtocol": "必须以 http:// 或 https:// 开头", + "tlsWarning": "当前站点使用 https,这里填写 http 的 URL 可能会导致异常。", + "blobUrlCache": "Blob URL 缓存", + "clearBlobUrlCache": "清除 Blob URL 缓存", + "clearBlobUrlCacheDes": "为了增加缓存命中率,Cloudreve 会缓存并复用 Blob URL。当 CDN 地址等设置发生变更时,请清除缓存。", + "cacheCleared": "缓存已清除" + }, + "giftCodes": { + "giftCodesSettings": "礼品码", + "generateGiftCodes": "生成礼品码", + "giftCodeQuantity": "数量", + "giftCodeQuantityHelp": "要生成的礼品码数量。", + "giftCodeProductType": "产品类型", + "giftCodeTypePoints": "积分", + "giftCodeTypeStorage": "存储空间", + "giftCodeTypeGroup": "用户组", + "giftCodePointsAmount": "积分数量", + "giftCodePointsAmountHelp": "兑换码被使用时将获得的积分数量。", + "giftCodeProduct": "产品", + "selectStorageProduct": "选择存储产品", + "selectGroupProduct": "选择用户组产品", + "giftCodeType": "类型", + "giftCodeAmount": "数量", + "giftCode": "礼品码", + "giftCodeStatus": "状态", + "giftCodeUsed": "已使用", + "giftCodeUnused": "可用", + "giftCodeDeleted": "礼品码已成功删除", + "giftCodesGenerated": "礼品码已成功生成", + "noGiftCodes": "暂无礼品码", + "generatedCodesTitle": "已生成的礼品码", + "generatedCodesDescription": "复制这些礼品码以分享给用户。每个礼品码只能使用一次。", + "copyAndClose": "复制并关闭", + "duratonTimes": "时长倍数", + "duratonTimesDes": "每个礼品码包含了多少份对应商品。", + "unknownProduct": "未知产品" }, "policy": { + "deletePolicyConfirmation": "确定要删除存储策略 {{name}} 吗?", + "streamSaver": "由浏览器处理下载", + "streamSaverDes": "开启后,用户下载文件时会强制由浏览器处理。因为 OneDrive 存储策略的限制,用户直接下载文件时得到的文件名无法与 Cloudreve 内文件名一致,由浏览器处理下载可以解决此问题。", + "oauthCallbackFailed": "授权失败", + "httpsRequired": "Entra ID 应用需要使用 HTTPS 重定向 URL,但是当前站点使用的是 HTTP,后续登录完成后可能会导致重定向失败,届时请手动将浏览器地址栏中的 HTTPS 替换为 HTTP。", + "authorizeMicrosoft": "使用 Microsoft 登录", + "redirectUrl": "重定向 URL", + "redirectUrlDes": "当前展示的是最新的符合要求的重定向 URL,请确认应用设置中的重定向 URL 一致。", + "authorizeOneDrive": "确认 Entra ID 应用设置", + "authorizeOneDriveDes": "请确认以下 Entra ID 应用信息是否仍然有效,如有需要请做出更改。", + "authorizeNow": "立即授权", + "authorizeAgain": "重新授权", + "notGranted": "无授权账号,存储策略无法使用。", + "granted": "已授权账号,凭证刷新于 <0>。", + "grantedNotRefresh": "已授权账号,凭证自上次启动后尚未刷新。", + "batchDeleteSize": "最大批量删除数量", + "batchDeleteSizeDes": "限制单次 API 请求的最大删除数量,此设置不会影响用户删除批量文件。不填写会使用默认值 <0>1000,这是官方 S3 API 的最大允许值。", + "bucketPolicy": "桶策略", + "cdnOrCustomDomain": "CDN 或自定义源站域名", + "bucketDomain": "存储空间域名", + "bucketDomainDes": "填写你为存储空间绑定的 CDN 加速域名或者自定义源站域名。", + "storageNodeInternal": "存储节点(内网 Endpoint)", + "chunkSizeDesOssObs": "允许范围:100 KB ~ 5 GB,", + "chunkSizeDesQiniuCos": "允许范围:1 MB ~ 1 GB,", + "chunkSizeDesS3": "允许范围:5 MB ~ 5 GB,", + "thisIsACustomDomain": "这是一个自定义域名", + "thisIsACustomDomainDes": "如果你为 Bucket 绑定了自定义域名,且需要通过自定义域名进行上传等管理操作,请勾选此选项。勾选后,Cloudreve 不会在请求域名中尝试补全 Bucket 名称。", + "addedManually": "我已自行设置", + "origin": "来源", + "allowMethods": "允许 Methods", + "exposeHeaders": "暴露 Headers", + "allowHeaders": "允许 Headers", + "maxAge": "缓存时间", + "accessCredential": "访问凭证", + "downloadTrafficDiagram": "下载流量路径演示图", + "downloadRelay": "下载中转", + "downloadRelayDes": "开启后,用户下载文件时会通过 Cloudreve 代理。", + "download": "下载", + "downloadCdn": "下载 CDN", + "useDownloadCdn": "使用 CDN 加速下载", + "skipSign": "不为 CDN 签名文件 URL", + "skipSignDes": "如果你在 COS 域名设置中开启了 “回源鉴权”,请勾选此项。", + "cdnHost": "CDN 地址", + "downloadCdnDes": "用户访问文件时的 URL 中的主机名、协议等部分会被替换为你指定的 CDN 域名。", + "mediaExtractorProxy": "代理提取媒体信息", + "mediaExtractorProxyDes": "开启后,对于存储端提取器不支持的文件,Cloudreve 会尝试提取文件媒体信息。请在 <0>媒体处理 中配置 Cloudreve 媒体信息提取器。", + "mediaExtractorNative": "原生提取器", + "mediaExtractorOss": "智能媒体管理(IMM)", + "mediaExtractorQiniu": "智能多媒体服务", + "mediaExtractorCos": "腾讯云数据万象", + "mediaExtractorObs": "图片处理服务", + "mediaExtractorUpyun": "图片处理服务", + "nativeMediaMetaExts": "使用<0>{{name}}的文件扩展名", + "nativeMediaMetaExtsGeneralDes": "半角逗号 , 隔开,留空表示不使用<0>{{name}}。", + "nativeMediaMetaExtsRemote": "对于从机存储,默认情况下支持 EXIF 和音乐元数据,你可以通过配置覆写在从机端启用其他生成器。", + "nativeMediaMetaExtOss": "智能媒体管理(IMM)服务支持处理音频、视频和图片。处理图片无需手动配置,但如果你需要处理音频或视频,需要手动开通 IMM 并绑定到 Bucket, 请参考 <0>文档 绑定。绑定完成后请在上面加上你想要处理的音视频的扩展名。", + "nativeMediaMetaExtQiniu": "智能多媒体服务支持处理常见音频、视频和图片,无需额外配置,在上方填写你想要处理的媒体的扩展名即可。", + "nativeMediaMetaExtCos": "腾讯云数据万象服务支持处理音频、视频和图片。处理图片无需手动配置,但如果你需要处理音频或视频, 请先前往 <0>数据万象 开通并绑定存储桶,然后前往 存储桶设置 - 媒体处理 中开通美图处理服务。绑定完成后请在上面加上你想要处理的音视频的扩展名。", + "nativeMediaMetaExtObs": "图片处理服务支持<0>提取图片 EXIF。无需手动配置,在上面加上你想要处理的图片的扩展名即可。", + "nativeMediaMetaExtUpyun": "图片处理服务支持<0>提取图片 EXIF。无需手动配置,在上面加上你想要处理的图片的扩展名即可。", + "thumbProxy": "代理生成缩略图", + "thumbProxyDes": "开启后,对于不符合原生缩略图条件的文件,Cloudreve 会尝试为其生成缩略图文件,并上传到存储端。请在 <0>媒体处理 中配置 Cloudreve 缩略图生成器。", + "nativeThumbnailMaxSize": "使用原生缩略图的最大文件大小", + "nativeThumbnailMaxSizeDes": "填写 0 表示不限制,超出此大小的文件将不会使用原生缩略图。", + "nativeThumbNailsSupportAllExts": "对所有文件扩展名使用", + "nativeThumbNails": "使用原生缩略图的扩展名", + "nativeThumbNailsGeneralDes": "半角逗号 , 隔开,留空表示不使用原生缩略图。对于列表中列出的文件扩展名,Cloudreve 会使用存储端的原生缩略图。", + "nativeThumbNailsGeneralRemote": "对于从机存储,默认情况下只支持简单图像和歌曲封面缩略图,你可以通过配置覆写在从机端启用其他生成器。", + "nativeThumbNailsGeneralOss": "对于阿里云 OSS 存储,<0>图片处理服务会被用来生成缩略图。", + "nativeThumbNailsGeneralQiniu": "对于七牛云存储,<0>图片基本处理(imageView2)服务会被用来生成缩略图。", + "nativeThumbNailsGeneralCos": "对于腾讯云 COS 存储,<0>腾讯云数据万象服务会被用来生成缩略图。", + "nativeThumbNailsGeneralObs": "对于华为云 OBS 存储,<0>图片处理服务会被用来生成缩略图。", + "nativeThumbNailsGeneralUpyun": "对于又拍云存储,<0>图片处理服务会被用来生成缩略图。", + "preallocate": "预分配硬盘空间", + "preallocateDes": "开启后,用户上传文件时会预先分配硬盘空间,只在 Linux 或 Darwin 下有效。", + "sourceWebEdit": "Web 在线编辑", + "uploadRelay": "中转上传", + "uploadRelayDes": "开启后,用户的上传请求会通过 Cloudreve 中转到存储端,因为无法进行分片上传,请注意调整 Web 服务器端最大上传大小限制。", + "customProxy": "自定义代理", + "storageNode": "存储提供商", + "sourceWeb": "Web / 官方客户端", + "sourceDav": "WebDAV", + "uploadTrafficDiagram": "上传流量路径演示图", + "node": "存储节点", + "nodeDes": "请选择一个从机节点用于存储文件,你可以到 <0>存储节点列表 中创建或管理从机节点。", + "noBindedGroupWarning": "当前存储策略没有被分配给任何用户组,请前往 <0>用户组列表 为当前存储策略绑定用户组。", + "nameRuleImmutable": "修改此设置不会影响存储策略下已有文件。Blob 路径在创建后固定,即使其中魔法变量发生改变,路径也不会更新。", + "uniqueVarRequired": "请至少包含一个唯一性变量:{{uuid}}、{{randomkey8}}、{{randomkey16}}。", + "storageAndUpload": "存储与上传", + "blobFolderNaming": "Blob 存储目录", + "blobFolderNamingDes": "文件 Blob 的存放目录,可以使用 <0>魔法变量 。", + "blobName": "Blob 名称", + "blobNameDes": "文件 Blob 的名称,可以使用 <0>魔法变量,需要确保为绝对唯一,即使在短时间内多次上传同一文件。", + "basicInfo": "基本信息", + "editX": "编辑 {{name}}", + "noGroupBinded": "没有绑定任何用户组", + "create": "创建", + "addXStoragePolicy": "添加 {{type}} 存储策略", + "loadSummary": "加载统计数据", + "policySummary": "{{count}} 个文件 Blob ({{size}})", "sharp": "#", "name": "名称", "type": "类型", @@ -319,58 +786,20 @@ "oss": "阿里云 OSS", "cos": "腾讯云 COS", "onedrive": "OneDrive", - "s3": "AWS S3", + "s3": "S3 兼容", + "obs": "华为云 OBS", "refresh": "刷新", "delete": "删除", "edit": "编辑", - "editInProMode": "专家模式编辑", - "editInWizardMode": "向导模式编辑", "selectAStorageProvider": "选择存储方式", - "comparesStoragePolicies": "存储策略对比", - "comparesStoragePoliciesLink": "https://docs.cloudreve.org/use/policy/compare", - "storagePathStep": "上传路径", - "sourceLinkStep": "直链设置", - "uploadSettingStep": "上传设置", - "finishStep": "完成", - "policyAdded": "存储策略已添加", - "policySaved": "存储策略已保存", - "editLocalStoragePolicy": "修改本机存储策略", - "addLocalStoragePolicy": "添加本机存储策略", - "optional": "可选", - "pathMagicVarDes": "请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; 可用魔法变量可参考 <0>路径魔法变量列表。", - "pathOfFolderToStoreFiles": "存储目录", - "filePathMagicVarDes": "是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 文件名。文件名也可使用魔法变量, 可用魔法变量可参考 <0>文件名魔法变量列表。", - "autoRenameStoredFile": "开启重命名", - "keepOriginalFileName": "不开启", - "renameRule": "命名规则", - "next": "下一步", - "enableGettingPermanentSourceLink": "是否允许获取文件永久直链?", - "enableGettingPermanentSourceLinkDes": "开启后,用户可以请求获得能直接访问到文件内容的直链,适用于图床应用或自用。您可能还需要在用户组设置中开启此功能,用户才可以获取直链。", - "allowed": "允许", - "forbidden": "禁止", - "useCDN": "是否要对下载/直链使用 CDN?", - "useCDNDes": "开启后,用户访问文件时的 URL 中的域名部分会被替换为 CDN 域名。", - "use": "使用", - "notUse": "不使用", - "cdnDomain": "选择协议并填写 CDN 域名", - "cdnPrefix": "CDN 前缀", - "back": "上一步", - "limitFileSize": "是否限制上传的单文件大小?", - "limit": "限制", - "notLimit": "不限制", - "enterSizeLimit": "输入限制:", - "maxSizeOfSingleFile": "单文件大小限制", - "limitFileExt": "是否限制上传文件扩展名?", - "enterFileExt": "输入允许上传的文件扩展名,多个请以半角逗号 , 隔开", - "extList": "扩展名列表", - "chunkSizeLabel": "请指定分片上传时的分片大小,填写为 0 表示不使用分片上传。", - "chunkSizeDes": "启用分片上传后,用户上传的文件将会被切分成分片逐个上传到存储端,当上传中断后,用户可以选择从上次上传的分片后继续开始上传。", - "chunkSize": "分片上传大小", - "nameThePolicy": "最后一步,为此存储策略命名:", - "policyName": "存储策略名", - "finish": "完成", - "furtherActions": "要使用此存储策略,请到用户组管理页面,为相应用户组绑定此存储策略。", - "backToList": "返回存储策略列表", + "maxSizeOfSingleFile": "文件大小限制", + "maxSizeOfSingleFileDes": "单个文件的最大大小,输入限制为 0 时表示不限制单文件大小。", + "enterFileExt": "留空表示不限制文件扩展名,多个请以半角逗号 , 隔开。", + "extList": "允许的文件扩展名", + "chunkSizeDes": "请指定分片上传时的分片大小,填写为 0 表示不使用分片上传,但最大上传大小可能受限于 Web 服务器。", + "chunkSizeDesSuffix": "{{prefix}}通过分片上传,用户上传的文件将会被切分成分片逐个上传到存储端,当上传中断后,用户可以选择从上次上传的分片后继续开始上传。", + "chunkSize": "上传分片大小", + "policyName": "存储策略的展示名,也会用于向用户展示。", "magicVar": { "fileNameMagicVar": "文件名魔法变量", "pathMagicVar": "路径魔法变量", @@ -388,391 +817,353 @@ "uuidV4": "UUID V4", "date": "日期", "dateAndTime": "日期时间", + "randomNumber": "范围内的随机数", "year": "年份", "month": "月份", "day": "日", "hour": "小时", "minute": "分钟", "second": "秒", - "userUploadPath": "用户上传路径" + "path": "用户上传文件时的初始路径" }, - "storageNode": "存储端配置", - "communicationOK": "通信正常", - "editRemoteStoragePolicy": "修改从机存储策略", - "addRemoteStoragePolicy": "添加从机存储策略", - "remoteDescription": "从机存储策略允许你使用同样运行了 Cloudreve 的服务器作为存储端, 用户上传下载流量通过 HTTP 直传。", - "remoteCopyBinaryDescription": "将和主站相同版本的 Cloudreve 程序拷贝至要作为从机的服务器上。", - "remoteSecretDescription": "下方为系统为您随机生成的从机端密钥,一般无需改动,如果有自定义需求,可将您的密钥填入下方:", - "remoteSecret": "从机密钥", - "modifyRemoteConfig": "修改从机配置文件。", - "addRemoteConfigDes": " 在从机端 Cloudreve 的同级目录下新建 <0>conf.ini 文件,填入从机配置,启动/重启从机端 Cloudreve。以下为一个可供参考的配置例子,其中密钥部分已帮您填写为上一步所生成的。", - "remoteConfigDifference": "从机端配置文件格式大致与主站端相同,区别在于:", - "remoteConfigDifference1": "<0>System 分区下的 <1>mode 字段必须更改为 <2>slave。", - "remoteConfigDifference2": "必须指定 <0>Slave 分区下的 <1>Secret 字段,其值为第二步里填写或生成的密钥。", - "remoteConfigDifference3": "必须启动跨域配置,即 <0>CORS 字段的内容,具体可参考上文范例或官方文档。如果配置不正确,用户将无法通过 Web 端向从机上传文件。", - "inputRemoteAddress": "填写从机地址。", - "inputRemoteAddressDes": "如果主站启用了 HTTPS,从机也需要启用,并在下方填入 HTTPS 协议的地址。", - "remoteAddress": "从机地址", - "testCommunicationDes": "完成以上步骤后,你可以点击下方的测试按钮测试通信是否正常。", - "testCommunication": "测试从机通信", - "pathMagicVarDesRemote": "请在下方输入文件的存储目录路径,可以为绝对路径或相对路径(相对于 从机的 Cloudreve)。路径中可以使用魔法变量,文件在上传时会自动替换这些变量为相应值; 可用魔法变量可参考 <0>路径魔法变量列表。", "storageBucket": "存储空间", - "editQiniuStoragePolicy": "修改七牛存储策略", - "addQiniuStoragePolicy": "添加七牛存储策略", - "wanSiteURLDes": "在使用此存储策略前,请确保您在 参数设置 - 站点信息 - 站点URL 中填写的 地址与实际相符,并且 <0>能够被外网正常访问。", - "createQiniuBucket": "前往 <0>七牛控制面板 创建对象存储资源。", - "enterQiniuBucket": "在下方填写您在七牛创建存储空间时指定的“存储空间名称”:", + "wanSiteURLDes": "在使用此存储策略前,请确保你在 参数设置 - 站点信息 - 站点URL 中填写的 地址与实际相符,并且 <0>能够被外网正常访问。", + "enterQiniuBucket": "前往 <0>七牛控制面板 创建对象存储资源。在填写你在七牛创建存储空间时指定的“存储空间名称”。", "qiniuBucketName": "存储空间名称", - "bucketTypeDes": "在下方选择您创建的空间类型,推荐选择“私有空间”以获得更高的安全性。", + "cosObsBucketName": "存储桶名称", + "bucketType": "Bucket 读写权限", + "bucketTypeDes": "请选择你创建的存储空间的读写权限类型。", + "aclType": "访问控制类型", + "accessTypePulic": "公有读私有写", + "accessTypePrivate": "私有读写", + "accessType": "访问权限", "privateBucket": "私有", - "publicBucket": "公有", - "bucketCDNDes": "填写您为存储空间绑定的 CDN 加速域名。", + "privateDes": "Cloudreve 会对文件 URL 签名。", + "publicBucket": "公共读", + "publicStorage": "公开", + "publicDes": "不推荐选择,Cloudreve 会直接返回文件的直链,无法有效控制文件的访问权限。", + "bucketCDNDes": "填写你为存储空间绑定的 CDN 加速域名。", "bucketCDNDomain": "CDN 加速域名", - "qiniuCredentialDes": "在七牛控制面板进入 个人中心 - 密钥管理,在下方填写获得到的 AK、SK。", + "qiniuCredentialDes": "在七牛控制面板进入 个人中心 - 密钥管理,填写获得到的 AK、SK。", "ak": "AK", "sk": "SK", "cannotEnableForPrivateBucket": "私有空间开启外链功能后,还需要在用户组里设置开启“使用重定向的外链”,否则无法正常生成外链", - "limitMimeType": "是否限制上传文件 MimeType?", - "mimeTypeDes": "输入允许上传的 MimeType,多个请以半角逗号 , 隔开。七牛服务器会侦测文件内容以判断 MimeType,再用判断值跟指定值进行匹配,匹配成功则允许上传。", - "mimeTypeList": "MimeType 列表", "chunkSizeLabelQiniu": "请指定分片上传时的分片大小,范围 1 MB - 1 GB。", - "createPlaceholderDes": "是否要再用户开始上传时就创建占位符文件并扣除用户容量?开启后,可以防止用户恶意发起多个上传请求但不完成上传。", - "createPlaceholder": "创建占位符文件", - "notCreatePlaceholder": "不创建", "corsSettingStep": "跨域策略", - "corsPolicyAdded": "跨域策略已添加", - "editOSSStoragePolicy": "修改阿里云 OSS 存储策略", - "addOSSStoragePolicy": "添加阿里云 OSS 存储策略", - "createOSSBucketDes": "前往 <0>OSS 管理控制台 创建 Bucket。注意:创建空间类型只能选择 <1>标准存储 或 <2>低频访问,暂不支持 <3>归档存储。", - "ossBucketNameDes": "在下方填写您创建 Bucket 时指定的 <0>Bucket 名称:", + "corsPolicyAdded": "跨域策略已添加。", + "createOSSBucketDes": "你可前往 <0>OSS 管理控制台 创建 Bucket。只支持 <1>标准存储 和 <2>低频访问 类型的 Bucket。", "bucketName": "Bucket 名称", "publicReadBucket": "公共读", "ossEndpointDes": "转到所创建 Bucket 的概览页面,填写 <0>访问域名 栏目下 <1>外网访问 一行中间的 <2>EndPoint(地域节点)。", + "ossEndpointDesInternalHint": "如需配置内网或自定义域名 Endpoint,可在创建存储策略后设置。", + "obsEndpointCnameHint": "如需配置自定义域名 Endpoint,可在创建存储策略后设置。", "endpoint": "EndPoint", - "endpointDomainOnly": "格式不合法,只需输入域名部分即可", - "ossLANEndpointDes": "如果您的 Cloudreve 部署在阿里云计算服务中,并且与 OSS 处在同一可用区下,您可以额外指定使用内网 EndPoint 以节省流量开支。是否要在服务端发送请求时使用 OSS 内网 EndPoint?", + "ossLANEndpointDes": "留空为不使用。如果你的 Cloudreve 部署在阿里云计算服务中,并且与 OSS 处在同一可用区下,你可以额外指定使用内网 EndPoint 以节省流量开支, Cloudreve 会在条件满足时切换到内网 EndPoint 发送请求。", "intranetEndPoint": "内网 EndPoint", "ossCDNDes": "是否要使用配套的 阿里云CDN 加速 OSS 访问?", "createOSSCDNDes": "前往 <0>阿里云 CDN 管理控制台 创建 CDN 加速域名,并设定源站为刚创建的 OSS Bucket。在下方填写 CDN 加速域名,并选择是否使用 HTTPS:", - "ossAKDes": "在阿里云 <0>安全信息管理 页面获取 用户 AccessKey,并填写在下方。", + "ossAKDes": "在阿里云 <0>安全信息管理 页面获取 AccessKey。你也可以在 <1>RAM 访问控制 中创建拥有 <2>AliyunOSSFullAccess 权限的 AccessKey。", "shouldNotContainSpace": "不能含有空格", "nameThePolicyFirst": "为此存储策略命名:", "chunkSizeLabelOSS": "请指定分片上传时的分片大小,范围 100 KB ~ 5 GB。", - "ossCORSDes": "此存储策略需要正确配置跨域策略后才能使用 Web 端上传文件,Cloudreve 可以帮您自动设置,您也可以参考文档步骤手动设置。如果您已设置过此 Bucket 的跨域策略,此步骤可以跳过。", + "ossCORSDes": "此存储策略需要正确配置如上跨域策略后才能使用 Web 端上传文件,Cloudreve 可以帮你自动设置,你也可以手动设置。如果你已设置过此 Bucket 的跨域策略,此步骤可以跳过。", "letCloudreveHelpMe": "让 Cloudreve 帮我设置", "skip": "跳过", - "editUpyunStoragePolicy": "修改又拍云存储策略", - "addUpyunStoragePolicy": "添加又拍云存储策略", - "createUpyunBucketDes": "前往 <0>又拍云面板 创建云存储服务。", - "storageServiceNameDes": "在下方填写所创建的服务名称:", + "createUpyunBucketDes": "填写在 <0>又拍云面板 创建云存储服务名称。", "storageServiceName": "服务名称", - "operatorNameDes": "为此服务创建或授权有读取、写入、删除权限的操作员,然后将操作员信息填写在下方:", "operatorName": "操作员名", "operatorPassword": "操作员密码", - "upyunCDNDes": "填写为云存储服务绑定的域名,并根据实际情况选择是否使用 HTTPS:", - "upyunOptionalDes": "此步骤可保持默认并跳过,但是强烈建议您跟随此步骤操作。", - "upyunTokenDes": "前往所创建云存储服务的 功能配置 面板,转到 访问配置 选项卡,开启 Token 防盗链并设定密码。", + "tokenStatus": "Token 防盗链", + "upyunTokenDes": "强烈建议开启 Token 防盗链,前往所创建云存储服务的 <0>功能配置 面板,转到 <1>访问控制 选项卡,开启 Token 防盗链并设定密码。", "tokenEnabled": "已开启 Token 防盗链", "tokenDisabled": "未开启 Token 防盗链", - "upyunTokenSecretDes": "填写您所设置的 Token 防盗链 密钥", - "upyunTokenSecret": "Token 防盗链 密钥", - "cannotEnableForTokenProtectedBucket": "开启 Token 防盗链后无法使用直链功能", - "callbackFunctionStep": "云函数回调", - "callbackFunctionAdded": "回调云函数已添加", - "editCOSStoragePolicy": "修改腾讯云 COS 存储策略", - "addCOSStoragePolicy": "添加腾讯云 COS 存储策略", - "createCOSBucketDes": "前往 <0>COS 管理控制台 创建存储桶。", - "cosBucketNameDes": "转到所创建存储桶的基础配置页面,将 <0>空间名称 填写在下方:", - "cosBucketFormatError": "空间名格式不正确, 举例:ccc-1252109809", - "cosBucketTypeDes": "在下方选择您创建的空间的访问权限类型,推荐选择 <0>私有读写 以获得更高的安全性,私有空间无法开启“获取直链”功能。", + "upyunTokenSecretDes": "填写你所设置的 Token 防盗链密钥。", + "upyunTokenSecret": "Token 防盗链密钥", + "createCOSBucketDes": "前往 <0>COS 管理控制台 创建存储桶,转到所创建存储桶的基础配置页面,将 <1>存储桶名称 填写到上方。", + "obsBucketDes": "前往 <0>OBS 管理控制台 创建存储桶,将 <1>桶名称 填写到上方。存储桶类别只支持 <2>标准存储 或 <3>低频访问存储。", "cosPrivateRW": "私有读写", "cosPublicRW": "公共读私有写", - "cosAccessDomainDes": "转到所创建 Bucket 的基础配置,填写 <0>基本信息 栏目下 给出的 <1>访问域名。", + "cosAccessDomainDes": "在所创建 Bucket 的概况页面,填写 <0>域名信息 栏目下 给出的 <1>访问域名。你也可以使用自己绑定的源站域名或 CDN 加速域名。", + "obsEndpointDes": "在所创建存储桶的概览页面,填写 <0>域名信息 栏目下 给出的 <1>Endpoint(终端节点)。", "accessDomain": "访问域名", - "cosCDNDes": "是否要使用配套的 腾讯云CDN 加速 COS 访问?", "cosCDNDomainDes": "前往 <0>腾讯云 CDN 管理控制台 创建 CDN 加速域名,并设定源站为刚创建的 COS 存储桶。在下方填写 CDN 加速域名,并选择是否使用 HTTPS:", - "cosCredentialDes": "在腾讯云 <0>访问密钥 页面获取一对访问密钥,并填写在下方。请确保这对密钥拥有 COS 和 SCF 服务的访问权限。", - "secretId": "SecretId", - "secretKey": "SecretKey", - "cosCallbackDes": "COS 存储桶 客户端直传需要借助腾讯云的 <0>云函数 产品以确保上传回调可控。如果您打算将此存储策略自用,或者分配给可信赖用户组,此步骤可以跳过。如果是作为公有使用,请务必创建回调云函数。", - "cosCallbackCreate": "Cloudreve 可以尝试帮你自动创建回调云函数,请选择 COS 存储桶 所在地域后继续。创建可能会花费数秒钟,请耐心等待。创建前请确保您的腾讯云账号已开启云函数服务。", - "cosBucketRegion": "存储桶所在地区", - "ap-beijing": "华北地区(北京)", - "ap-chengdu": "西南地区(成都)", - "ap-guangzhou": "华南地区(广州)", - "ap-guangzhou-open": "华南地区(广州Open)", - "ap-hongkong": "港澳台地区(中国香港)", - "ap-mumbai": "亚太南部(孟买)", - "ap-shanghai": "华东地区(上海)", - "na-siliconvalley": "美国西部(硅谷)", - "na-toronto": "北美地区(多伦多)", - "applicationRegistration": "应用授权", + "cosCredentialDes": "填写在腾讯云 <0>访问密钥 页面获取一对访问密钥。请确保这对密钥拥有 COS 服务的访问权限。你也可以创建带有 <1>编程访问 能力的<2>子用户,为其赋予 COS 服务的访问权限。", + "obsCredentialDes": "填写在华为云 <0>访问密钥 页面获取一对访问密钥。你也可以创建带有 <1>编程访问 能力的<2>IAM 用户,为其赋予 <3>OBS OperateAccess 权限。", "grantAccess": "账号授权", - "warning": "警告", - "odHttpsWarning": "您必须启用 HTTPS 才能使用 OneDrive/SharePoint 存储策略;启用后同步更改 参数设置 - 站点信息 - 站点URL。", - "editOdStoragePolicy": "修改 OneDrive/SharePoint 存储策略", - "addOdStoragePolicy": "添加 OneDrive/SharePoint 存储策略", - "creatAadAppDes": "前往 <0>Azure Active Directory 控制台 (国际版账号) 或者 <1>Azure Active Directory 控制台 (世纪互联账号) 并登录,登录后进入<2>Azure Active Directory 管理面板,这里登录使用的账号和最终存储使用的 OneDrive 所属账号可以不同。", - "createAadAppDes2": "进入左侧 <0>应用注册 菜单,并点击 <1>新注册 按钮。", - "createAadAppDes3": "填写应用注册表单。其中,名称可任取;<0>受支持的帐户类型 选择为 <1>任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox);<2>重定向 URI (可选) 请选择 <3>Web,并填写 <4>{{url}}; 其他保持默认即可", - "aadAppIDDes": "创建完成后进入应用管理的 <0>概览 页面,复制 <1>应用程序(客户端) ID 并填写在下方:", + "grantAccessLater": "点击下方按钮创建存储策略后,还需要在存储策略设置页面进行账号授权。", + "odHttpsWarning": "你必须启用 HTTPS 才能使用 OneDrive/SharePoint 存储策略;启用后同步更改 参数设置 - 站点信息 - 站点URL。", + "creatAadAppDes": "前往 <0>Microsoft Entra ID 控制台 并登录,登录后进入<1>Microsoft Entra ID 管理面板,这里登录使用的账号和最终存储使用的 OneDrive 所属账号可以不同。", + "createAadAppDes2": "进入左侧 <0>应用注册 菜单,并点击 <1>新注册 按钮。填写应用注册表单。其中,名称可任取;<2>受支持的帐户类型 选择为 <3>任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox);<4>重定向 URI (可选) 请选择 <5>Web,并填写 <6>{{url}}; 其他保持默认即可。", + "aadAppIDDes": "进入应用管理的 <0>概览 页面,看到的 <1>应用程序(客户端) ID 的值。", + "entraIdApp": "Entra ID 应用信息", "aadAppID": "应用程序(客户端) ID", - "addAppSecretDes": "进入应用管理页面左侧的 <0>证书和密码 菜单,点击 <1>新建客户端密码 按钮,<2>截止期限 选择为 <3>从不。创建完成后将客户端密码的值填写在下方:", + "addAppSecretDes": "客户端密码的创建方式:进入应用管理页面左侧的 <0>证书和密码 菜单,点击 <1>新建客户端密码 按钮,<2>截止期限 选择为最长时间。客户端密码过期后,需要重新创建并将其填入存储策略设置中。", "aadAppSecret": "客户端密码", - "aadAccountCloudDes": "选择您的 Microsoft 365 账号类型:", - "multiTenant": "国际版", - "gallatin": "世纪互联版", + "aadAccountCloud": "Microsoft Graph 端点", + "aadAccountCloudDes": "请根据你使用的 Microsoft 365 账号类型选择对应的端点。", + "multiTenant": "公有(国际版)", + "gallatin": "世纪互联", "sharePointDes": "是否将文件存放在 SharePoint 中?", - "saveToSharePoint": "存到指定 SharePoint 中", "saveToOneDrive": "存到账号默认 OneDrive 驱动器中", "spSiteURL": "SharePoint 站点地址", "odReverseProxyURLDes": "是否要在文件下载时替换为使用自建的反代服务器?", "odReverseProxyURL": "反代服务器地址", - "chunkSizeLabelOd": "请指定分片上传时的分片大小,OneDrive 要求必须为 320 KiB (327,680 bytes) 的整数倍。", - "limitOdTPSDes": "是否限制服务端 OneDrive API 请求频率?", + "chunkSizeDesOd": "允许范围:5 MB ~ 5GB,OneDrive 要求必须为 320 KiB (327,680 bytes) 的整数倍。", + "limitOdTPSDes": "限制 OneDrive API 请求频率", "tps": "TPS 限制", - "tpsDes": "限制此存储策略每秒向 OneDrive 发送 API 请求最大数量。超出此频率的请求会被限速。多个 Cloudreve 节点转存文件时,它们会各自使用自己的限流桶,请根据情况按比例调低此数值。Web 端上传请求并不受此限制。", + "tpsDes": "留空表示不限制。限制此存储策略每秒向 OneDrive 发送 API 请求最大数量。超出此频率的请求会被限速。多个 Cloudreve 节点转存文件时,它们会各自使用自己的限流桶,请根据情况按比例调低此数值。Web 端上传请求并不受此限制。", "tpsBurst": "TPS 突发请求", "tpsBurstDes": "请求空闲时,Cloudreve 可将指定数量的名额预留给未来的突发流量使用。", "odOauthDes": "但是你需要点击下方按钮,并使用 OneDrive 登录授权以完成初始化后才能使用。日后你可以在存储策略列表页面重新进行授权。", "gotoAuthPage": "转到授权页面", - "s3SelfHostWarning": "S3 类型存储策略目前仅可用于自己使用,或者是给受信任的用户组使用。", - "editS3StoragePolicy": "修改 AWS S3 存储策略", - "addS3StoragePolicy": "添加 AWS S3 存储策略", - "s3BucketDes": "前往 AWS S3 控制台创建存储桶,在下方填写您创建存储桶时指定的 <0>Bucket 名称:", - "publicAccessDisabled": "阻止全部公共访问权限", - "publicAccessEnabled": "允许公共读取", - "s3EndpointDes": "(可选) 指定存储桶的 EndPoint(地域节点),填写为完整的 URL 格式,比如 <0>https://bucket.region.example.com。留空则将使用系统生成的默认接入点。", - "selectRegionDes": "选择存储桶所在的区域,或者手动输入区域代码", - "enterAccessCredentials": "获取访问密钥,并填写在下方。", - "accessKey": "AccessKey", + "s3BucketDes": "前往 AWS S3 控制台创建存储桶,在下方填写你创建存储桶时指定的 <0>Bucket 名称:", + "s3EndpointDes": "指定存储桶的 EndPoint(地域节点),填写为完整的 URL 格式,比如 <0>https://bucket.region.example.com。", + "selectRegionDes": "输入存储桶所在的区域代码,如 <0>us-east-1。对于非 AWS 的 S3 兼容存储提供商,请在其文档中查找如何填写此项。", "chunkSizeLabelS3": "请指定分片上传时的分片大小,范围 5 MB ~ 5 GB。", - "editPolicy": "编辑存储策略", - "setting":"设置项", - "value": "值", - "description": "描述", - "id": "ID", - "policyID": "存储策略编号", - "policyType": "存储策略类型", - "server": "Server", - "policyEndpoint": "存储端 Endpoint", - "bucketID": "存储桶标识", - "yes": "是", - "no": "否", - "privateBucketDes": "是否为私有空间", - "resourceRootURL": "文件资源根 URL", - "resourceRootURLDes": "预览/获取文件外链时生成 URL 的前缀", - "akDes": "AccessKey / 刷新 Token", - "maxSizeBytes": "最大单文件尺寸 (Bytes)", - "maxSizeBytesDes": "最大可上传的文件尺寸,填写为 0 表示不限制", - "autoRename": "自动重命名", - "autoRenameDes": "是否根据规则对上传物理文件重命名", - "storagePath": "存储路径", - "storagePathDes": "文件物理存储路径", - "fileName": "存储文件名", - "fileNameDes": "文件物理存储文件名", - "allowGetSourceLink": "允许获取外链", - "allowGetSourceLinkDes": "是否允许获取外链。注意,某些存储策略类型不支持,即使在此开启,获取的外链也无法使用", - "upyunToken": "又拍云防盗链 Token", - "upyunOnly": "仅对又拍云存储策略有效", - "allowedFileExtension": "允许文件扩展名", - "emptyIsNoLimit": "留空表示不限制", - "allowedMimetype": "允许的 MimeType", - "qiniuOnly": "仅对七牛存储策略有效", - "odRedirectURL": "OneDrive 重定向地址", - "noModificationNeeded": "一般添加后无需修改", - "odReverseProxy": "OneDrive 反代服务器地址", - "odOnly": "仅对 OneDrive 存储策略有效", - "odDriverID": "OneDrive/SharePoint 驱动器资源标识", - "odDriverIDDes": "仅对 OneDrive 存储策略有效,留空则使用用户的默认 OneDrive 驱动器", - "s3Region": "Amazon S3 Region", - "s3Only": "仅对 Amazon S3 存储策略有效", - "lanEndpoint": "内网 EndPoint", - "ossOnly": "仅对 OSS 存储策略有效", - "chunkSizeBytes": "上传分片大小 (Bytes)", - "chunkSizeBytesDes": "分片上传时单个分片的大小,仅部分存储策略支持", - "placeHolderWithSize": "上传前预支用户存储", - "placeHolderWithSizeDes": "是否在上传会话创建时就对用户存储进行预支,仅部分存储策略支持", - "saveChanges": "保存更改", - "s3EndpointPathStyle": "选择 S3 Endpoint 地址的格式,如果您不知道该选什么,保持默认即可。某些第三方 S3 兼容存储策略可能需要更改此选项。开启后,将会强制使用路径格式地址,比如 <0>http://s3.amazonaws.com/BUCKET/KEY。", - "usePathEndpoint": "强制路径格式", - "useHostnameEndpoint": "主机名优先", + "policyEndpoint": "Endpoint", + "s3Region": "地区代码", + "s3EndpointPathStyle": "选择是否强制使用路径格式 Endpoint。某些第三方 S3 兼容存储可能需要勾选此选项。开启后,将会强制使用路径格式地址,比如 <0>http://s3.amazonaws.com/BUCKET/KEY。", + "usePathEndpoint": "强制路径格式 Endpoint", "thumbExt": "可生成缩略图的文件扩展名", - "thumbExtDes": "留空表示使用存储策略预定义集合。对本机、S3存储策略无效" + "thumbExtDes": "留空表示使用存储策略预定义集合。对本机、S3存储策略无效", + "driverRoot": "驱动器根目录", + "driverRootDes": "选择在 OneDrive 账户中保存文件的位置。更改此选项会导致存储策略中已有文件无法访问。", + "saveToDefaultOneDrive": "保存文件到默认 OneDrive 驱动器", + "saveToSharePoint": "保存文件到 SharePoint", + "sharePointUrlDes": "输入 SharePoint 站点 URL。失去焦点后,系统将自动转换为正确的驱动器标识。" }, "node": { - "#": "#", - "name": "名称", - "status": "当前状态", - "features": "已启用功能", - "action": "操作", - "remoteDownload": "离线下载", - "nodeDisabled": "节点已暂停使用", - "nodeEnabled": "节点已启用", - "nodeDeleted": "节点已删除", - "disabled": "未启用", - "online": "在线", - "offline": "离线", - "addNewNode": "接入新节点", - "refresh": "刷新", - "enableNode": "启用节点", - "disableNode": "暂停使用节点", - "edit": "编辑", - "delete": "删除", - "slaveNodeDes": "您可以添加同样运行了 Cloudreve 的服务器作为从机端,正常运行工作的从机端可以为主机分担某些异步任务(如离线下载)。请参考下面向导部署并配置连接 Cloudreve 从机节点。<0>如果你已经在目标服务器上部署了从机存储策略,您可以跳过本页面的某些步骤,只将从机密钥、服务器地址在这里填写并保持与从机存储策略中一致即可。 在后续版本中,从机存储策略的相关配置会合并到这里。", - "overwriteDes": "; 以下为可选的设置,对应主机节点的相关参数,可以通过配置文件应用到从机节点,请根据<0>; 实际情况调整。更改下面设置需要重启从机节点后生效。", - "workerNumDes": "任务队列最多并行执行的任务数", - "parallelTransferDes": "任务队列中转任务传输时,最大并行协程数", - "chunkRetriesDes": "中转分片上传失败后重试的最大次数", - "multipleMasterDes": "一个从机 Cloudreve 实例可以对接多个 Cloudreve 主节点,只需在所有主节点中添加此从机节点并保持密钥一致即可。", - "ariaSuccess": "连接成功,Aria2 版本为:{{version}}", "slave": "从机", "master": "主机", - "aria2Des": "Cloudreve 的离线下载功能由 <0>Aria2 驱动。如需使用,请在目标节点服务器上以和运行 Cloudreve 相同的用户身份启动 Aria2, 并在 Aria2 的配置文件中开启 RPC 服务,<1>Aria2 需要和{{mode}} Cloudreve 进程共用相同的文件系统。 更多信息及指引请参考文档的 <2>离线下载 章节。", - "slaveTakeOverRemoteDownload": "是否需要此节点接管离线下载任务?", - "masterTakeOverRemoteDownload": "是否需要主机接管离线下载任务?", - "routeTaskSlave": "开启后,用户的离线下载请求可以被分流到此节点处理。", - "routeTaskMaster": "开启后,用户的离线下载请求可以被分流到主机处理。", - "enable": "启用", - "disable": "关闭", - "slaveNodeTarget": "在目标节点服务器上与节点", - "masterNodeTarget": "在与", - "aria2ConfigDes": "{{target}} Cloudreve 进程相同的文件系统环境下启动 Aria2 进程。在启动 Aria2 时,需要在其配置文件中启用 RPC 服务,并设定 RPC Secret,以便后续使用。以下为一个供参考的配置:", - "enableRPCComment": "启用 RPC 服务", - "rpcPortComment": "RPC 监听端口", - "rpcSecretComment": "RPC 授权令牌,可自行设定", - "rpcConfigDes": "推荐在日常启动流程中,先启动 Aria2,再启动节点 Cloudreve,这样节点 Cloudreve 可以向 Aria2 订阅事件通知,下载状态变更处理更及时。当然,如果没有这一流程,节点 Cloudreve 也会通过轮询追踪任务状态。", - "rpcServerDes": "在下方填写{{mode}} Cloudreve 与 Aria2 通信的 RPC 服务地址。一般可填写为 <0>http://127.0.0.1:6800/,其中端口号 <1>6800 与上文配置文件中 <2>rpc-listen-port保持一致。", - "rpcServer": "RPC 服务器地址", - "rpcServerHelpDes": "包含端口的完整 RPC 服务器地址,例如:http://127.0.0.1:6800/,留空表示不启用 Aria2 服务", - "rpcTokenDes": "RPC 授权令牌,与 Aria2 配置文件中 <0>rpc-secret 保持一致,未设置请留空。", - "aria2PathDes": "在下方填写 Aria2 用作临时下载目录的 节点上的 <0>绝对路径,节点上的 Cloudreve 进程需要此目录的读、写、执行权限。", - "aria2SettingDes": "在下方按需要填写一些 Aria2 额外参数信息。", - "refreshInterval": "状态刷新间隔 (秒)", - "refreshIntervalDes": "Cloudreve 向 Aria2 请求刷新任务状态的间隔。", - "rpcTimeout": "RPC 调用超时 (秒)", - "rpcTimeoutDes": "调用 RPC 服务时最长等待时间", - "globalOptions": "全局任务参数", - "globalOptionsDes": "创建下载任务时携带的额外设置参数,以 JSON 编码后的格式书写,您可也可以将这些设置写在 Aria2 配置文件里,可用参数请查阅官方文档", - "testAria2Des": "完成以上步骤后,你可以点击下方的测试按钮测试{{mode}} Cloudreve 向 Aria2 通信是否正常。", - "testAria2DesSlaveAddition": "在进行测试前请先确保您已进行并通过上一页面中的“从机通信测试”。", - "testAria2": "测试 Aria2 通信", - "aria2DocURL": "https://docs.cloudreve.org/use/aria2", - "nameNode": "为此节点命名:", - "loadBalancerRankDes": "为此节点指定负载均衡权重,数值为整数。某些负载均衡策略会根据此数值加权选择节点", + "noCapabilities": "未启用任何功能", + "active": "已启用", + "suspended": "已禁用", + "deleteNodeConfirmation": "确定要删除节点 {{name}} 吗?", + "editNode": "编辑节点 {{node}}", + "thisIsMasterNodes": "你正在编辑一个主机节点,即正在服务当前站点的 Cloudreve 实例。", + "enableNode": "启用节点", + "enableNodeDes": "启用节点后,节点会接受处理已开启的功能。", + "name": "名称", + "nameNode": "节点名称,也用于向用户展示。", + "type": "类型", + "server": "节点地址", + "serverDes": "用于与节点通信的地址。如果你要在此节点存储文件,此地址也会暴露给用户端用于上传文件。", + "loadBalancerRankDes": "为此节点指定负载均衡权重,数值为整数, 权重越高,节点被选中的概率越大。", "loadBalancerRank": "负载均衡权重", - "nodeSaved": "节点已保存!", - "nodeSavedFutureAction": "如果您添加了新节点,还需要在节点列表手动启动节点才能正常使用。", - "backToNodeList": "返回节点列表", - "communication": "通信配置", - "otherSettings": "杂项信息", - "finish": "完成", - "nodeAdded": "节点已添加", - "nodeSavedNow": "节点已保存", - "editNode": "编辑节点", - "addNode": "添加节点" + "slaveSecret": "从机密钥", + "slaveSecretDes": "用于从机节点与主机节点通信的密钥。需要与从机配置文件中 <0>Slave 下的 <1>Secret 保持一致。", + "testNode": "测试节点通信", + "testNodeSuccess": "节点通信成功", + "createArchiveDes": "接受创建压缩文件的任务请求。", + "extractArchiveDes": "接受解压文件的任务请求。", + "remoteDownloadDes": "接受离线下载的任务请求。启用后还需要在下方配置离线下载相关信息。", + "downloader": "下载器", + "aria2Des": "请在目标节点服务器上以和运行 Cloudreve 相同的用户/权限启动 Aria2, 并在 Aria2 的配置文件中开启 RPC 服务,更多信息及指引请参考文档的“离线下载”章节。", + "qbittorrentDes": "请在目标节点服务器上以和运行 Cloudreve 相同的用户/权限启动 qBittorrent, 并在 qBittorrent 的设置中开启“Web UI”服务,更多信息及指引请参考文档的“离线下载”章节。", + "rpcServer": "RPC 服务器地址", + "rpcServerHelpDes": "包含端口的完整 RPC 服务器地址,例如:<0>http://127.0.0.1:6800/。", + "rpcToken": "RPC 授权令牌", + "rpcTokenDes": "与 Aria2 配置文件中 <0>rpc-secret 保持一致,未设置请留空。", + "downloaderOptionDes": "在创建下载任务时额外携带的下载器配置,以 JSON 键值对格式书写,具体可参考<0>下载器官方文档。", + "refreshInterval": "状态刷新间隔 (秒)", + "refreshIntervalDes": "Cloudreve 向下载器请求刷新任务状态的间隔,实际刷新间隔也取决于“离线下载”队列的配置和繁忙程度。", + "waitForSeeding": "等待做种完成", + "waitForSeedingDes": "启用后,当离线下载任务完成后,会保留此任务在做种状态,直到在下载器配置的做种结束条件满足。等待做种发生在离线下载任务完成后,不会影响用户使用下载的文件。", + "webUIEndpoint": "Web UI 地址", + "webUIEndpointDes": "qBittorrent 的 Web UI 地址,比如 <0>http://127.0.0.1:8080/。", + "tempPath": "临时下载目录", + "tempPathDes": "节点上用于临时存放离线下载文件的目录,节点上的 Cloudreve 进程需要此目录的读、写、执行权限,下载器也要能够访问此目录。留空会使用默认的临时文件路径。", + "webUIUsername": "Web UI 用户名", + "webUIPassword": "Web UI 密码", + "webUICredDes": "如果未启用认证,此处请留空。", + "downloaderTestPass": "成功连接到下载器,版本:{{version}}", + "testDownloader": "测试下载器通信", + "addNewNode": "新建节点", + "nameTheNode": "为节点命名:", + "runCrSlave": "在节点上运行和主站相同版本的 Cloudreve,并使用以下配置文件启动:", + "keepIfUpload": "如果你未来需要使用此节点存储,请保留下面的跨域配置。", + "storeFiles": "存储文件", + "storeFilesDes": "使用此节点存储用户文件。", + "storeFilesHint": "如果你想使用此节点存储文件,清前往 <0>存储策略 页面新建从机存储策略,并选择此节点。", + "runCrWithConfig": "将上述文件保存为 <0>config.ini 文件,并使用此文件启动 Cloudreve:<0>./cloudreve -c config.ini。一个从机 Cloudreve 实例可以对接多个 Cloudreve 主节点,只需在所有主节点中添加此从机节点并保持密钥一致即可。", + "inputServer": "输入节点的地址:", + "testButton": "可以点击下面按钮测试通信是否正常。", + "hostHeaderHint": "如果有签名错误,请检查从机前置反代是否呈递了 <0>Host 头。", + "features": "已启用功能", + "remoteDownload": "离线下载", + "refresh": "刷新" }, "group": { + "countUser": "统计", + "anonymous": "未登录访客用户组", + "sysGroup": "系统用户组", + "adminGroup": "管理员用户组", "#": "#", "name": "名称", "type": "存储策略", "count": "下属用户数", "size": "最大容量", - "action": "操作", - "deleted": "用户组已删除", - "new": "新建用户组", - "aria2FormatError": "Aria2 设置项格式错误", - "atLeastOnePolicy": "至少要为用户组选择一个存储策略", - "added": "用户组已添加", - "saved": "用户组已保存", - "editGroup": "编辑 {{group}}", "nameOfGroup": "用户组名", - "nameOfGroupDes": "用户组的名称", - "storagePolicy": "存储策略", - "storageDes": "指定用户组的存储策略。", + "nameOfGroupDes": "用户组的名称,用于向用户展示。", + "availablePolicies": "可用存储策略", + "availablePoliciesDes": "指定用户组可用的存储策略,修改此设置不会影响用户已上传的文件。", + "availablePolicyDesPro": "可多选,用户可在选定范围内自由切换存储策略.", "initialStorageQuota": "初始容量", - "initialStorageQuotaDes": "用户组下的用户初始可用最大容量", - "downloadSpeedLimit": "下载限速", - "downloadSpeedLimitDes": "填写为 0 表示不限制。开启限制后,此用户组下的用户下载所有支持限速的存储策略下的文件时,下载最大速度会被限制。", - "bathSourceLinkLimit": "批量生成外链数量限制", - "bathSourceLinkLimitDes": "对于支持的存储策略下的文件,允许用户单次批量获取外链的最大文件数量,填写为 0 表示不允许批量生成外链。", - "allowCreateShareLink": "允许创建分享", - "allowCreateShareLinkDes": "关闭后,用户无法创建分享链接", - "allowDownloadShare": "允许下载分享", - "allowDownloadShareDes": "关闭后,用户无法下载别人创建的文件分享", + "initialStorageQuotaDes": "用户组下的用户初始可用最大容量。", + "isAdmin": "管理员用户组", + "isAdminDes": "开启后,用户组下的用户将拥有管理员权限。", + "share": "分享", + "allowCreateShareLink": "创建分享链接", + "allowCreateShareLinkDes": "关闭后,用户无法创建分享链接。", + "shareFree": "无需购买分享链接", + "shareFreeDes": "开启后,用户无需购买即可访问所有付费分享链接。", + "fileManagement": "文件管理", "allowWabDAV": "WebDAV", - "allowWabDAVDes": "关闭后,用户无法通过 WebDAV 协议连接至网盘", + "allowWabDAVDes": "关闭后,用户无法通过 WebDAV 协议连接至网盘。", "allowWabDAVProxy": "WebDAV 代理", - "allowWabDAVProxyDes": "启用后, 用户可以配置 WebDAV 代理下载文件的流量", - "disableMultipleDownload": "禁止多次下载请求", - "disableMultipleDownloadDes": "只针对本机存储策略有效。开启后,用户无法使用多线程下载工具。", + "allowWabDAVProxyDes": "启用后,用户可以配置 WebDAV 下载经由 Cloudreve 中转。", + "compressTask": "压缩/解压缩文件", + "compressTaskDes": "开启后,用户可以在线压缩/解压缩文件。", + "compressSize": "待压缩文件最大大小", + "compressSizeDes": "用户可创建的压缩任务的文件最大总大小,填写为 0 表示不限制。这一限制在创建压缩任务时不会检查,当执行时已处理原始文件总大小超过此限制时,任务会失败。", + "decompressSize": "待解压文件最大大小", + "decompressSizeDes": "用户可创建的解压缩任务的文件最大总大小,填写为 0 表示不限制。", "allowRemoteDownload": "离线下载", - "allowRemoteDownloadDes": "是否允许用户创建离线下载任务", - "aria2Options": "Aria2 任务参数", - "aria2OptionsDes": "此用户组创建离线下载任务时额外携带的参数,以 JSON 编码后的格式书写,您可也可以将这些设置写在 Aria2 配置文件里,可用参数请查阅官方文档", - "aria2BatchSize": "Aria2 批量下载最大数量", - "aria2BatchSizeDes": "允许用户同时进行的离线下载任务数量,填写为 0 或留空表示不限制。", + "allowRemoteDownloadDes": "是否允许用户创建离线下载任务。如需使用离线下载,还需要在 <0>节点列表 中有开启离线下载功能的节点。", + "aria2Options": "下载器任务参数", + "aria2OptionsDes": "qBittorrent 或 Aria2 下载器的任务额外配置参数,以 JSON 编码后的键-值格式书写,可用参数请查阅官方文档。", + "aria2BatchSize": "批量离线下载最大数量", + "aria2BatchSizeDes": "批量创建离线下载时的最大数量,填写为 0 表示不限制。", + "migratePolicy": "存储策略转移", + "migratePolicyDes": "是否用户创建存储策略转移任务。", + "advanceDelete": "高级文件删除选项", + "advanceDeleteDes": "开启后,用户在前台删除文件时可以选择是否保留物理文件,请只开放给可信用户组。", + "allowSelectNode": "允许选择节点", + "allowSelectNodeDes": "开启后,用户可以在创建任务前选择处理节点;关闭后,系统会在用户组允许的节点下自动分配节点。", + "allowedNodes": "可用节点", + "allowedNodesDes": "指定用户组可用的任务处理节点,留空表示全部节点都可用。用户只能在此列表内选择或被负载均衡分配节点。目前覆盖的任务范围是:离线下载、文件压缩或解压缩。其他任务会分配给主机处理。", + "allNodes": "所有节点", + "esclateAnonymity": "提升匿名用户权限", + "esclateAnonymityDes": "开启后,用户可以为匿名用户设置更高权限(修改/创建/删除);关闭后,用户最高只能赋予匿名用户只读权限。更改此设置不会影响已设置的分享链接或文件。", + "allowDownloadShare": "访问分享链接", + "allowDownloadShareDes": "关闭后,用户无法查看别人的分享链接。此项设置优先级高于分享链接的权限设置。", + "deletedNode": "已删除节点 #{{id}}", + "maxWalkedFiles": "最大遍历文件数", + "maxWalkedFilesDes": "在某些需要深层遍历文件的操作中,最大允许遍历的文件数。", + "trashBinDuration": "回收站保留时间(秒)", + "trashBinDurationDes": "回收站中文件的保留时长,超期后文件将被彻底删除。更改此设置不会影响已经在回收站中的文件。", "serverSideBatchDownload": "服务端打包下载", "serverSideBatchDownloadDes": "是否允许用户多选文件使用服务端中转打包下载,关闭后,用户仍然可以使用纯 Web 端打包下载功能。", - "compressTask": "压缩/解压缩 任务", - "compressTaskDes": "是否用户创建 压缩/解压缩 任务", - "compressSize": "待压缩文件最大大小", - "compressSizeDes": "用户可创建的压缩任务的文件最大总大小,填写为 0 表示不限制", - "decompressSize": "待解压文件最大大小", - "decompressSizeDes": "用户可创建的解压缩任务的文件最大总大小,填写为 0 表示不限制", - "redirectedSource": "使用重定向的外链", - "redirectedSourceDes": "开启后,用户获取的文件外链将由 Cloudreve 中转,链接较短。关闭后,用户获取的文件外链会变成文件的原始链接。部分存储策略获取的非中转外链无法保持永久有效,请参阅 <0>比较存储策略。", - "advanceDelete": "允许使用高级文件删除选项", - "advanceDeleteDes": "开启后,用户在前台删除文件时可以选择是否强制删除、是否仅解除物理链接。这些选项与后台管理面板删除文件时类似,请只开放给可信用户组。" + "uploadDownload": "上传和下载", + "getDirectLink": "获取直链", + "getDirectLinkDes": "是否允许用户获取文件的直链。", + "bathSourceLinkLimit": "批量生成外直链量限制", + "bathSourceLinkLimitDes": "允许用户单次批量获取直链的最大文件数量,填写为 0 表示不允许获取直链。", + "redirectedSource": "使用重定向的直链", + "redirectedSourceDes": "推荐开启。开启后,用户获取的文件直链将由 Cloudreve 中转,链接较短。关闭后,用户获取的文件直链会变成文件的原始链接,且与文件版本绑定。部分存储策略在某些设置下获取的非中转直链无法保持永久有效,请参阅 Cloudreve 文档。", + "downloadSpeedLimit": "下载限速", + "downloadSpeedLimitDes": "填写为 0 表示不限制。开启限制后,用户下载所有支持限速的存储策略下的文件时,下载最大速度会被限制。", + "anonymousHint": "此用户组对应着未登录的匿名访客。", + "create": "新建", + "copyFromExisting": "从现有用户组复制?", + "notCopy": "不复制", + "confirmDelete": "确认要删除用户组 {{group}}?", + "new": "新建用户组", + "editGroup": "编辑 {{group}}" }, "user": { + "createdAt": "创建日期", + "originUserGroup": "原始用户组", + "originUserGroupDes": "用户在购买用户组前所属的用户组,当前用户组到期后会回退到此用户组。", + "noOriginUserGroup": "无", + "groupExpired": "用户组过期日期", + "groupExpiredDes": "ISO8601 格式的用户组到期日期,留空表示当前用户组永久有效。", + "openUserFiles": "打开用户文件", + "id": "ID", + "idValue": "{{id}} ({{hash_id}})", + "avatar": "头像", + "removeAvatar": "移除头像", + "userDialogTitle": "用户详情", + "2FAEnabled": "已启用二步验证", + "qqEnabled": "已绑定 QQ", + "logtoEnabled": "已绑定 Logto", "deleted": "用户已删除", "new": "新建用户", "filter": "过滤", + "emptyNoFilter": "留空表示不过滤此项。", "selectedObjects": "已选择 {{num}} 个对象", "nick": "昵称", "email": "Email", "group": "用户组", "status": "状态", "usedStorage": "已用空间", - "active": "正常", - "notActivated": "未激活", - "banned": "被封禁", - "bannedBySys": "超额封禁", + "status_active": "正常", + "status_inactive": "未激活", + "status_manual_banned": "手动封禁", + "status_sys_banned": "系统封禁", "toggleBan": "封禁/解封", "filterCondition": "过滤条件", "all": "全部", "userStatus": "用户状态", - "searchNickUserName": "搜索 昵称 / 用户名", "apply": "应用", - "added": "用户已添加", - "saved": "用户已保存", "editUser": "编辑 {{nick}}", "password": "密码", "passwordDes": "留空表示不修改", "groupDes": "用户所属用户组", - "2FASecret": "二步验证密钥", - "2FASecretDes": "用户二步验证器的密钥,清空表示未启用。" + "2FA": "二步验证", + "notEnabled": "未启用", + "reset2Fa": "关闭", + "reset": "重置", + "confirmDelete": "确认要删除用户 {{user}}?", + "deleteXUsers": "删除 {{num}} 个用户", + "confirmBatchDelete": "确认要删除 {{num}} 个用户?", + "calibrateStorage": "校准存储空间", + "calibrateStorageSuccess": "存储空间校准成功" }, "file": { + "deleteXFiles": "删除 {{num}} 个文件", + "confirmBatchDelete": "确定要删除 {{num}} 个文件?", + "confirmDelete": "确认要删除文件 {{file}}?", + "haveShares": "拥有分享链接", + "haveDirectLinks": "拥有中转直链", + "directLinkId": "链接标识", + "directLinks": "中转直链", + "noRecords": "没有记录", + "speed": "限速", + "downloads": "下载次数", + "shareLink": "分享链接", + "shareLinkNum": "{{num}} 个 (<0>查看)", + "blobType": "类型", + "noEntities": "没有 Blob", + "blobs": "Blobs", + "creator": "创建者", + "source": "源", + "key": "键", + "value": "值", + "isPublic": "公开", + "noMetadata": "没有元数据", + "metadata": "元数据", + "id": "ID", + "primaryStoragePolicy": "首选存储策略", + "fileDialogTitle": "文件详情", "name": "文件名", "deleteAsync": "删除任务将在后台执行", - "import": "从外部导入", "forceDelete": "强制删除", "size": "大小", - "uploader": "上传者", + "sizeUsed": "占用空间", + "uploader": "所有者", "createdAt": "创建于", "uploading": "上传中", "unknownUploader": "未知", - "uploaderID": "上传者 ID", + "uploaderID": "所有者 ID", "searchFileName": "搜索文件名", "storagePolicy": "存储策略", "selectTargetUser": "请先选择目标用户", - "importTaskCreated": "导入任务已创建,您可以在“持久任务”中查看执行情况", + "importTaskCreated": "导入任务已创建,你可以在“持久任务”中查看执行情况", "manuallyPathOnly": "选择的存储策略只支持手动输入路径", "selectFolder": "选择目录", "importExternalFolder": "导入外部目录", - "importExternalFolderDes": "您可以将存储策略中已有文件、目录结构导入到 Cloudreve 中,导入操作不会额外占用物理存储空间,但仍会正常扣除用户已用容量空间,空间不足时将停止导入。", + "importExternalFolderDes": "你可以将存储策略中已有文件、目录结构导入到 Cloudreve 中,导入操作不会额外占用物理存储空间,但仍会正常扣除用户已用容量空间,空间不足时将停止导入。", "storagePolicyDes": "选择要导入文件目前存储所在的存储策略", "targetUser": "目标用户", "targetUserDes": "选择要将文件导入到哪个用户的文件系统中,可通过昵称、邮箱搜索用户", @@ -786,27 +1177,95 @@ "createImportTask": "创建导入任务", "unlink": "解除关联(保留物理文件)" }, + "entity": { + "refenenceCount": "引用次数", + "waitForRecycle": "等待回收", + "entityDialogTitle": "Blob 详情", + "uploadSessionID": "上传会话 ID", + "referredFiles": "关联文件", + "confirmBatchDelete": "确认要删除 {{num}} 个 Blob?", + "deleteXEntities": "删除 {{num}} 个 Blob", + "forceDelete": "强制删除", + "forceDeleteDes": "无论物理文件是否删除成功,都会删除 Blob 记录。" + }, + "event": { + "initiator": "发起者", + "event": "事件", + "userID": "用户 ID", + "ip": "IP", + "type": "类型", + "correlationId": "请求 ID", + "fileID": "文件 ID", + "emailSend": "发送邮件 “{{title}}” 到 {{email}}", + "emailFailed": "邮件队列启动失败", + "signinFailed": "登录失败: {{reason}}", + "createDavAccount": "创建 WebDAV 账户: {{account}}", + "updateDavAccount": "更新 WebDAV 账户: {{account}}", + "deleteDavAccount": "删除 WebDAV 账户: {{account}}", + "pointsChange": "积分变化: {{points}}", + "storageAdded": "购买了 {{size}} 容量", + "nickChange": "昵称从 {{old}} 改为 {{new}}", + "eventDialogTitle": "事件详情", + "userAgent": "用户代理", + "linkedUser": "关联用户", + "datetime": "时间", + "linkedFile": "关联文件", + "linkedEntity": "关联 Blob", + "linkedShare": "关联分享", + "rawContent": "原始记录", + "confirmDelete": "确认要删除这个事件?", + "deleteXEvents": "删除 {{num}} 个事件", + "confirmBatchDelete": "确认要删除 {{num}} 个事件?" + }, "share": { - "deleted": "分享已删除", - "objectName": "对象名", + "confirmBatchDelete": "确认要删除 {{num}} 个分享?", + "confirmDelete": "确认要删除这个分享?", + "deleteXShares": "删除 {{num}} 个分享", + "shareDialogTitle": "分享详情", + "shareLink": "分享链接", + "deleted": "文件已删除", + "srcFileName": "源文件", "views": "浏览", "downloads": "下载", "price": "积分", "autoExpire": "自动过期", "owner": "分享者", "createdAt": "分享于", - "public": "公开", - "private": "私密", - "afterNDownloads":"{{num}} 次下载后", + "private": "从个人主页隐藏", + "yes": "是", + "no": "否", + "afterNDownloads": "{{num}} 次下载后", "none": "无", "srcType": "源文件类型", "folder": "目录", "file": "文件" }, "task": { - "taskDeleted": "任务已删除", - "howToConfigAria2": "如何配置离线下载?", - "srcURL": "源地址", + "confirmDelete": "确认要删除这个任务?", + "confirmBatchDelete": "确认要删除 {{num}} 个任务?", + "deleteXTasks": "删除 {{num}} 个任务", + "blobID": "Blob ID", + "retryIndex": "重试序号", + "entityError": "回收失败的 Blob", + "updatedAt": "更新于", + "taskDialogTitle": "任务详情", + "explicitEntityRecycle": "显式回收文件 Blob: {{blobs}}", + "entityRecycleRoutine": "定时扫描回收文件 Blob", + "mediaMetadata": "提取 Blob <0>#{{entityID}} 的媒体信息", + "uploadSentinelCheck": "检查上传会话 {{uploadSessionID}} 状态", + "remoteDownload": "离线下载:", + "owner": "所有者", + "content": "内容", + "status": "状态", + "create_archive": "创建压缩文件", + "extract_archive": "解压文件", + "relocate": "转移存储策略", + "remote_download": "离线下载", + "media_meta": "媒体信息提取", + "entity_recycle_routine": "Blob 扫描回收", + "explicit_entity_recycle": "显式 Blob 回收", + "upload_sentinel_check": "上传哨兵检查", + "type": "类型", "node": "处理节点", "createdBy": "创建者", "ready": "就绪", @@ -817,11 +1276,165 @@ "finished": "完成", "canceled": "取消/停止", "unknown": "未知", - "aria2Des": "Cloudreve 的离线下载支持主从分散模式。您可以配置多个 Cloudreve 从机节点,这些节点可以用来处理离线下载任务,分散主节点的压力。当然,您也可以配置只在主节点上处理离线下载任务,这是最简单的一种方式。", - "masterAria2Des": "如果您只需要为主机启用离线下载功能,请 <0>点击这里 编辑主节点;", - "slaveAria2Des": "如果您想要在从机节点上分散处理离线下载任务,请 <0>点击这里 添加并配置新节点。", - "editGroupDes": "当你添加多个可用于离线下载的节点后,主节点会将离线下载请求轮流发送到这些节点处理。节点离线下载配置完成后,您可能还需要 <0>到这里 编辑用户组,为对应用户组开启离线下载权限。", - "lastProgress": "最后进度", "errorMsg": "错误信息" + }, + "payment": { + "tradeNo": "交易单号", + "productType": "商品类型", + "providerID": "支付方式", + "status": "状态", + "deleteXPayments": "删除 {{num}} 个订单" + }, + "vas": { + "confirmDelete": "确认要删除这些订单?", + "vas": "增值服务", + "reports": "举报", + "orders": "订单", + "initialFiles": "初始文件", + "initialFilesDes": "指定用户注册后初始拥有的文件。输入文件 ID 搜索并添加现有文件。", + "filterEmailProvider": "过滤注册邮箱域", + "filterEmailProviderDisabled": "不启用", + "filterEmailProviderWhitelist": "白名单", + "filterEmailProviderBlacklist": "黑名单", + "filterEmailProviderDes": "只允许使用特定的邮箱注册站点,第三方 SSO 登录不受此限制。", + "filterEmailProviderRule": "邮箱域过滤规则", + "filterEmailProviderRuleDes": "多个域请使用半角逗号隔开。", + "qqConnect": "QQ 互联", + "qqConnectHint": "在 <0>QQ 互联开放平台 创建应用时,回调地址请填写:{{url}}", + "enableQQConnect": "开启QQ互联", + "enableQQConnectDes": "是否允许绑定QQ、使用QQ登录本站", + "loginWithoutBinding": "未绑定时可直接登录", + "loginWithoutBindingDes": "开启后,如果用户使用了第三方登录,但是没有已绑定的注册用户,系统会为其创建用户并登录。这种方式创建的用户日后只能使用第三方登录。", + "appid": "APP ID", + "appidDes": "应用管理页面获取到的的 APP ID", + "appKey": "APP KEY", + "appKeyDes": "应用管理页面获取到的的 APP KEY", + "overuseReminder": "超额提醒", + "overuseReminderDes": "用户因增值服务过期,容量超出限制后发送的提醒邮件模板", + "vasSetting": "支付/杂项设置", + "storagePack": "容量包", + "purchasableGroups": "可购用户组", + "giftCodes": "兑换码", + "enable": "开启", + "appID": "App- ID", + "appIDDes": "当面付应用的 APPID", + "rsaPrivate": "RSA 应用私钥", + "rsaPrivateDes": "当面付应用的 RSA2 (SHA256) 私钥,一般是由你自己生成。详情参考 <0>生成 RSA 密钥。", + "alipayPublicKey": "支付宝公钥", + "alipayPublicKeyDes": "由支付宝提供,可在 应用管理 - 应用信息 - 接口加签方式 中获取。", + "wechatPay": "微信官方扫码支付", + "applicationID": "应用 ID", + "applicationIDDes": "直连商户申请的公众号或移动应用 appid", + "merchantID": "直连商户号", + "merchantIDDes": "直连商户的商户号,由微信支付生成并下发。", + "apiV3Secret": "API v3 密钥", + "apiV3SecretDes": "商户需先在【商户平台】-【API安全】的页面设置该密钥,请求才能通过微信支付的签名校验。密钥的长度为 32 个字节。", + "mcCertificateSerial": "商户证书序列号", + "mcCertificateSerialDes": "登录商户平台【API安全】-【API证书】-【查看证书】,可查看商户 API 证书序列号。", + "mcAPISecret": "商户API 私钥", + "mcAPISecretDes": "私钥文件 apiclient_key.pem 的内容。", + "payjs": "PAYJS 微信支付", + "payjsWarning": "此服务由第三方平台 <0>PAYJS 提供, 产生的任何纠纷与 Cloudreve 开发者无关。", + "mcNumber": "商户号", + "mcNumberDes": "可在 PAYJS 管理面板首页看到", + "communicationSecret": "通信密钥", + "otherSettings": "杂项设置", + "banBufferPeriod": "封禁缓冲期 (秒)", + "banBufferPeriodDes": "用户保持容量超额状态的最长时长,超出时长该用户会被系统冻结。", + "allowSellShares": "允许为分享定价", + "allowSellSharesDes": "开启后,用户可为分享设定积分价格,下载需要扣除积分。", + "creditPriceRatio": "积分到账比率 (%)", + "creditPriceRatioDes": "购买下载设定价格的分享,分享者实际到账的积分比率。", + "creditPrice": "积分价格 (分)", + "creditPriceDes": "充值积分时的价格", + "add": "添加", + "name": "名称", + "price": "单价", + "duration": "时长", + "size": "大小", + "actions": "操作", + "orCredits": " 或 {{num}} 积分", + "highlight": "突出展示", + "yes": "是", + "no": "否", + "productName": "商品名", + "qyt": "数量", + "code": "兑换码", + "status": "状态", + "invalidProduct": "已失效商品", + "used": "已使用", + "notUsed": "未使用", + "generatingResult": "生成结果", + "addStoragePack": "添加容量包", + "editStoragePack": "编辑容量包", + "productNameDes": "商品展示名称", + "packSizeDes": "容量包的大小", + "durationDay": "有效期 (天)", + "durationDayDes": "每个容量包的有效期", + "priceYuan": "单价 (元)", + "packPriceDes": "容量包的单价", + "priceCredits": "单价 (积分)", + "priceCreditsDes": "使用积分购买时的价格,填写为 0 表示不能使用积分购买", + "editMembership": "编辑可购用户组", + "addMembership": "添加可购用户组", + "group": "用户组", + "groupDes": "购买后升级的用户组", + "durationGroupDes": "购买后升级的用户组单位购买时间的有效期", + "groupPriceDes": "用户组的单价", + "productDescription": "商品描述 (一行一个)", + "productDescriptionDes": "购买页面展示的商品描述", + "highlightDes": "开启后,在商品选择页面会被突出展示", + "generateGiftCode": "生成兑换码", + "numberOfCodes": "生成数量", + "numberOfCodesDes": "激活码批量生成数量", + "linkedProduct": "对应商品", + "productQyt": "商品数量", + "productQytDes": "对于积分类商品,此处为积分数量,其他商品为时长倍数", + "freeDownload": "免积分下载分享", + "freeDownloadDes": "开启后,用户可以免费下载需付积分的分享", + "credits": "积分", + "markSuccessful": "标记成功", + "markAsResolved": "标记为已处理", + "reportedContent": "举报对象", + "reason": "原因", + "description": "补充描述", + "reportTime": "举报时间", + "invalid": "[已失效]", + "deleteShare": "删除分享", + "orderDeleted": "订单记录已删除", + "orderName": "订单名", + "product": "商品", + "orderNumber": "订单号", + "paidBy": "支付方式", + "orderOwner": "创建者", + "amount": "金额", + "unpaid": "未支付", + "paid": "已支付", + "shareLink": "分享链接", + "mobileApp": "移动客户端", + "showAppPromotion": "展示客户端引导页面", + "showAppPromotionDes": "开启后,用户可以在 “连接与挂载” 页面中看到移动客户端的使用引导。", + "customPaymentName": "付款方式名称", + "customPaymentNameDes": "用于展示给用户的付款方式名称", + "customPaymentSecretDes": "Cloudreve 用于签名付款请求的密钥。", + "customPaymentEndpoint": "支付接口地址", + "customPaymentEndpointDes": "创建支付订单时请求的接口 URL", + "appFeedback": "反馈页面 URL", + "appForum": "用户论坛 URL", + "appLinkDes": "用于在 App 设置页面展示,留空即不展示链接按钮,仅当 VOL 授权有效时此项设置才会生效。" + }, + "pro": { + "title": "Pro 版本专属功能", + "description": "您尝试访问的功能仅在 Cloudreve Pro 版本中可用,升级以解锁所有高级功能。", + "proInclude": "Pro 版本包含:", + "shareLinkCollabration": "分享链接协同编辑", + "filePermission": "文件权限管理", + "multipleStoragePolicy": "多存储策略和目录存储策略切换", + "auditAndActivity": "文件和系统活动日志", + "vasService": "增值服务和积分系统", + "sso": "SSO 单点登录", + "more": "......", + "later": "稍后再说", + "learnMore": "了解 Pro 版本详情" } -} +} \ No newline at end of file diff --git a/public/locales/zh-CN/image_editor.json b/public/locales/zh-CN/image_editor.json new file mode 100644 index 0000000..1aba4c6 --- /dev/null +++ b/public/locales/zh-CN/image_editor.json @@ -0,0 +1,113 @@ +{ + "name": "名称", + "save": "保存", + "saveAs": "另存为", + "back": "后退", + "loading": "加载中...", + "resetOperations": "重置/删除所有操作", + "changesLoseWarningHint": "如果您按下“重置”按钮,您的更改将丢失。确定要继续吗?", + "discardChangesWarningHint": "如果关闭窗口,您的最后更改将不会被保存。", + "cancel": "取消", + "apply": "应用", + "warning": "警告", + "confirm": "确认", + "discardChanges": "放弃更改", + "undoTitle": "撤消上次操作", + "redoTitle": "重做上次操作", + "showImageTitle": "显示原始图像", + "zoomInTitle": "放大", + "zoomOutTitle": "缩小", + "toggleZoomMenuTitle": "切换缩放菜单", + "adjustTab": "调整", + "finetuneTab": "微调", + "filtersTab": "滤镜", + "watermarkTab": "水印", + "annotateTabLabel": "注释", + "resize": "调整大小", + "resizeTab": "调整大小", + "imageName": "图片名称", + "invalidImageError": "提供的图像无效。", + "uploadImageError": "上传图像时出错。", + "areNotImages": "不是图像", + "isNotImage": "不是图像", + "toBeUploaded": "待上传", + "cropTool": "裁剪", + "original": "原始", + "custom": "自定义", + "square": "正方形", + "landscape": "横向", + "portrait": "纵向", + "ellipse": "椭圆", + "classicTv": "经典电视", + "cinemascope": "宽银幕电影", + "arrowTool": "箭头", + "blurTool": "模糊", + "brightnessTool": "亮度", + "contrastTool": "对比度", + "ellipseTool": "椭圆", + "unFlipX": "取消翻转 X", + "flipX": "翻转 X", + "unFlipY": "取消翻转 Y", + "flipY": "翻转 Y", + "hsvTool": "HSV", + "hue": "色调", + "brightness": "亮度", + "saturation": "饱和度", + "value": "数值", + "imageTool": "图像", + "importing": "导入...", + "addImage": "+ 添加图片", + "uploadImage": "上传图片", + "fromGallery": "来自画廊", + "lineTool": "线", + "penTool": "画笔", + "polygonTool": "多边形", + "sides": "侧面", + "rectangleTool": "长方形", + "cornerRadius": "拐角半径", + "resizeWidthTitle": "宽度(以像素为单位)", + "resizeHeightTitle": "高度(以像素为单位)", + "toggleRatioLockTitle": "切换比率锁定", + "resetSize": "重置为原始图像大小", + "rotateTool": "旋转", + "textTool": "文本", + "textSpacings": "文字间距", + "textAlignment": "文本对齐", + "fontFamily": "字体", + "size": "尺寸", + "letterSpacing": "字母间距", + "lineHeight": "线高", + "warmthTool": "色温", + "addWatermark": "+ 添加水印", + "addTextWatermark": "+ 添加文字水印", + "addWatermarkTitle": "选择水印类型", + "uploadWatermark": "上传水印", + "addWatermarkAsText": "添加为文本", + "padding": "内间距", + "paddings": "内间距", + "shadow": "阴影", + "horizontal": "水平", + "vertical": "垂直", + "blur": "模糊", + "opacity": "不透明度", + "transparency": "透明度", + "position": "位置", + "stroke": "线条", + "saveAsModalTitle": "另存为", + "extension": "扩展", + "format": "格式", + "nameIsRequired": "文件名为必填项。", + "quality": "质量", + "imageDimensionsHoverTitle": "保存的图像尺寸(宽x高)", + "cropSizeLowerThanResizedWarning": "请注意,所选的裁剪区域低于应用的调整大小,这可能会导致质量下降", + "actualSize": "实际尺寸(100%)", + "fitSize": "适合尺寸", + "addImageTitle": "选择要添加的图像...", + "mutualizedFailedToLoadImg": "加载图像失败。", + "tabsMenu": "菜单", + "download": "下载", + "width": "宽度", + "height": "高度", + "plus": "+", + "cropItemNoEffect": "此裁剪项目没有可用的预览" +} \ No newline at end of file diff --git a/public/locales/zh-CN/markdown_editor.json b/public/locales/zh-CN/markdown_editor.json new file mode 100644 index 0000000..1adb754 --- /dev/null +++ b/public/locales/zh-CN/markdown_editor.json @@ -0,0 +1,105 @@ +{ + "frontmatterEditor": { + "title": "编辑前置元数据", + "key": "键", + "value": "值", + "addEntry": "添加项目" + }, + "dialogControls": { + "save": "保存", + "cancel": "取消" + }, + "uploadImage": { + "uploadInstructions": "从您的设备中上传图片:", + "addViaUrlInstructions": "或从网址新增图片:", + "autoCompletePlaceholder": "选择或粘贴图片", + "alt": "替代文本:", + "title": "标题:" + }, + "imageEditor": { + "editImage": "编辑图片" + }, + "createLink": { + "url": "网址", + "urlPlaceholder": "选择或粘贴网址", + "title": "标题", + "saveTooltip": "设置网址", + "cancelTooltip": "取消更改" + }, + "linkPreview": { + "open": "在新窗口中打开 {{url}}", + "edit": "编辑链接网址", + "copyToClipboard": "复制到剪贴板", + "copied": "已复制!", + "remove": "移除链接" + }, + "table": { + "deleteTable": "删除表格", + "columnMenu": "列菜单", + "textAlignment": "文字对齐", + "alignLeft": "左对齐", + "alignCenter": "居中对齐", + "alignRight": "右对齐", + "insertColumnLeft": "在当前列左侧插入一列", + "insertColumnRight": "在当前列右侧插入一列", + "deleteColumn": "删除此列", + "rowMenu": "行菜单", + "insertRowAbove": "在当前行上方插入一行", + "insertRowBelow": "在当前行下方插入一行", + "deleteRow": "删除此行" + }, + "toolbar": { + "blockTypes": { + "paragraph": "段落", + "quote": "引用", + "heading": "标题 {{level}}" + }, + "blockTypeSelect": { + "selectBlockTypeTooltip": "选择块类型", + "placeholder": "块类型" + }, + "toggleGroup": "切换组", + "removeBold": "移除粗体", + "bold": "粗体", + "removeItalic": "移除斜体", + "italic": "斜体", + "underline": "移除下划线", + "removeUnderline": "下划线", + "removeInlineCode": "移除内联代码样式", + "inlineCode": "内联代码样式", + "link": "创建链接", + "richText": "富文本", + "diffMode": "差异模式", + "source": "源码模式", + "admonition": "插入注释区块", + "codeBlock": "插入代码块", + "editFrontmatter": "编辑前置元数据", + "insertFrontmatter": "插入前置元数据", + "image": "插入图片", + "insertSandpack": "插入 Sandpack", + "table": "插入表格", + "thematicBreak": "插入主题换行", + "bulletedList": "无序列表", + "numberedList": "有序列表", + "checkList": "任务列表", + "deleteSandpack": "删除 Sandpack", + "undo": "撤销 {{shortcut}}", + "redo": "重做 {{shortcut}}" + }, + "admonitions": { + "note": "注意", + "tip": "提示", + "danger": "危险", + "info": "信息", + "caution": "警告", + "changeType": "选择注释区块类型", + "placeholder": "注释区块类型" + }, + "codeBlock": { + "language": "代码块语言", + "selectLanguage": "选择代码块语言" + }, + "contentArea":{ + "editableMarkdown": "可编辑的 Markdown" + } +} \ No newline at end of file diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 621d1d9..d21e545 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -1,102 +1,275 @@ { "login": { - "email": "電子信箱", + "lastStep": "最後一步", + "siginToYourAccount": "登入你的賬號", + "createNewAccount": "建立新賬號", + "enterPassword": "請輸入密碼", + "enterPasswordHint": "請輸入賬號 {{email}} 對應的密碼", + "paswordlessHint": "賬號 {{email}} 為無密碼賬戶,請選擇下列方式認證:", + "noAccountSignupNow": "還沒有賬號?<0>立即注冊", + "haveAccountSignInNow": "已由賬號?<0>立即登入", + "privacyPolicy": "隱私政策", + "termOfUse": "使用條款", + "signupHint": "你輸入的賬戶 {{email}} 不存在,是否立即注冊?", + "accountNotFoundHint": "你輸入的賬戶 {{email}} 不存在。", + "or": "或者", + "selectAccountToUse": "選擇要使用的賬號", + "useOtherAccount": "使用其他賬號", + "email": "電子郵箱", "password": "密碼", "captcha": "驗證碼", "captchaError": "驗證碼載入失敗: {{message}}", "signIn": "登入", - "signUp": "註冊", - "signUpAccount": "註冊帳號", - "useFIDO2": "使用外部驗證器登入", + "signUp": "注冊", + "signUpAccount": "注冊賬號", + "useFIDO2": "使用通行金鑰器登入", "usePassword": "使用密碼登入", - "forgetPassword": "忘記密碼", - "2FA": "兩步驟驗證", - "input2FACode": "請輸入 6 位兩步驟驗證碼", + "forgetPassword": "忘記密碼?", + "2FA": "二步驗證", + "input2FACode": "請輸入 6 位二步驗證程式碼", "passwordNotMatch": "兩次密碼輸入不一致", - "findMyPassword": "重設密碼", + "findMyPassword": "找回密碼", "passwordReset": "密碼已重設", "newPassword": "新密碼", - "repeatNewPassword": "再次輸入新密碼", + "repeatNewPassword": "重複新密碼", "repeatPassword": "重複密碼", "resetPassword": "重設密碼", "backToSingIn": "返回登入", - "sendMeAnEmail": "發送密碼重設郵件", - "resetEmailSent": "密碼重設郵件已傳送,請注意查收", - "browserNotSupport": "當前瀏覽器或或環境不支援", + "sendMeAnEmail": "傳送密碼重置郵件", + "resetEmailSent": "密碼重置郵件已傳送,請注意查收", + "browserNotSupport": "當前瀏覽器或環境不支援", "success": "登入成功", - "signUpSuccess": "註冊完成", - "activateSuccess": "啟動成功", - "accountActivated": "您的帳號已成功被啟動。", + "signUpSuccess": "注冊成功", + "activateSuccess": "啟用成功", + "accountActivated": "您的賬號已被成功啟用", "title": "登入 {{title}}", - "sinUpTitle": "註冊 {{title}}", - "activateTitle": "信箱驗證", - "activateDescription": "一封啟動郵件已發送至您的信箱,請點擊郵件中的連結以完成註冊。", + "sinUpTitle": "注冊 {{title}}", + "activateTitle": "郵件啟用", + "activateDescription": "一封啟用郵件已經發送至您的郵箱,請訪問郵件中的連結以繼續完成注冊。", "continue": "下一步", - "logout": "登出", - "loggedOut": "您已登出", - "clickToRefresh": "點擊重新整理驗證碼" + "back": "上一步", + "logout": "退出登入", + "loggedOut": "您已退出登入", + "clickToRefresh": "點選重新整理驗證碼" }, "navbar": { - "myFiles": "我的文件", + "notBefore": "不早於", + "notAfter": "不晚於", + "minimum": "最小", + "maximum": "最大", + "fileSize": "檔案大小", + "searchBase": "搜尋路徑", + "searchInBase": "搜尋 <0>", + "conditionDuplicate": "條件已存在", + "fileType": "檔案型別", + "addCondition": "新增條件", + "notNameOpOr": "需包含全部關鍵詞", + "caseFolding": "忽略大小寫", + "keywords": "關鍵字", + "fileNameKeywordsHelp": "輸入後按回車鍵新增關鍵字", + "advancedSearch": "高階搜尋", + "searchFilesTitle": "搜尋檔案", + "searchIn": "搜尋 <0>{{keywords}}", + "recentlyViewed": "最近瀏覽", + "searchFiles": "搜尋檔案...", + "showMore": "更多", + "myFiles": "我的檔案", + "hisFiles": "他的檔案", + "trash": "回收站", + "sharedWithMe": "與我共享", "myShare": "我的分享", "remoteDownload": "離線下載", - "connect": "連接", - "taskQueue": "任務列", - "setting": "個人設定", - "videos": "影片", + "connect": "連線與掛載", + "taskQueue": "後臺任務", + "setting": "設定", + "videos": "視訊", "photos": "圖片", "music": "音樂", "documents": "文件", "addATag": "新增標籤...", "addTagDialog": { - "selectFolder": "選擇資料夾", - "fileSelector": "文件分類", - "folderLink": "目錄捷徑", + "selectFolder": "選擇目錄", + "fileSelector": "檔案分類", + "folderLink": "目錄快捷方式", "tagName": "標籤名", - "matchPattern": "檔案名應對規則", - "matchPatternDescription": "你可以使用 <0>* 作為通用。比如 <1>*.png 表示對應 png 格式圖像。多行規則間會以「或」的關係運算。", + "matchPattern": "檔名匹配規則", + "matchPatternDescription": "你可以使用 <0>* 作為萬用字元。比如 <1>*.png 表示匹配 png 格式影象。多行規則間會以 “或” 的關系進行運算。", "icon": "圖示:", "color": "顏色:", - "folderPath": "資料夾路徑" + "folderPath": "目錄路徑" }, "storage": "儲存空間", "storageDetail": "已使用 {{used}}, 共 {{total}}", "notLoginIn": "未登入", "visitor": "遊客", - "objectsSelected": "{{num}} 個對象", - "searchPlaceholder": "搜尋...", - "searchInFiles": "在我的文件中搜尋 <0>{{name}}", - "searchInFolders": "在當前目錄中搜尋 <0>{{name}}", - "searchInShares": "在全站分享中搜尋 <0>{{name}}", - "backToHomepage": "返回首頁", - "toDarkMode": "切換到深色模式", - "toLightMode": "切換到淺色模式", - "myProfile": "個人首頁", - "dashboard": "管理面板", - "exceedQuota": "您的已用容量已超過容量配額,請盡快刪除多餘文件" + "objectsSelected": "{{num}} 個物件", + "searchPlaceholder": "按下 <0>/ 開始搜尋", + "backToHomepage": "返回主頁", + "darkModeSwitch": "設定黑暗模式", + "toDarkMode": "黑暗", + "toLightMode": "淺色", + "myProfile": "個人主頁", + "dashboard": "管理面板" }, "fileManager": { - "open": "打開", - "openParentFolder": "打開所在目錄", + "shareWithMeEmpty": "沒有找到別人的分享", + "shareWithMeEmptyDes": "如需要在此看到別人的分享,請在訪問別人分享連結時,在右上角將快捷方式保存到你的文件中的任意位置。", + "selectAll": "全選", + "selectNone": "取消選擇", + "invertSelection": "反選", + "imageSize": "圖片尺寸", + "focalLength": "焦距", + "columnExisted": "列已存在", + "metadataColumn": "元資料 ({{metadata}})", + "column": "列", + "listColumnSetting": "列設定", + "addColumn": "新增列", + "failedLoadPreview": "預覽載入失敗", + "recursiveLimitReached": "搜尋深度達到上限", + "recursiveLimitReachedDes": "系統已停止搜尋更深層的目錄,請嘗試縮小搜尋目錄的範圍。", + "searchConditions": "{{num}} 個條件", + "createDate": "建立日期", + "updatedDate": "修改日期", + "cameraMake": "相機制造商", + "cameraModel": "相機型號", + "lensModel": "鏡頭型號", + "lensMake": "鏡頭制造商", + "metadataKey": "鍵", + "metadataValue": "值", + "metadata": "元資料", + "symbolicFile": "快捷方式", + "relocation": "轉移儲存策略", + "downloadingFile": "正在下載 “{{name}}”, 請不要關閉本頁面...", + "mountOwner": "只有當前目錄的所有者可以掛載策略", + "uploading": "上傳中", + "noActionsCanBeDone": "沒有可以進行的操作", + "newFileName": "新檔案.{{ext}}", + "newDocumentType": "{{display_name}} (.{{ext}})", + "text": "文字", + "diagram": "圖表", + "whiteboard": "白板", + "selectApplications": "選擇應用...", + "newlyCreatedFolder": "新建資料夾", + "expandAllApp": "展開所有應用", + "epubViewer": "ePub 閱讀器", + "googledocs": "Google Docs 線上閱讀器", + "m365viewer": "Microsoft Office 線上閱讀器", + "pdfViewer": "PDF 閱讀器", + "viewerFileSizeWarning": "開啟的檔案大小 ({{file_size}}) 超過了 {{app}} 的限制 ({{max}}),可能無法正常工作。", + "testSubtitleStyle": "測試字幕樣式 AaBbCc", + "color": "顏色", + "fontSize": "字型大小", + "disableSubtitle": "禁用字幕", + "noSubtitle": "沒有在當前目錄下找到 ASS/SRT/VTT 字幕檔案。", + "subtitleStyles": "字幕樣式", + "subtitles": "字幕", + "markdownEditor": "Markdown 編輯器", + "saveSuccess": "在 {{time}} 儲存成功", + "drawioLng": "zh", + "charset": "編碼", + "textType": "文字型別", + "fileSaved": "檔案已儲存", + "failedToLoadFile": "檔案載入失敗: {{msg}}", + "monacoEditor": "Monaco 程式碼編輯器", + "preparingOpenFile": "正在準備開啟檔案...", + "openWithDescription": "選擇一個應用開啟 .{{ext}} 檔案。", + "openWith": "開啟方式", + "readOnly": "只讀", + "save": "儲存", + "noMoreImages": "當前頁面無可瀏覽影象", + "imageViewer": "圖片檢視器", + "logFileDeleteShare": "刪除分享連結", + "logFileEditShare": "編輯分享連結", + "deleteShareWarning": "確定要刪除此分享連結嗎?", + "edit": "編輯", + "editAndReactivate": "編輯並重新啟用", + "yes": "是", + "no": "否", + "permanentValid": "永久有效", + "manageShares": "管理分享連結", + "deleteVersionWarning": "確定要刪除此版本嗎?此操作無法撤銷。", + "setAsCurrent": "設為當前版本", + "current": "[當前版本]", + "createdBy": "建立者", + "manageVersions": "管理版本", + "livePhoto": "Live Photo", + "version": "版本", + "actions": "操作", + "versionEntity": "檔案資料和歷史版本", + "data": "資料", + "owned": "擁有此檔案", + "ownedSymbolic": "擁有此快捷方式", + "expires": "過期時間", + "originalLocation": "原始位置", + "descendant": "子物件", + "folderChildren": "{{files}} 個檔案,{{folders}} 個資料夾", + "moreThan": "大於 {{text}}", + "calculate": "計算", + "unset": "未設定", + "folder": "資料夾", + "file": "檔案", + "symbolicLink": "快捷方式 ({{srcType}})", + "type": "型別", + "storageUsed": "佔用空間", + "location": "位置", + "basicInfo": "基本資訊", + "format": "格式", + "duration": "時長", + "artist": "藝術家", + "album": "專輯", + "title": "標題", + "resolution": "解析度", + "takenAt": "拍攝時間", + "software": "軟體", + "copyright": "作者", + "exposureBias": "曝光補償", + "flash": "閃光燈", + "copyToClipboard": "復制到剪下板", + "searchSomething": "搜尋 \"{{text}}\"...", + "iso": "ISO", + "exposureValue": "{{num}} 秒", + "exposure": "曝光", + "aperture": "光圈", + "mediaInfo": "媒體資訊", + "details": "詳情", + "activity": "活動", + "goToSharedLink": "轉到分享連結", + "saveShortcut": "儲存分享為快捷方式", + "customizeIcon": "自定義圖示", + "tags": "標籤", + "apply": "應用", + "customizeColor": "自定義顏色", + "folderColor": "資料夾顏色", + "restore": "還原", + "unpin": "取消固定", + "youDontHaveReadPermissionToThisFile": "你沒有許可權讀取此內容", + "sharedWithOthers": "與他人分享", + "new": "新建", + "open": "開啟", + "openParentFolder": "轉到所在目錄", "download": "下載", "batchDownload": "打包下載", "share": "分享", "rename": "重新命名", + "organize": "整理", + "pin": "固定到側邊欄", + "pinAlias": "展示別名", + "optional": "可選", "move": "移動", "delete": "刪除", "moreActions": "更多操作", "refresh": "重新整理", - "compress": "壓縮", + "createArchive": "建立壓縮檔案", "newFolder": "建立資料夾", - "newFile": "建立文件", + "newFile": "建立檔案", "showFullPath": "顯示路徑", "listView": "列表", - "gridViewSmall": "小圖示", - "gridViewLarge": "大圖示", + "gridView": "網格", + "galleryView": "畫廊", "paginationSize": "分頁大小", "paginationOption": "{{option}} / 頁", "noPagination": "不分頁", - "sortMethod": "排序方式", + "sortMethod": "排序", "sortMethods": { "A-Z": "A-Z", "Z-A": "Z-A", @@ -114,113 +287,182 @@ "currentFolder": "當前目錄", "backToParentFolder": "上級目錄", "folders": "資料夾", - "files": "文件", - "listError": ":( 請求時出現錯誤", - "dropFileHere": "拖拽文件至此", - "orClickUploadButton": "或點擊右下方「上傳文件」按鈕新增文件", + "files": "檔案", + "listError": "請求時出現錯誤", + "dropFileHere": "拖拽檔案至此", + "orClickUploadButton": "或點選左上方“建立”按鈕新增檔案", "nothingFound": "什麼都沒有找到", - "uploadFiles": "上傳文件", + "uploadFiles": "上傳檔案", "uploadFolder": "上傳目錄", "newRemoteDownloads": "離線下載", "enter": "進入", - "getSourceLink": "獲取外鏈", - "getSourceLinkInBatch": "批次獲取外鏈", + "getSourceLink": "獲取直鏈", "createRemoteDownloadForTorrent": "建立離線下載任務", - "decompress": "解壓縮", + "extractArchive": "解壓縮", "createShareLink": "建立分享連結", - "viewDetails": "詳細訊息", - "copy": "複製", + "viewDetails": "詳細資訊", + "copy": "復制", "bytes": " ({{bytes}} 位元組)", "storagePolicy": "儲存策略", - "inheritedFromParent": "跟隨父目錄", "childFolders": "包含目錄", - "childFiles": "包含文件", + "childFiles": "包含檔案", "childCount": "{{num}} 個", "parentFolder": "所在目錄", "rootFolder": "根目錄", "modifiedAt": "修改於", - "createdAt": "建立於", - "statisticAt": "統計於 <1>", - "musicPlayer": "音訊播放", + "createdAt": "創建於", + "statisticAt": "統計於", + "musicPlayer": "音訊播放器", "closeAndStop": "退出播放", - "playInBackground": "後台播放", - "copyTo": "複製到", - "copyToDst": "複製到 <0>{{dst}}", - "errorReadFileContent": "無法讀取文件內容:{{msg}}", + "playInBackground": "後臺播放", + "copyTo": "復制到", + "copyToDst": "復制到 <0>", + "moveTo": "移動到", + "moveToDst": "移動到 <0>", + "errorReadFileContent": "無法讀取檔案內容:{{msg}}", "wordWrap": "自動換行", "pdfLoadingError": "PDF 載入失敗:{{msg}}", "subtitleSwitchTo": "字幕切換到:{{subtitle}}", - "noSubtitleAvailable": "影片目錄下沒有可用字幕文件 (支援:ASS/SRT/VTT)", + "noSubtitleAvailable": "視訊目錄下沒有可用字幕檔案 (支援:ASS/SRT/VTT)", "subtitle": "選擇字幕", "playlist": "播放列表", - "openInExternalPlayer": "用外部播放器打開", + "openInExternalPlayer": "用外部播放器開啟", "searchResult": "搜尋結果", "preparingBathDownload": "正在準備打包下載...", - "preparingDownload": "獲取下載網址...", + "preparingDownload": "正在準備下載...", + "browserDownload": "瀏覽器端下載到本地目錄", + "browserDownloadDescription": "由瀏覽器逐一下載檔案結構到你指定到本地目錄。", "browserBatchDownload": "瀏覽器端打包", - "browserBatchDownloadDescription": "由瀏覽器即時下載並打包,並非所有環境都支援。", + "browserBatchDownloadDescription": "由瀏覽器實時下載並打包為 Zip 檔案,無法下載大於 4GB 的資料。", "serverBatchDownload": "服務端中轉打包", - "serverBatchDownloadDescription": "由服務端中轉打包並即時發送到用戶端下載。", - "selectArchiveMethod": "選擇打包下載方式", - "batchDownloadStarted": "打包下載已開始,請不要關閉此分頁", + "serverBatchDownloadDescription": "由服務端中轉打包為 Zip 檔案並實時傳送到客戶端下載,不支援分享快捷方式。", + "selectArchiveMethod": "選擇批量下載方式", + "batchDownloadStarted": "打包下載已開始,請不要關閉此頁面...", "batchDownloadError": "打包遇到錯誤:{{msg}}", - "userDenied": "用戶拒絕", - "directoryDownloadReplace": "替換對象", - "directoryDownloadReplaceDescription": "將會替換 {{duplicates}} 等共 {{num}} 個對象。", - "directoryDownloadSkip": "跳過對象", - "directoryDownloadSkipDescription": "將會跳過 {{duplicates}} 等共 {{num}} 個對象。", - "selectDirectoryDuplicationMethod": "重複對象處理方式", - "directoryDownloadStarted": "下載已開始,請不要關閉此分頁", - "directoryDownloadFinished": "下載完成,無失敗對象", - "directoryDownloadFinishedWithError": "下載完成, 失敗 {{failed}} 個對象", - "directoryDownloadPermissionError": "無權限操作,請允許讀寫本地文件" + "userDenied": "使用者拒絕", + "directoryDownloadReplace": "替換此檔案", + "directoryDownloadReplaceDescription": "將會覆蓋本地的 “{{name}}”", + "directoryDownloadSkip": "跳過此檔案", + "directoryDownloadSkipDescription": "將會跳過下載 “{{name}}”", + "selectDirectoryDuplicationMethod": "檔案重名", + "directoryDownloadReplaceAll": "替換此檔案和後續所有重名檔案", + "directoryDownloadReplaceAllDescription": "將會覆蓋本地的 “{{name}}”,並記住選擇", + "directoryDownloadSkipAll": "跳過此檔案和後續所有重名檔案", + "directoryDownloadSkipAllDescription": "將會跳過下載 “{{name}}”,並記住選擇", + "directoryDownloadStarted": "下載已開始,請不要關閉此標籤頁", + "directoryDownloadFinished": "下載完成,無失敗物件", + "directoryDownloadFinishedWithError": "下載完成, 失敗 {{failed}} 個物件", + "directoryDownloadPermissionError": "無許可權操作,請允許讀寫本地檔案", + "back": "後退", + "view": "檢視", + "layout": "布局", + "thumbnails": "縮圖", + "on": "開啟", + "off": "關閉" }, "modals": { - "processing": "處理中...", - "duplicatedObjectName": "新名稱與已有文件重複", - "duplicatedFolderName": "資料夾名稱重複", + "showFileName": "顯示檔名", + "archiveFile": "壓縮檔案", + "cancelDownload": "取消下載", + "always": "始終", + "justOnce": "僅一次", + "quality": "質量", + "saveAsOtherFormat": "另存為其他格式", + "conflictDes1": "檔案版本發生衝突,可能的原因是:", + "conflictDes2": "<0>該檔案在你開啟後被從它處更新了新版本。<1>如果你另存為了新檔名或新位置,可能已有同名檔案存在。", + "saveAs": "另存為", + "versionConflict": "版本衝突", + "overwrite": "覆蓋", + "editShareLink": "編輯分享連結", + "clearPermissions": "清除許可權設定", + "shortcutCreated": "快捷方式已建立", + "createShortcut": "建立快捷方式", + "createShortcutTo": "在 <0> 建立快捷方式", + "targetExisted": "目標已存在", + "users": "使用者", + "groups": "使用者組", + "resetToDefault": "重置為預設", + "duplicateTag": "標籤 \"{{tag}}\" 已存在", + "colorForTag": "自定義新標籤顏色", + "enterForNewTag": "按回車鍵新增新標籤", + "manageTags": "管理標籤", + "onlyOwner": "只有檔案所有者可以強制解鎖此檔案", + "forceUnlock": "強制解鎖", + "forceUnlockAll": "強制解鎖全部", + "forceUnlockDes": "強制解鎖可能會導致檔案狀態異常,推薦優先等待檔案被主動釋放。確定要繼續解鎖嗎?", + "webdav": "WebDAV", + "soft-delete": "移至回收站", + "updateMetadata": "更新元資料", + "upload": "上傳", + "moveCopy": "移動或復制", + "view": "檢視", + "cannotPerformAction": "不支援移動或復制到此處", + "cannotMoveCopyToChild": "無法移動或復制到子目錄", + "copySuccess": "成功復制 {{num}} 個檔案", + "moveSuccess": "成功移動 {{num}} 個檔案", + "unknownParent": "未知父目錄", + "unknownParentDes": "被佔用的目錄是共享目錄的父目錄,它不屬於你所有", + "lockConflictTitle": "檔案被佔用", + "lockConflictDescription": "操作無法完成,因為下列檔案正在被使用,請稍後重試。 如果你是檔案所有者,並且確定檔案沒有被使用,你可以強制解鎖檔案並重試。", + "application": "應用", + "errorDetailsTitle": "錯誤詳情", + "processingMoving": "正在移動檔案...", + "processingCopying": "正在復制檔案...", + "processingRestoring": "正在恢復檔案...", + "fileRestored": "已恢復 {{num}} 個檔案至原位", + "duplicatedObjectName": "新名稱與已有檔案重複", + "newNameLengthError": "檔名長度必須在 1~255 個字元之間", + "newNameCharacterError": "檔名不能包含以下字元:\\ / : * ? \" < > |", + "newNameDotError": "檔名不能為 \".\" 或 \"..\"", "taskCreated": "任務已建立", "taskCreateFailed": "{{failed}} 個任務建立失敗:{{details}}", - "linkCopied": "連結已複製", - "getSourceLinkTitle": "獲取文件外鏈", - "sourceLink": "文件外鏈", + "linkCopied": "連結已復制", + "getSourceLinkTitle": "獲取檔案直鏈", + "sourceLink": "檔案直鏈", "folderName": "資料夾名稱", "create": "建立", - "fileName": "檔案名稱", + "fileName": "檔名", "renameDescription": "輸入 <0>{{name}} 的新名稱:", "newName": "新名稱", - "moveToTitle": "移動至", "moveToDescription": "移動至 <0>{{name}}", "saveToTitle": "儲存至", "saveToTitleDescription": "儲存至 <0>{{name}}", - "deleteTitle": "刪除對象", - "deleteOneDescription": "確定要刪除 <0>{{name}} 嗎?", - "deleteMultipleDescription": "確定要刪除這 {{num}} 個對象嗎?", - "newRemoteDownloadTitle": "新增離線下載任務", + "deleteTitle": "刪除物件", + "deleteOneDescription": "確定要將 <0>{{name}} 移至回收站嗎?", + "deleteMultipleDescription": "確定要將這 {{num}} 個物件移至回收站嗎?", + "deleteOneDescriptionHard": "確定要永久刪除 <0>{{name}} 嗎?", + "trashRetention": "回收站中的檔案會在 <0>{{num}} 後自動刪除。", + "deleteMultipleDescriptionHard": "確定要永久刪除這 {{num}} 個物件嗎?", + "newRemoteDownloadTitle": "新建離線下載任務", "remoteDownloadURL": "下載連結", - "remoteDownloadURLDescription": "輸入文件下載網址,一行一個,支援 HTTP(s) / FTP / 磁力鏈", + "remoteDownloadURLDescription": "輸入檔案下載地址,一行一個", "remoteDownloadDst": "下載至", + "processNode": "處理節點", + "remoteDownloadNodeAuto": "自動分配", "createTask": "建立任務", - "downloadTo": "下載至 <0>{{name}}", + "downloadToDst": "下載至 <0>{{name}}", + "downloadTo": "下載至", "decompressTo": "解壓縮至", "decompressToDst": "解壓縮至 <0>{{name}}", "defaultEncoding": "預設", "chineseMajorEncoding": "簡體中文常見編碼", - "selectEncoding": "選擇 ZIP 文件特殊字元編碼", + "selectEncoding": "ZIP 檔案編碼", "noEncodingSelected": "未選擇編碼方式", - "listingFiles": "列取文件中...", - "listingFileError": "列取文件時出錯:{{message}}", + "listingFiles": "列取檔案中...", + "listingFileError": "列取檔案時出錯:{{message}}", "generatingSourceLinks": "生成外鏈中...", - "noFileCanGenerateSourceLink": "沒有可以生成外鏈的文件", - "sourceBatchSizeExceeded": "當前用戶組最大可同時為 {{limit}} 個文件生成外鏈", - "zipFileName": "ZIP 檔案名", + "noFileCanGenerateSourceLink": "沒有可以生成外鏈的檔案", + "sourceBatchSizeExceeded": "當前使用者組最大可同時為 {{limit}} 個檔案生成外鏈", + "zipFileName": "壓縮檔名", "shareLinkShareContent": "我向你分享了:{{name}} 連結:{{link}}", "shareLinkPasswordInfo": " 密碼: {{password}}", "createShareLink": "建立分享連結", - "usePasswordProtection": "使用密碼保護", + "privateShare": "隱藏分享", + "privateShareDes": "勾選後,其他人無法在你的個人主頁看到此分享連結。", + "expireAfterDownload": "下載後自動過期", "sharePassword": "分享密碼", "randomlyGenerate": "隨機生成", - "expireAutomatically": "自動過期", + "expireAutomatically": "超時自動過期", "downloadLimitOptions": "{{num}} 次下載", "or": "或者", "5minutes": "5 分鐘", @@ -228,45 +470,54 @@ "1day": "1 天", "7days": "7 天", "30days": "30 天", - "custom": "自訂", - "seconds": "秒", + "custom": "自定義", + "minutes": "分鐘", "downloads": "次下載", - "downloadSuffix": "後過期", + "expirePrefix": "", + "expireSuffix": "後過期", "allowPreview": "允許預覽", - "allowPreviewDescription": "是否允許在分享頁面預覽文件內容", + "allowPreviewDescription": "是否允許在分享頁面預覽檔案內容", "shareLink": "分享連結", - "sendLink": "發送連結", + "sendLink": "傳送連結", "directoryDownloadReplaceNotifiction": "已覆蓋 {{name}}", "directoryDownloadSkipNotifiction": "已跳過 {{name}}", - "directoryDownloadTitle": "下載", - "directoryDownloadStarted": "開始下載 {{name}}", - "directoryDownloadFinished": "下載完成", + "directoryDownloadTitle": "批量下載日誌", + "directoryDownloadStarted": "開始下載 “{{name}}”", + "directoryDownloadFinished": "下載完成 “{{name}}”", "directoryDownloadError": "遇到錯誤:{{msg}}", "directoryDownloadErrorNotification": "下載 {{name}} 遇到錯誤:{{msg}}", "directoryDownloadAutoscroll": "自動滾動", "directoryDownloadCancelled": "已取消下載", - "advanceOptions": "高級選項", - "forceDelete": "強制刪除文件", - "forceDeleteDes": "強制刪除文件記錄,無論物理文件是否被成功刪除", - "unlinkOnly": "僅解除連結", - "unlinkOnlyDes": "僅刪除文件記錄,物理文件不會被刪除" + "advanceOptions": "高階選項", + "skipSoftDelete": "徹底刪除檔案", + "skipSoftDeleteDes": "跳過回收站,直接刪除檔案", + "unlinkOnly": "保留物理檔案", + "unlinkOnlyDes": "僅刪除檔案記錄,物理檔案不會被刪除" }, "uploader": { - "fileNotMatchError": "所選擇文件與原始文件不符", + "fileCopyName": "副本_", + "overwriteTooltip": "檔案重名時覆蓋已有檔案,只針對新新增的任務有效", + "rename": "使用新檔名重試", + "overwrite": "覆蓋已有檔案", + "pasteFilesHere": "將檔案貼上到此處", + "clipboardDefaultFileName": "剪貼簿 {{date}}.png", + "uploadFromClipboard": "從剪貼簿上傳", + "uploadList": "上傳列表", + "fileNotMatchError": "所選擇檔案與原始檔案不符", "unknownError": "出現未知錯誤:{{msg}}", "taskListEmpty": "沒有上傳任務", "hideTaskList": "隱藏列表", - "uploadTasks": "上傳隊列", + "uploadTasks": "上傳佇列", "moreActions": "更多操作", - "addNewFiles": "新增新文件", - "toggleTaskList": "展開/摺疊隊列", + "addNewFiles": "新增新檔案", + "toggleTaskList": "展開/折疊佇列", "pendingInQueue": "排隊中...", "preparing": "準備中...", "processing": "處理中...", "progressDescription": "已上傳 {{uploaded}} , 共 {{total}} - {{percentage}}%", "progressDescriptionFull": "{{speed}} 已上傳 {{uploaded}} , 共 {{total}} - {{percentage}}%", "progressDescriptionPlaceHolder": "已上傳 - ", - "uploadedTo": "已上傳至 ", + "uploaded": "已上傳", "rootFolder": "根目錄", "unknownStatus": "未知", "resumed": "斷點續傳", @@ -274,15 +525,16 @@ "retry": "重試", "deleteTask": "刪除任務記錄", "cancelAndDelete": "取消並刪除", - "selectAndResume": "選取同樣文件並恢復上傳", - "fileName": "檔案名:", + "selectAndResume": "選取同樣檔案並恢復上傳", + "fileName": "檔名:", "fileSize": "檔案大小:", - "sessionExpiredIn": "<0> 過期", + "sessionExpiredIn": "<0>過期", "chunkDescription": "({{total}} 個分片, 每個分片 {{size}})", "noChunks": "(無分片)", - "destination": "儲存路徑:", + "destination": "存放位置:", + "storagePolicy": "儲存策略:", "uploadSession": "上傳會話:", - "errorDetails": "錯誤訊息:", + "errorDetails": "錯誤資訊:", "uploadSessionCleaned": "上傳會話已清除", "hideCompletedTooltip": "列表中不顯示已完成、失敗、被取消的任務", "hideCompleted": "隱藏已完成任務", @@ -299,22 +551,22 @@ "cleanCompletedTooltip": "清除列表中已完成、失敗、被取消的任務", "cleanCompleted": "清除已完成任務", "retryFailedTasks": "重試所有失敗任務", - "retryFailedTasksTooltip": "重試隊列中所有已失敗的任務", + "retryFailedTasksTooltip": "重試佇列中所有已失敗的任務", "setConcurrentTooltip": "設定同時進行的任務數量", "setConcurrent": "設定並行數量", "sizeExceedLimitError": "檔案大小超出儲存策略限制(最大:{{max}})", - "suffixNotAllowedError": "儲存策略不支援上傳此副檔名的文件(目前支援:{{supported}})", + "suffixNotAllowedError": "儲存策略不支援上傳此副檔名的檔案(當前支援:{{supported}})", "createUploadSessionError": "無法建立上傳會話", "deleteUploadSessionError": "無法刪除上傳會話", "requestError": "請求失敗: {{msg}} ({{url}})", "chunkUploadError": "分片 [{{index}}] 上傳失敗", - "conflictError": "同名文件的上傳任務已經在處理中", + "conflictError": "同名檔案的上傳任務已經在處理中", "chunkUploadErrorWithMsg": "分片上傳失敗: {{msg}}", "chunkUploadErrorWithRetryAfter": "(請在 {{retryAfter}} 秒後重試)", - "emptyFileError": "暫不支援上傳空文件至 OneDrive,請透過建立文件按鈕建立空文件", - "finishUploadError": "無法完成文件上傳", - "finishUploadErrorWithMsg": "無法完成文件上傳: {{msg}}", - "ossFinishUploadError": "無法完成文件上傳: {{msg}} ({{code}})", + "emptyFileError": "暫不支援上傳空檔案至 OneDrive,請通過建立檔案按鈕建立空檔案", + "finishUploadError": "無法完成檔案上傳", + "finishUploadErrorWithMsg": "無法完成檔案上傳: {{msg}}", + "ossFinishUploadError": "無法完成檔案上傳: {{msg}} ({{code}})", "cosUploadFailed": "上傳失敗: {{msg}} ({{code}})", "upyunUploadFailed": "上傳失敗: {{msg}}", "parseResponseError": "無法解析響應: {{msg}} ({{content}})", @@ -322,91 +574,156 @@ "dropFileHere": "鬆開滑鼠開始上傳" }, "share": { - "expireInXDays": "{{num}} 天後到期", - "days":"{{count}} day", - "days_other":"{{count}} days", - "expireInXHours": "{{num}} 小時後到期", - "hours":"an hour", - "hours_other":"{{count}} hours", - "createdBy": "此分享由 <0>{{nick}} 建立", - "sharedBy": "<0>{{nick}} 向您分享了 {{num}} 個文件", - "files":"1 file", - "files_other":"{{count}} files", - "statistics": "{{views}} 次瀏覽 • {{downloads}} 次下載 • {{time}}", - "views":"{{count}} view", - "views_other":"{{count}} views", - "downloads":"{{count}} download", - "downloads_other":"{{count}} downloads", + "statistics": "統計", + "expireAt": "<0>過期", + "expireAfterDownloads": "{{downloads}} 次下載後過期", + "somebodyShare": "{{name}} 的分享", + "expiredLink": "已失效的分享", + "sharedBy": "<0>{{nick}} 向您分享了 {{num}} 個檔案", + "files": "1 file", + "files_other": "{{count}} files", + "statisticsViews": "{{views}} 次瀏覽", + "statisticsDownloads": "{{downloads}} 次下載 ", + "views": "{{count}} view", + "views_other": "{{count}} views", + "downloads": "{{count}} download", + "downloads_other": "{{count}} downloads", "privateShareTitle": "{{nick}} 的加密分享", - "enterPassword": "輸入分享密碼", + "enterPassword": "分享密碼", "continue": "繼續", - "shareCanceled": "分享已取消", + "shareCanceled": "分享連結已刪除", "listLoadingError": "載入失敗", "sharedFiles": "我的分享", - "createdAtDesc": "建立日期由晚到早", - "createdAtAsc": "建立日期由早到晚", - "downloadsDesc": "下載次數由大到小", - "downloadsAsc": "下載次數由小到大", - "viewsDesc": "瀏覽次數由大到小", - "viewsAsc": "瀏覽次數由小到大", + "createdAtDesc": "最新", + "createdAtAsc": "最早", "noRecords": "沒有分享記錄.", - "sourceNotFound": "[原始對象不存在]", + "sourceNotFound": "[原始物件不存在]", "expired": "已失效", "changeToPublic": "變更為公開分享", "changeToPrivate": "變更為私密分享", - "viewPassword": "查看密碼", + "viewPassword": "檢視密碼", "disablePreview": "禁止預覽", "enablePreview": "允許預覽", "cancelShare": "取消分享", "sharePassword": "分享密碼", "readmeError": "無法讀取 README 內容:{{msg}}", - "enterKeywords": "請輸入搜尋關鍵字", + "enterKeywords": "請輸入搜尋關鍵詞", "searchResult": "搜尋結果", "sharedAt": "分享於 <0>", "pleaseLogin": "請先登入", - "cannotShare": "此文件無法預覽", + "cannotShare": "此檔案無法預覽", "preview": "預覽", "incorrectPassword": "密碼不正確", - "shareNotExist": "分享不存在或已過期" + "shareNotExist": "分享不存在或已過期", + "copyLinkToClipboard": "復制連結到剪下板" }, "download": { + "cancelTaskConfirm": "確定要取消此任務嗎?", + "saveChanges": "儲存更改", "failedToLoad": "載入失敗", "active": "進行中", "finished": "已完成", "activeEmpty": "沒有下載中的任務", "finishedEmpty": "沒有已完成的任務", "loadMore": "載入更多", - "taskFileDeleted": "文件已刪除", + "taskFileDeleted": "檔案已刪除", "unknownTaskName": "[未知]", "taskCanceled": "任務已取消,狀態會在稍後更新", "operationSubmitted": "操作成功,狀態會在稍後更新", - "deleteThisFile": "刪除此文件", - "openDstFolder": "打開存放目錄", - "selectDownloadingFile": "選擇要下載的文件", + "deleteThisFile": "刪除此檔案", + "openDstFolder": "開啟存放目錄", + "selectDownloadingFile": "選擇要下載的檔案", "cancelTask": "取消任務", "updatedAt": "更新於:", - "uploaded": "上傳大小:", - "uploadSpeed": "上傳速度:", - "InfoHash": "InfoHash:", + "uploaded": "上傳大小", + "uploadSpeed": "上傳速度", + "InfoHash": "InfoHash", "seederCount": "做種者:", "seeding": "做種中:", "downloadNode": "節點:", "isSeeding": "是", "notSeeding": "否", "chunkSize": "分片大小:", - "chunkNumbers": "分片數量:", + "chunkNumbers": "分片數量", "taskDeleted": "刪除成功", - "transferFailed": "文件轉存失敗", + "transferFailed": "檔案轉存失敗", "downloadFailed": "下載出錯:{{msg}}", "canceledStatus": "已取消", "finishedStatus": "已完成", "pending": "已完成,轉存排隊中", - "transferring": "已完成,轉存中", + "transferring": "轉存中", "deleteRecord": "刪除記錄", "createdAt": "建立日期:" }, "setting": { - "avatarUpdated": "頭像已更新,重新整理後生效", + "noAuthenticator": "新增通行金鑰以使用人臉、指紋或 USB 金鑰登入賬號", + "neverUsed": "從未使用過", + "usedAt": "上次使用於 <0>", + "passkeyName": "{os} 上的 {browser}", + "versionRetentionMax": "最大版本數量,0 表示無限制", + "versionRetentionEnabledExt": "啟用的副檔名", + "versionRetentionEnabledExtDes": "按回車鍵新增,留空時會對所有檔案啟用", + "enableVersionRetention": "啟用版本保留", + "enableVersionRetentionDes": "啟用後,對於符合條件的檔案,系統會保留其的歷史版本", + "versionRetention": "版本保留", + "languageDes": "設定應用展示語言和首選郵件語言", + "timezoneDes": "設定展示時區,預設跟隨系統時區", + "nickNameDes": "用於公開展示的名字,可使用真實姓名或暱稱", + "cropAvatar": "裁剪頭像", + "preference": "偏好", + "accountCreatedAt": "創建於 <0>", + "shoeQr": "顯示", + "deviceNothing": "當前使用者組不支援 WebDAV", + "connectionInfo": "連線資訊", + "proxyTooltip": "服務端代理所有檔案下載請求", + "readonlyTooltip": "使用者只能通過此賬號讀取檔案", + "rootFolderIn": "選擇 <0>", + "createWebDavAccount": "建立 WebDAV 賬號", + "editWebDavAccount": "編輯 {{name}}", + "seeding": "做種中", + "awaitSeeding": "等待做種", + "awaitSeedingDes": "等待下載任務做種完成。", + "downloadTransferDes": "將檔案轉存到目的地。", + "downloadDes": "下載指定的檔案。", + "retryErrorHistory": "歷史重試錯誤", + "retryCount": "重試次數", + "resumeAt": "下次恢復執行", + "executeDuration": "執行淨耗時", + "input": "輸入", + "output": "輸出", + "suspended": " (已掛起)", + "updatedAt": "更新於", + "taskDetails": "任務詳情", + "partialSuccessWarning": "有 {{num}} 個物件處理失敗,已將其跳過。", + "sendTask": "傳送任務", + "sendTaskDes": "將任務傳送到處理節點。", + "downloaded": "已下載", + "extractedFiles": "已解壓檔案數量", + "extractedFilesSize": "已解壓檔案大小", + "extractingFiles": "解壓檔案", + "extractingFilesDes": "將所有檔案解壓到指定目錄。", + "downloadingZip": "獲取壓縮檔案", + "downloadingZipDes": "將壓縮檔案下載到臨時工作區。", + "progressNotAvailable": "進度資訊尚未可用", + "uploadedSize": "轉存檔案", + "archivedFiles": "已處理檔案數量", + "transferredFiles": "已轉存檔案數量", + "archivedFilesSize": "已處理檔案大小", + "createArchiveFinishing": "提交新增檔案更改。", + "indexForArchiveDes": "檢索所有待壓縮檔案。", + "prepare": "準備", + "preparingWorkspaceDes": "準備臨時工作區。", + "compressFiles": "建立壓縮檔案", + "compressFilesDes": "將檔案壓縮到臨時工作區。", + "uploadArchiveFileDes": "將壓縮檔案轉存到目的地。", + "uploadWorker": "上傳執行緒 #{{num}}", + "queueToStart": "排隊開始", + "indexingFiles": "檢索檔案", + "indexingFilesDes": "檢索所有待轉移檔案,並將其鎖定。", + "transferring": "轉存", + "committingChanges": "提交更改", + "autoRefresh": "自動重新整理", + "avatarUpdated": "頭像已更新,最新頭像展示可能有延遲", "nickChanged": "暱稱已更改,重新整理後生效", "settingSaved": "設定已儲存", "themeColorChanged": "主題配色已更換", @@ -414,10 +731,10 @@ "avatar": "頭像", "uid": "UID", "nickname": "暱稱", - "group": "用戶組", - "regTime": "註冊時間", - "privacyAndSecurity": "安全隱私", - "profilePage": "個人首頁", + "group": "使用者組", + "regTime": "注冊時間", + "security": "密碼和安全", + "profilePage": "個人主頁", "accountPassword": "登入密碼", "2fa": "二步驗證", "enabled": "已開啟", @@ -425,78 +742,84 @@ "appearance": "個性化", "themeColor": "主題配色", "darkMode": "黑暗模式", - "syncWithSystem": "跟隨系統", - "fileList": "文件列表", + "syncWithSystem": "系統", + "fileList": "檔案列表", "timeZone": "時區", - "webdavServer": "連接地址", + "webdavServer": "連線地址", "userName": "使用者名稱", - "manageAccount": "帳號管理", - "uploadImage": "從文件上傳", + "manageAccount": "賬號管理", + "uploadImage": "從檔案上傳", "useGravatar": "使用 Gravatar 頭像 ", "changeNick": "修改暱稱", "originalPassword": "原密碼", "enable2FA": "啟用二步驗證", "disable2FA": "關閉二步驗證", - "2faDescription": "請使用任意二步驗證APP或者支援二步驗證的密碼管理軟體掃描左側二維碼新增本站。掃描完成後請填寫二步驗證APP給出的6位驗證碼以開啟二步驗證。", - "inputCurrent2FACode": "請驗證當前二步驗證代碼。", + "2faDescription": "請使用任意二步驗證 APP 或者支援二步驗證的密碼管理軟體掃描二維碼新增本站。掃描完成後請填寫二步驗證 APP 給出的 6 位驗證碼以開啟二步驗證。", + "inputCurrent2FACode": "輸入當前二步驗證 APP 給出的 6 位驗證碼:", "timeZoneCode": "IANA 時區名稱標識", "authenticatorRemoved": "憑證已刪除", "authenticatorAdded": "驗證器已新增", "browserNotSupported": "當前瀏覽器或環境不支援", "removedAuthenticator": "刪除憑證", "removedAuthenticatorConfirm": "確定要吊銷這個憑證嗎?", - "addNewAuthenticator": "新增新驗證器", - "hardwareAuthenticator": "外部認證器", - "copied": "已複製到剪切板", - "pleaseManuallyCopy": "當前瀏覽器不支援,請手動複製", - "webdavAccounts": "WebDAV 帳號管理", - "webdavHint": "WebDAV的地址為:{{url}};登入使用者名稱統一為:{{name}} ;密碼為所建立帳號的密碼。", - "annotation": "備註名", + "addNewAuthenticator": "新增新憑證", + "hardwareAuthenticator": "通行金鑰", + "copied": "已復制到剪下板", + "pleaseManuallyCopy": "當前瀏覽器不支援,請手動復制", + "webdavAccounts": "WebDAV 賬號管理", + "webdavHint": "WebDAV的地址為:{{url}};登入使用者名稱統一為:{{name}} ;密碼為所建立賬號的密碼。", + "annotation": "備注名", "rootFolder": "相對根目錄", "createdAt": "建立日期", "action": "操作", - "readonlyOn": "開啓只讀", - "readonlyOff": "關閉只讀", - "useProxyOn": "開啓反代", - "useProxyOff": "關閉反代", + "readonlyOn": "只讀", + "readonlyOff": "讀寫", + "proxy": "反向代理", + "none": "無", + "proxied": "已代理", "delete": "刪除", "listEmpty": "沒有記錄", - "createNewAccount": "建立新帳號", - "taskType": "任務類型", + "createNewAccount": "建立新賬號", + "taskType": "任務型別", "taskStatus": "狀態", - "lastProgress": "最後進度", - "errorDetails": "錯誤訊息", + "taskProgress": "任務進度", + "errorDetails": "錯誤資訊", "queueing": "排隊中", "processing": "處理中", "failed": "失敗", "canceled": "取消", "finished": "已完成", - "fileTransfer": "文件中轉", - "fileRecycle": "文件回收", + "fileTransfer": "檔案中轉", + "fileRecycle": "檔案回收", "importFiles": "匯入外部目錄", - "transferProgress": "已完成 {{num}} 個文件", + "transferProgress": "已完成 {{num}} 個檔案", "waiting": "等待中", "compressing": "壓縮中", "decompressing": "解壓縮中", "downloading": "下載中", - "transferring": "轉存中", "indexing": "索引中", "listing": "插入中", "allShares": "全部分享", "trendingShares": "熱門分享", "totalShares": "分享總數", - "fileName": "檔案名", + "fileName": "檔名", "shareDate": "分享日期", "downloadNumber": "下載次數", "viewNumber": "瀏覽次數", "language": "語言", - "iOSApp": "iOS 用戶端", - "connectByiOS": "透過 iOS 設備連接到 <0>{{title}}", - "downloadOurApp": "下載並安裝我們的 iOS 應用:", - "fillInEndpoint": "使用我們的 iOS 應用掃描下方二維碼(其他掃碼應用無效):", - "loginApp": "完成綁定,你可以開始使用 iOS 客戶端了。如果掃碼綁定遇到問題,你也可以嘗試手動輸入用戶名和密碼登入。", - "aboutCloudreve": "關於 Cloudreve", - "githubRepo": "GitHub 倉庫", - "homepage": "主頁" + "iOSApp": "iOS/iPadOS 客戶端", + "connectByiOS": "通過 iOS/iPadOS 裝置連線到 <0>{{title}}", + "downloadOurApp": "下載並安裝我們的應用:", + "fillInEndpoint": "使用我們的應用掃描下方二維碼(其他掃碼應用無效):", + "loginApp": "完成繫結,你可以開始使用客戶端了。如果掃碼繫結遇到問題,你也可以嘗試手動輸入使用者名稱和密碼登入。", + "relocateFileTo": "將 <0>{{more}} 的儲存策略轉移至 {{policy}}", + "extractFileTo": "將 <0>{{more}} 解壓縮至 <1>", + "createArchiveTo": "將 <0>{{more}} 打包至 <1>" + }, + "vas": { + "points": "積分", + "quota": "容量配額", + "used": "已使用 - {{size}}", + "total": "總容量 - {{size}}" } -} +} \ No newline at end of file diff --git a/public/locales/zh-TW/common.json b/public/locales/zh-TW/common.json index dfbae92..aad1ec3 100644 --- a/public/locales/zh-TW/common.json +++ b/public/locales/zh-TW/common.json @@ -3,77 +3,96 @@ "unknownError": "未知錯誤", "errLoadingSiteConfig": "無法載入站點配置:", "newVersionRefresh": "當前頁面有新版本可用,準備重新整理。", - "errorDetails": "錯誤詳情", + "errorDetails": "詳情", "renderError": "頁面渲染出現錯誤,請嘗試重新整理此頁面。", "ok": "確定", "cancel": "取消", "select": "選擇", - "copyToClipboard": "複製", + "copyToClipboard": "復制", "close": "關閉", + "dismiss": "關閉", "intlDateTime": "{{val, datetime}}", - "timeAgoLocaleCode": "zh_TW", - "forEditorLocaleCode": "zh-TW", - "artPlayerLocaleCode": "zh-tw", + "seconds": "s 秒", + "minutes": "m 分 s 秒", + "hours": "H 小時 m 分", + "days": "{{d}} 天", + "timeAgoLocaleCode": "zh_CN", + "forEditorLocaleCode": "zh-CN", + "artPlayerLocaleCode": "zh-cn", + "requestID": "請求 ID: {{id}}", + "object": "物件", + "error": "錯誤", + "areYouSure": "確認", + "incorrectSizeInput": "不符合尺寸限制", + "of": "共", + "rowsPerPage": "每頁行數", + "custom": "自定義", + "enter": "輸入", "errors": { "401": "請先登入", - "403": "此操作被禁止", + "403": "你沒有許可權執行此操作", "404": "資源不存在", "409": "發生衝突 ({{message}})", - "40001": "輸入參數有誤 ({{message}})", + "40001": "輸入引數有誤 ({{message}})", "40002": "上傳失敗", "40003": "目錄建立失敗", - "40004": "同名對象已存在", - "40004": "同名對象已存在", - "40005": "簽名過期", - "40006": "不支援的儲存策略類型", - "40007": "當前用戶組無法進行此操作", + "40004": "同名物件已存在", + "40005": "籤名過期", + "40006": "不支援的儲存策略型別", + "40007": "當前使用者組無法進行此操作", "40011": "上傳會話不存在或已過期", "40012": "分片序號無效 ({{message}})", "40013": "正文長度無效 ({{message}})", - "40014": "超出批次獲取外鏈限制", + "40014": "超出批量獲取外鏈限制", "40015": "超出最大離線下載任務數量限制", "40016": "路徑不存在", - "40017": "該帳號已被封禁", - "40018": "該帳號未啟用", + "40017": "該賬號已被封禁", + "40018": "該賬號未啟用", "40019": "此功能未啟用", - "40020": "用戶信箱或密碼錯誤", - "40021": "用戶不存在", - "40022": "驗證代碼不正確", + "40020": "憑證無效或過期", + "40021": "使用者不存在", + "40022": "驗證程式碼不正確", "40023": "登入會話不存在", "40024": "無法初始化 WebAuthn", "40025": "驗證失敗", "40026": "驗證碼錯誤", "40027": "驗證失敗,請重新整理網頁重試", - "40028": "郵件發送失敗", + "40028": "郵件傳送失敗", "40029": "無效的連結", "40030": "此連結已過期", - "40032": "此信箱已被使用", - "40033": "用戶未啟用,已重新傳送啟用郵件", - "40034": "該用戶無法被啟用", + "40032": "此郵箱已被使用", + "40033": "使用者未啟用,已重新發送啟用郵件", + "40034": "該使用者無法被啟用", "40035": "儲存策略不存在", - "40039": "用戶組不存在", - "40044": "文件不存在", - "40045": "無法列取目錄下的對象", - "40047": "無法初始化文件系統", + "40039": "使用者組不存在", + "40044": "檔案不存在", + "40045": "無法列取目錄下的物件", + "40047": "無法初始化檔案系統", "40048": "建立任務出錯", "40049": "檔案大小超出限制", - "40050": "文件類型不允許", + "40050": "檔案型別不允許", "40051": "容量空間不足", - "40052": "對象名非法,請移除特殊字元", + "40052": "物件名非法,請移除特殊字元", "40053": "不支援對根目錄執行此操作", - "40054": "話當前目錄下已經有同名文件正在上傳中,請嘗試清空上傳會話", - "40055": "文件訊息不一致", - "40056": "不支援該格式的壓縮文件", - "40057": "可用儲存策略發生變化,請重新整理文件列表並重新新增此任務", + "40054": "話當前目錄下已經有同名檔案正在上傳中,請嘗試清空上傳會話", + "40055": "檔案資訊不一致", + "40056": "不支援該格式的壓縮檔案", + "40057": "可用儲存策略發生變化,請重新整理檔案列表並重新新增此任務", "40058": "分享不存在或已過期", "40069": "密碼不正確", "40070": "此分享無法預覽", - "40071": "簽名無效", + "40071": "籤名無效", + "40073": "檔案被佔用", + "40074": "所選檔案數量超出限制", + "40079": "超出最大遍歷檔案數限制,請縮小操作範圍", + "40081": "操作未完全成功", + "40082": "只有檔案所有者可以執行此操作", + "40080": "使用者郵箱或密碼錯誤", "50001": "資料庫操作失敗 ({{message}})", - "50002": "URL 或請求簽名失敗 ({{message}})", + "50002": "URL 或請求籤名失敗 ({{message}})", "50004": "I/O 操作失敗 ({{message}})", "50005": "內部錯誤 ({{message}})", "50010": "目標節點不可用", - "50011": "文件元訊息查詢失敗" + "50011": "檔案元資訊查詢失敗" } -} +} \ No newline at end of file diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index c5bcc9a..cf355ec 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -1,267 +1,412 @@ { - "errors":{ + "errors": { "40036": "預設儲存策略無法刪除", - "40037": "有 {{message}} 個文件仍在使用此儲存策略,請先刪除這些文件", - "40038": "有 {{message}} 個用戶組綁定了此儲存策略,請先解除綁定", - "40040": "無法對系統用戶組執行此操作", - "40041": "有 {{message}} 位用戶仍屬於此用戶組,請先刪除這些用戶或者更改用戶組", - "40042": "無法更改初始用戶的用戶組", - "40043": "無法對初始用戶執行此操作", + "40037": "有檔案 Blob 仍在使用此儲存策略,請先刪除這些檔案 Blob", + "40038": "有 {{message}} 個使用者組綁定了此儲存策略,請先解除繫結", + "40040": "無法對系統使用者組執行此操作", + "40041": "有 {{message}} 位使用者仍屬於此使用者組,請先刪除這些使用者或者更改使用者組", + "40042": "無法更改初始使用者的使用者組", + "40043": "無法對初始使用者執行此操作", "40046": "無法對主機節點執行此操作", - "40060": "從機無法向主機發送回調請求,請檢查主機端 參數設定 - 站點訊息 - 站點URL設定,並確保從機可以連接到此地址 ({{message}})", + "40060": "從機無法向主機發送回調請求,請檢查主機端 引數設定 - 站點資訊 - 站點URL設定,並確保從機可以連線到此地址 ({{message}})", "40061": "Cloudreve 版本不一致 ({{message}})", - "50008": "設置項更新失敗 ({{message}})", + "40086": "節點正在被以下儲存策略使用:{{message}}", + "50008": "設定項更新失敗 ({{message}})", "50009": "跨域策略新增失敗" }, "nav": { "summary": "面板首頁", - "settings": "參數設定", - "basicSetting": "站點訊息", - "publicAccess": "註冊與登入", + "settings": "引數設定", + "basicSetting": "站點資訊", "email": "郵件", - "transportation": "傳輸與通信", + "transportation": "傳輸與通訊", "appearance": "外觀", - "image": "圖像與預覽", + "image": "影象與預覽", "captcha": "驗證碼", "storagePolicy": "儲存策略", - "nodes": "離線下載節點", - "groups": "用戶組", - "users": "用戶", - "files": "文件", + "nodes": "節點", + "groups": "使用者組", + "users": "使用者", + "files": "檔案", + "entities": "檔案 Blob", "shares": "分享", - "tasks": "持久任務", + "tasks": "後臺任務", "remoteDownload": "離線下載", - "generalTasks": "一般任務", - "title": "控制台", - "dashboard": "Cloudreve 控制台" + "generalTasks": "常規任務", + "title": "儀表盤", + "dashboard": "Cloudreve 儀表盤", + "userSession": "使用者會話", + "fileSystem": "檔案系統", + "mediaProcessing": "媒體處理", + "queue": "佇列", + "events": "事件", + "server": "伺服器" }, "summary": { - "newsletterError": "Cloudreve 公告載入失敗", + "generatedAt": "生成於 <0>", "confirmSiteURLTitle": "確定站點URL設定", - "siteURLNotSet": "您尚未設定站點URL,是否要將其設定為當前的 {{current}} ?", - "siteURLNotMatch": "您設定的站點URL與當前實際不一致,是否要將其設定為當前的 {{current}} ?", - "siteURLDescription": "此設定非常重要,請確保其與您站點的實際地址一致。你可以在 參數設定 - 站點訊息 中更改此設定。", + "siteURLNotMatch": "你設定的站點 URL 並未包含當前的 {{current}},是否要更改設定?", + "setAsPrimary": "設定為主要站點 URL", + "setAsPrimaryDes": "將 {{current}} 設定為主要站點 URL,用於與外部服務通訊和接受回撥,請使用能被公網訪問的 URL。", + "setAsSecondary": "新增到備選站點 URL", + "setAsSecondaryDes": "將 {{current}} 新增到備選站點 URL,Cloudreve 會根據使用者實際訪問的 URL 自動選擇是否使用。", + "siteURLDescription": "此設定非常重要,請確保其與你站點的實際地址一致。你可以在 引數設定 - 站點資訊 中更改此設定。", "ignore": "忽略", "changeIt": "更改", "trend": "趨勢", "summary": "總計", - "totalUsers": "註冊用戶", - "totalFiles": "文件總數", - "publicShares": "公開分享總數", - "privateShares": "私密分享總數", - "homepage": "首頁", - "documents": "文件", + "totalUsers": "注冊使用者", + "totalFiles": "檔案", + "shareLinks": "分享連結", + "totalBlobs": "檔案 Blob", + "homepage": "主頁", + "documents": "檔案", "forum": "討論社群", "forumLink": "https://forum.cloudreve.org", - "telegramGroup": "Telegram 群組", - "telegramGroupLink": "https://t.me/cloudreve_official", - "buyPro": "購買捐助版", + "discordCommunity": "Discord 社群", + "buyPro": "升級到 Pro", "publishedAt": "發表於 <0>", - "newsTag": "notice" + "newsTag": "notice", + "licenseExpireAt": "授權有效期", + "permanentLicense": "永久", + "offlineLicenseExpireAy": "離線授權過期日期", + "offlineLicenseDes": "在連線網路的情況下,Cloudreve 會在過期前自動更新離線授權", + "licensedDomains": "授權的域名", + "renew": "更新離線授權", + "manageLicense": "管理授權", + "volPurchase": "客戶端 VOL 授權需要單獨在 <0>授權管理面板 購買。VOL 授權允許你的使用者免費使用 <1>Cloudreve iOS 客戶端 連線到你的站點,無需使用者再付費訂閱 iOS 客戶端。購買授權後請點選下方更新授權。", + "iosVol": "iOS 客戶端批量授權 (VOL)", + "refreshSuccessfully": "重新整理成功", + "manualRefresh": "手動重新整理離線授權", + "manualRefreshDes": "自動重新整理離線授權失敗,請嘗試登入 <0>授權管理面板 獲取最新的離線授權,將其貼上在下方。" + }, + "queue": { + "queueName_io_intense": "IO 密集型", + "queueName_io_intenseDes": "用於處理大量 IO 操作的佇列,包括: 轉移儲存策略、解壓縮、壓縮。", + "queueName_media_meta": "媒體元資料提取", + "queueName_media_metaDes": "用於提取媒體檔案的元資料。", + "queueName_recycle": "Blob 回收", + "queueName_recycleDes": "用於刪除過期的檔案 Blob。", + "queueName_thumb": "縮圖生成", + "queueName_thumbDes": "用於為檔案生成縮圖。", + "queueName_remote_download": "離線下載", + "queueName_remote_downloadDes": "用於處理離線下載任務。", + "failed": "失敗 ({{count}})", + "success": "成功 ({{count}})", + "suspending": "掛起 ({{count}})", + "busyWorker": "處理中 ({{count}})", + "submited": "已提交 ({{count}})", + "editQueueSettings": "編輯佇列設定 - {{name}}", + "workerNum": "工作執行緒數", + "workerNumDes": "任務佇列最多並行執行的任務數。", + "maxExecution": "最大執行時間", + "maxExecutionDes": "任務最大執行時間(秒),超過此時間任務將被終止。", + "backoffFactor": "退避因子", + "backoffFactorDes": "任務重試時間間隔的增長因子。", + "backoffMaxDuration": "最大退避時間", + "backoffMaxDurationDes": "任務重試的最大退避時間(秒)。", + "maxRetry": "最大重試次數", + "maxRetryDes": "任務失敗後的最大重試次數。", + "retryDelay": "重試延遲", + "retryDelayDes": "任務重試的初始延遲時間(秒)。" }, "settings": { + "resetUrl": "重置連結", + "exceedToleranceDays": "設置的封禁寬容天數", + "activateUrl": "激活連結", + "perPage": "每頁 {{num}} 條", + "noNodes": "沒有可用的節點。", + "extractMediaMeta": "媒體資訊提取", + "extractMediaMetaDes": "提取媒體檔案的元資料以用於展示和搜尋。預設情況下,非本機儲存策略只會使用“儲存策略原生”方式提取。你可以在儲存策略設定頁面開啟“提取器代理”功能擴充套件第三方儲存策略的縮圖能力。", + "exif": "EXIF", + "exifDes": "從圖片檔案中提取 EXIF 元資料以用於展示和搜尋。", + "music": "音樂元資料", + "musicDes": "從音樂檔案中提取元資料,包括標題、藝術家、專輯等資訊。", + "ffprobe": "FFprobe", + "ffprobeDes": "使用 FFprobe 從視訊和音訊檔案中提取元資料。", + "maxSizeLocal": "最大檔案大小(本地儲存)", + "maxSizeLocalDes": "當檔案儲存在本地儲存策略時,允許提取元資料的最大檔案大小,填寫為 0 時不限制。", + "maxSizeRemote": "最大檔案大小(遠端儲存)", + "maxSizeRemoteDes": "當檔案儲存在第三方儲存策略時,允許提取元資料的最大檔案大小,填寫為 0 時不限制。", + "exifBruteForce": "必要時使用暴力搜尋", + "exifBruteForceDes": "啟用後,如果在標準頭部位置找不到 EXIF 資料,將掃描整個檔案以查詢 EXIF 資料。這可能會增加處理時間,但可以找到非標準位置的 EXIF 資料。", + "musicCover": "歌曲封面", + "musicCoverDes": "提取音訊檔案中的專輯封面, 支援 ID3 (v1, 2.2, 2.3, 2.4) 元資料容器。這一生成器依賴於任一其他影象生成器(Cloudreve 內建 或 VIPS)。", + "notAppliedToNativeGenerator": "{{prefix}}不適用於儲存策略原生生成器。", + "fileBlobMargin": "檔案 Blob 臨時 URL 快取冗餘(秒)", + "fileBlobMarginDes": "當相同的檔案 Blob 被多次請求時,如果最初的 URL 剩餘有效期大於冗餘時長,相同的 URL 會被複用。", + "fileBlobTimeout": "檔案 Blob 臨時 URL 有效期", + "fileBlobTimeoutDes": "限制使用者開啟或下載檔案時,所獲得的臨時連結的有效期,只針對本機儲存策略、WebDAV 或經 Cloudreve 代理的檔案下載。", + "wopiSessionTimeout": "WOPI 會話有效期(秒)", + "wopiSessionTimeoutDes": "限制使用者使用 WOPI 編輯檔案時,單個會話的有效期,過期後使用者需要重新從 Cloudreve 開啟檔案。", + "oauthRefresh": "OAuth 儲存策略憑證重新整理間隔", + "oauthRefreshDes": "設定多久重新整理需要使用 OAuth 的儲存策略(OneDrive)的憑證,可以避免長期未使用儲存策略導致的憑證過期", + "transitParallelNum": "中轉最大並行傳輸", + "transitParallelNumDes": "當單個服務端檔案中轉任務包含多個檔案時,最大並行上傳的數量。", + "failedChunkRetry": "分片錯誤最大重試", + "failedChunkRetryDes": "分片上傳失敗後重試的最大次數,只適用於服務端上傳或中轉。", + "cacheChunks": "快取流式分片檔案以用於重試", + "cacheChunksDes": "開啟後,流式中轉分片上傳時會將分片資料快取在系統臨時目錄,以便用於分片上傳失敗後的重試;\n 關閉後,流式中轉分片上傳不會額外佔用硬碟空間,但分片上傳失敗後整個上傳會立即失敗。", + "folderPropsTimeout": "目錄統計資訊有效期(秒)", + "folderPropsTimeoutDes": "使用者計算目錄統計資訊(大小,包含檔案數量等)時,結果快取的有效期。", + "slaveAPIExpiration": "從機 API 籤名有效期(秒)", + "slaveAPIExpirationDes": "主機訪問從機 API 時使用的籤名有效期。", + "uploadSessionTimeout": "上傳會話有效期 (秒)", + "uploadSessionDes": "在上傳會話有效期內,對於支援的儲存策略,使用者可以斷點續傳未完成的任務。最大可設定的值受限於不同儲存策略服務商的規則。", + "archiveTimeout": "服務端打包下載會話有效期 (秒)", + "advanceOptions": "高階設定", + "emojiOptions": "Emoji 選項", + "addCategorize": "新增分類", + "category": "分類", + "searchQuery": "檔案分類查詢", + "importWopi": "匯入 WOPI 應用設定", + "wopiEndpoint": "WOPI Discovery Endpoint", + "wopiDes": "通過對接支援 WOPI 協議的線上檔案處理系統,擴充套件 Cloudreve 的檔案線上預覽和編輯能力。請在此填寫 WOPI 服務發現地址,比如 https://example.com/hosting/discovery", + "embeddedWebpageViewer": "嵌入網頁式應用", + "wopiViewer": "WOPI 協議應用", + "ext": "副檔名", + "invalidWopiActionMapping": "WOPI Action 對映無效", + "woapiActionMapping": "WOPI Action 對映", + "drawioHost": "DrawIO 例項", + "drawioHostDes": "你可以填寫自建例項的地址。", + "openInNew": "在新視窗直接開啟", + "openInNewDes": "勾選後,會直接彈出新標籤開啟此應用。", + "maxSize": "最大檔案大小", + "maxSizeDes": "此應用支援的最大檔案大小,填寫 0 表示不限制,超出大小時仍會嘗試開啟檔案,但會警告使用者。", + "srcEncodedVar": "經過 URL 編碼後的檔案 Blob 臨時訪問地址", + "srcVar": "檔案 Blob 臨時訪問地址", + "nameEncodedVar": "經過 URL 編碼後的檔名", + "versionEntityVar": "開啟的檔案版本 Blob ID,為空時表示開啟的是最新版本。", + "fileIdVar": "檔案 ID", + "userIdVar": "使用者 ID,未登入時為空。", + "userDisplayNameVar": "經過 URL 編碼後的使用者暱稱。", + "fileViewers": "檔案瀏覽應用", + "addViewer": "新增應用", + "viewerGroupTitle": "應用分組 #{{index}}", + "viewerType": "型別", + "displayName": "名稱", + "displayNameDes": "展示名稱,支援 i18next 鍵值。", + "viewerEnabled": "啟用", + "newFileAction": "新建檔案對映", + "newFileActionDes": "新增對映後,使用者點選“新建”按鈕後會出現此應用的選項。", + "addNewFileAction": "新增對映", + "builtinViewerType": "內建應用", + "wopiViewerType": "WOPI", + "customViewerType": "自定義", + "nMapping": "{{num}} 個", + "editViewerTitle": "編輯 {{name}}", + "builtInIconUrlDes": "此內建應用有預設圖示,圖示地址留空時會使用預設圖示。", + "viewerUrl": "應用 URL", + "viewerUrlDes": "自定義應用的 URL 地址,支援使用 <0>魔法變數。", + "addIcon": "新增圖示", + "exts": "副檔名列表", + "icon": "圖示", + "iconUrl": "圖示地址", + "iconColor": "圖示顏色", + "iconColorDark": "圖示顏色(黑暗模式)", + "fileIcons": "檔案圖示", + "builtinIcon": "內建圖示", + "mimeMapping": "MIME 型別對映", + "mimeMappingDes": "JSON 格式的 MIME 型別對映表,鍵為副檔名,值為 MIME 型別。Cloudreve 會根據副檔名和此設定判斷檔案 MIME 型別。", + "mapProvider": "地圖提供商", + "mapProviderDes": "展示媒體位置資訊時使用的地圖提供商。", + "mapGoogle": "Google Maps", + "mapOpenStreetMap": "OpenStreetMap", + "tileType": "預設地圖型別", + "tileTypeDes": "Google Maps 預設地圖型別。", + "tileTypeTerrain": "地形", + "tileTypeSatellite": "衛星", + "tileTypeGeneral": "常規", + "maxPageSize": "最大分頁大小", + "maxPageSizeDes": "限制使用者可調整的每頁最大檔案數量。", + "maxRecursiveSearch": "最大遞迴搜尋數量", + "maxRecursiveSearchDes": "使用者搜尋檔案時,如果已搜尋的檔案數量超出此限制,搜尋會停止並警告使用者。", + "maxBatchSize": "最大批量運算元量", + "maxBatchSizeDes": "使用者可批量操作的最大檔案數量,只會統計頂層數量,子目錄下的檔案數量不會計入。", + "defaultPagination": "檔案列表分頁方式", + "cursorPagination": "遊標分頁", + "cursorPaginationDes": "使用者滾動到底端後會自動載入更多檔案,對於大量檔案列表效能較好,但無法看到總頁數。", + "offsetPagination": "傳統分頁", + "offsetPaginationDes": "頁面底部會展示分頁導航,使用者可以看到總頁數並跳轉到某一頁,在對於大量檔案列表下效能較差。", + "defaultPaginationDes": "無論上面設定如何,使用者在搜尋時會強制使用遊標分頁。", + "publicResourceMaxAge": "靜態資源快取有效期 (秒)", + "publicResourceMaxAgeDes": "用於告知瀏覽器或 CDN 快取靜態資源的有效期,單位為秒。影響範圍包括檔案、縮圖和使用者頭像。", + "cronDes": "{{des}},此處需要填寫正確的 <0>Cron 表示式。重啟 Cloudreve 後生效。", + "entityCollectInterval": "檔案 Blob 回收間隔", + "entityCollectIntervalDes": "設定多久掃描並刪除過期的檔案 Blob", + "trashBinInterval": "回收站掃描間隔", + "trashBinIntervalDes": "設定多久掃描並刪除回收站中的過期檔案", + "logtoName": "登入方式名稱", + "logtoNameDes": "用於展示給使用者的登入方式名稱,預設為“SSO”,支援 i18next 鍵值。", + "logtoDirectSSO": "直達第三方登入", + "logtoDirectSSODes": "如果你想要跳過 Logto 登入螢幕,直接跳轉到對接的第三方登入或 SSO,請在此填寫第三方登入聯結器的標識,詳情請參考 <0>Logto 檔案。", + "logtoEndpoint": "Logto 端點", + "logtoEndpointDes": "應用管理面板獲取到的 Logto 端點地址,可以為自己部署的例項。", + "logtoKey": "應用金鑰", + "logtoKeyDes": "應用管理頁面建立的應用金鑰。", + "logtoAppIDDes": "你所建立的應用 ID", + "logto": "Logto", + "logtoDes": "借由 <0>Logto, 你可以實現更多第三方平臺的互聯登入,比如 Apple、GitHub、Microsoft Entra ID、Google、簡訊 等。請在 Logto 管理面板建立一個 “傳統網頁應用”,並將 {{url}} 加入到 “重定向 URIs”中。", + "thirdPartySignIn": "第三方登入", + "logo": "LOGO", + "logoDes": "LOGO 影象的地址,用於在左上角展示;請分別提供黑暗模式和日間模式下不同的 LOGO", + "dark": "黑暗模式", + "light": "日間模式", + "tosUrl": "使用條款連結", + "tosUrlDes": "用於在使用者登入或注冊頁尾展示,留空不展示。", + "privacyUrl": "隱私政策連結", + "privacyUrlDes": "用於在使用者登入或注冊頁尾展示,留空不展示。", + "addSecondary": "新增備選站點 URL", + "secondarySiteURL": "備選", + "secondaryDes": "你還可以新增其他備選站點 URL,Cloudreve 會根據使用者實際訪問的 URL 自動選擇是否使用。", + "primarySiteURL": "主要", + "primarySiteURLDes": "主要站點 URL 用於與外部服務通訊和接受回撥(比如:儲存提供商),請使用能被公網訪問的 URL。", + "revert": "撤銷更改", "saved": "設定已更改", "save": "儲存", - "basicInformation": "基本訊息", - "mainTitle": "主標題", - "mainTitleDes": "站點的主標題", - "subTitle": "副標題", - "subTitleDes": "站點的副標題", + "basicInformation": "基本資訊", + "mainTitle": "站點名稱", + "mainTitleDes": "站點的名稱。", "siteDescription": "站點描述", - "siteDescriptionDes": "站點描述訊息,可能會在分享頁面摘要內展示", + "siteDescriptionDes": "站點描述資訊,可能會在分享頁面摘要內展示。", "siteURL": "站點 URL", - "siteURLDes": "非常重要,請確保與實際情況一致。使用雲端儲存策略、支付平台時,請填入可以被外網訪問的地址", "customFooterHTML": "頁尾程式碼", - "customFooterHTMLDes": "在頁面底部插入的自訂 HTML 程式碼", - "pwa": "漸進式應用 (PWA)", + "customFooterHTMLDes": "在頁面底部插入的自定義 HTML 程式碼。", + "announcement": "站點公告", + "announcementDes": "展示給已登陸使用者的公告,留空不展示。當此項內容更改時,所有使用者會重新看到公告。", + "supportHTML": "支援 HTML 程式碼", + "branding": "圖示", "smallIcon": "小圖示", "smallIconDes": "副檔名為 ico 的小圖示地址", "mediumIcon": "中圖示", - "mediumIconDes": "192x192 的中等圖示地址,png 格式", + "mediumIconDes": "192x192 的中等圖示地址,png 格式。", "largeIcon": "大圖示", - "largeIconDes": "512x512 的大圖示地址,png 格式。此圖示還會被用於在 iOS 用戶端切換站點時展示", + "largeIconDes": "512x512 的大圖示地址,png 格式。此圖示還會被用於在 iOS 客戶端切換站點時展示。", "displayMode": "展示模式", - "displayModeDes": "PWA 應用新增後的展示模式", + "displayModeDes": "PWA 應用新增後的展示模式。", "themeColor": "主題色", - "themeColorDes": "CSS 色值,影響 PWA 啟動畫面上狀態欄、內容頁中狀態欄、地址欄的顏色", + "themeColorDes": "CSS 色值,影響 PWA 啟動畫面上狀態列、內容頁中狀態列、位址列的顏色。", "backgroundColor": "背景色", "backgroundColorDes": "CSS 色值", "hint": "提示", - "webauthnNoHttps": "Web Authn 需要您的站點啟用 HTTPS,並確認 參數設定 - 站點訊息 - 站點URL 也使用了 HTTPS 後才能開啟。", - "accountManagement": "註冊與登入", - "allowNewRegistrations": "允許新用戶註冊", - "allowNewRegistrationsDes": "關閉後,無法再透過前台註冊新的用戶", + "webauthnNoHttps": "Web Authn 需要你的站點啟用 HTTPS,並確認 引數設定 - 站點資訊 - 站點URL 也使用了 HTTPS 後才能開啟。", + "accountManagement": "注冊與登入", + "allowNewRegistrations": "允許新使用者注冊", + "allowNewRegistrationsDes": "關閉後,無法再通過前臺注冊新的使用者。", "emailActivation": "郵件啟用", - "emailActivationDes": "開啟後,新用戶註冊需要點擊郵件中的啟用連結才能完成。請確認郵件發送設定是否正確,否則啟用郵件無法送達。", - "captchaForSignup": "註冊驗證碼", - "captchaForSignupDes": "是否啟用註冊表單驗證碼", + "emailActivationDes": "開啟後,新使用者注冊需要點選郵件中的啟用連結才能完成。請確認 <0>郵件發信設定 是否正確,否則啟用郵件無法送達。", + "captchaForSignup": "注冊驗證碼", + "captchaForSignupDes": "是否啟用注冊表單驗證碼。", "captchaForLogin": "登入驗證碼", - "captchaForLoginDes": "是否啟用登入表單驗證碼", + "captchaForLoginDes": "是否啟用登入表單驗證碼。", "captchaForReset": "找回密碼驗證碼", - "captchaForResetDes": "是否啟用找回密碼表單驗證碼", - "webauthnDes": "是否允許用戶使用綁定的外部驗證器登入,站點必須啟用 HTTPS 才能使用。", - "webauthn": "外部驗證器登入", - "defaultGroup": "預設用戶組", - "defaultGroupDes": "用戶註冊後的初始用戶組", - "testMailSent": "測試郵件已發送", + "captchaForResetDes": "是否啟用找回密碼表單驗證碼。", + "webauthnDes": "是否允許使用者使用繫結的硬體認證裝置登入,比如:人臉、指紋或 USB 金鑰;站點必須啟用 HTTPS 才能使用。", + "webauthn": "使用通行金鑰登入", + "defaultGroup": "預設使用者組", + "defaultGroupDes": "使用者注冊後的初始使用者組。", + "testMailSent": "測試郵件已傳送", "testSMTPSettings": "發件測試", - "testSMTPTooltip": "發送測試郵件前,請先儲存已更改的郵件設定;郵件發送結果不會立即回饋,如果您長時間未收到測試郵件,請檢查 Cloudreve 在終端輸出的錯誤日誌。", + "testSMTPTooltip": "Cloudreve 會使用你當前 SMTP 設定傳送測試郵件,測試前無需儲存設定。", "recipient": "收件人地址", - "send": "發送", + "send": "傳送", "smtp": "發信", "senderName": "發件人名", - "senderNameDes": "郵件中展示的發件人姓名", - "senderAddress": "發件人信箱", - "senderAddressDes": "發件信箱的地址", + "senderNameDes": "郵件中展示的發件人姓名。", + "senderAddress": "發件人郵箱", + "senderAddressDes": "發件郵箱的地址。", "smtpServer": "SMTP 伺服器", - "smtpServerDes": "發件伺服器地址,不含埠號", + "smtpServerDes": "發件伺服器地址,不含埠號。", "smtpPort": "SMTP 埠", - "smtpPortDes": "發件伺服器地址埠號", + "smtpPortDes": "發件伺服器地址埠號。", "smtpUsername": "SMTP 使用者名稱", - "smtpUsernameDes": "發信信箱使用者名稱,一般與信箱地址相同", + "smtpUsernameDes": "發信郵箱使用者名稱,一般與郵箱地址相同。", "smtpPassword": "SMTP 密碼", - "smtpPasswordDes": "發信信箱密碼", - "replyToAddress": "回信信箱", - "replyToAddressDes": "用戶回復系統發送的郵件時,用於接收回信的信箱", - "enforceSSL": "強制使用 SSL 連接", - "enforceSSLDes": "是否強制使用 SSL 加密連接。如果無法發送郵件,可關閉此項, Cloudreve 會嘗試使用 STARTTLS 並決定是否使用加密連接", - "smtpTTL": "SMTP 連接有效期 (秒)", - "smtpTTLDes": "有效期內建立的 SMTP 連接會被新郵件發送請求復用", + "smtpPasswordDes": "發信郵箱密碼", + "replyToAddress": "回信郵箱", + "replyToAddressDes": "使用者回復系統傳送的郵件時,用於接收回信的郵箱。", + "enforceSSL": "強制使用 SSL 連線", + "enforceSSLDes": "是否強制使用 SSL 加密連線。如果無法傳送郵件,可關閉此項, Cloudreve 會嘗試使用 STARTTLS 並決定是否使用加密連線。", + "smtpTTL": "SMTP 連線有效期 (秒)", + "smtpTTLDes": "有效期內建立的 SMTP 連線會被新郵件傳送請求復用。", "emailTemplates": "郵件模板", - "activateNewUser": "新用戶啟用", - "activateNewUserDes": "新用戶註冊後啟用郵件的模板", - "resetPassword": "重設密碼", - "resetPasswordDes": "密碼重設郵件模板", - "sendTestEmail": "發送測試郵件", + "activateNewUser": "新使用者啟用", + "resetPassword": "重置密碼", + "sendTestEmail": "傳送測試郵件", "transportation": "傳輸", "workerNum": "Worker 數量", - "workerNumDes": "主機節點任務隊列最多並行執行的任務數,儲存後需要重啟 Cloudreve 生效", - "transitParallelNum": "中轉並行傳輸", - "transitParallelNumDes": "任務隊列中轉任務傳輸時,最大並行協程數", + "workerNumDes": "主機節點任務佇列最多並行執行的任務數,儲存後需要重啟 Cloudreve 生效", "tempFolder": "臨時目錄", - "tempFolderDes": "用於存放解壓縮、壓縮等任務產生的臨時文件的目錄路徑", - "textEditMaxSize": "文件線上編輯最大尺寸", - "textEditMaxSizeDes": "文件文件可線上編輯的最大大小,超出此大小的文件無法線上編輯。此項設定適用於純文本文件、程式碼文件、Office 文件 (WOPI)", - "failedChunkRetry": "分片錯誤重試", - "failedChunkRetryDes": "分片上傳失敗後重試的最大次數,只適用於服務端上傳或中轉", - "cacheChunks": "快取流式分片文件以用於重試", - "cacheChunksDes": "開啟後,流式中轉分片上傳時會將分片數據快取在系統臨時目錄,以便用於分片上傳失敗後的重試;\n 關閉後,流式中轉分片上傳不會額外占用硬碟空間,但分片上傳失敗後整個上傳會立即失敗。", - "resetConnection": "上傳校驗失敗時強制重設連接", - "resetConnectionDes": "開啟後,如果本次策略、頭像等數據上傳校驗失敗,伺服器會強制重設連接", - "expirationDuration": "有效期 (秒)", + "tempFolderDes": "用於存放解壓縮、壓縮等任務產生的臨時檔案的目錄路徑", + "textEditMaxSize": "檔案線上編輯最大大小", + "textEditMaxSizeDes": "檔案檔案可線上編輯的最大大小,超出此大小的檔案無法線上編輯。此項設定適用於純文字檔案、程式碼檔案、Office 檔案 (WOPI)等 Web 線上編輯器。", + "resetConnection": "上傳校驗失敗時強制重置連線", + "resetConnectionDes": "開啟後,如果本次策略、頭像等資料上傳校驗失敗,伺服器會強制重置連線", "batchDownload": "打包下載", - "downloadSession": "下載會話", "previewURL": "預覽連結", - "docPreviewURL": "Office 文件預覽連結", - "uploadSession": "上傳會話", - "uploadSessionDes": "在上傳會話有效期內,對於支援的儲存策略,用戶可以斷點續傳未完成的任務。最大可設定的值受限於不同儲存策略服務商的規則。", - "downloadSessionForShared": "分享下載會話", - "downloadSessionForSharedDes": "設定時間內重複下載分享文件,不會被記入總下載次數", - "onedriveMonitorInterval": "OneDrive 用戶端上傳監控間隔", - "onedriveMonitorIntervalDes": "每間隔所設定時間,Cloudreve 會向 OneDrive 請求檢查用戶端上傳情況已確保用戶端上傳可控", - "onedriveCallbackTolerance": "OneDrive 回調等待", - "onedriveCallbackToleranceDes": "OneDrive 用戶端上傳完成後,等待回調的最大時間,如果超出會被認為上傳失敗", - "onedriveDownloadURLCache": "OneDrive 下載請求快取", - "onedriveDownloadURLCacheDes": "OneDrive 獲取文件下載 URL 後可將結果快取,減輕熱門文件下載API請求頻率", - "slaveAPIExpiration": "從機API請求超時(秒)", - "slaveAPIExpirationDes": "主機等待從機API請求響應的超時時間", - "heartbeatInterval": "節點心跳間隔(秒)", - "heartbeatIntervalDes": "主機節點向從機節點發送心跳的間隔", - "heartbeatFailThreshold": "心跳失敗重試閾值", - "heartbeatFailThresholdDes": "主機向從機發送心跳失敗後,主機可最大重試的次數。重試失敗後,節點會進入恢復模式", - "heartbeatRecoverModeInterval": "恢復模式心跳間隔(秒)", - "heartbeatRecoverModeIntervalDes": "節點因異常被主機標記為恢復模式後,主機嘗試重新連接節點的間隔", - "slaveTransitExpiration": "從機中轉超時(秒)", - "slaveTransitExpirationDes": "從機執行文件中轉任務可消耗的最長時間", - "nodesCommunication": "節點通信", "cannotDeleteDefaultTheme": "不能刪除預設配色", - "keepAtLeastOneTheme": "請至少保留一個配色方案", - "duplicatedThemePrimaryColor": "主色調不能與已有配色重複", - "themes": "主題配色", - "colors": "關鍵色", "themeConfig": "色彩配置", "actions": "操作", "wrongFormat": "格式不正確", - "createNewTheme": "新增配色方案", - "themeConfigDoc": "https://v4.mui.com/zh/customization/default-theme/", - "themeConfigDes": "完整的配置項可在 <0>預設主題 - Material-UI 查閱。", - "defaultTheme": "預設配色", - "defaultThemeDes": "用戶未指定偏好配色時,站點預設使用的配色方案", - "appearance": "界面", - "personalFileListView": "個人文件列表預設樣式", - "personalFileListViewDes": "用戶未指定偏好樣式時,個人文件頁面列表預設樣式", - "sharedFileListView": "目錄分享頁列表預設樣式", - "sharedFileListViewDes": "用戶未指定偏好樣式時,目錄分享頁面的預設樣式", - "primaryColor": "主色調", - "primaryColorText": "主色調文字", - "secondaryColor": "輔色調", - "secondaryColorText": "輔色調文字", "avatar": "頭像", "gravatarServer": "Gravatar 伺服器", - "gravatarServerDes": "Gravatar 伺服器地址,可選擇使用國內鏡像", + "gravatarServerDes": "Gravatar 伺服器地址,可選擇使用國內映象", "avatarFilePath": "頭像儲存路徑", - "avatarFilePathDes": "用戶上傳自訂頭像的儲存路徑", + "avatarFilePathDes": "使用者上傳自定義頭像的儲存路徑,相對於 Cloudreve 資料目錄。", "avatarSize": "頭像檔案大小限制", - "avatarSizeDes": "用戶可上傳頭像文件的最大大小", - "smallAvatarSize": "小頭像尺寸", - "mediumAvatarSize": "中頭像尺寸", - "largeAvatarSize": "大頭像尺寸", - "filePreview": "文件預覽", - "officePreviewService": "Office 文件預覽服務", - "officePreviewServiceDes": "可使用以下替換變數:", - "officePreviewServiceSrcDes": "文件 URL", - "officePreviewServiceSrcB64Des": " Base64 編碼後的文件 URL", - "officePreviewServiceName": "檔案名", + "avatarSizeDes": "使用者可上傳頭像檔案的最大大小", + "avatarImageSize": "影象尺寸 (px)", + "avatarImageSizeDes": "使用者所上傳頭像會被調整到給定的尺寸,單位為畫素。", + "filePreview": "檔案預覽", "thumbnails": "縮圖", - "thumbnailDoc": "有關配置縮圖的更多信息,請參閱 <0>官方文件。", - "thumbnailDocLink":"https://docs.cloudreve.org/use/thumbnails", + "thumbnailDoc": "有關配置縮圖的更多資訊,請參閱 <0>官方檔案。", + "thumbnailDocLink": "https://docs.cloudreve.org/use/thumbnails", "thumbnailBasic": "基本設定", - "generators": "生成器", - "thumbMaxSize": "最大原始文件尺寸", - "thumbMaxSizeDes": "可生成縮圖的最大原始文件的大小,超出此大小的文件不會生成縮圖", - "generatorProxyWarning": "預設情況下,非本機儲存策略只會使用「儲存策略原生」生成器。你可以透過開啓「生成器代理」功能擴展第三方儲存策略的縮圖能力。", + "generators": "縮圖生成器", + "thumbMaxSize": "最大原始檔案尺寸", + "thumbMaxSizeDes": "可生成縮圖的最大原始檔案的大小,超出此大小的檔案不會生成縮圖。", + "generatorProxyWarning": "預設情況下,非本機儲存策略只會使用“儲存策略原生”生成器。你可以在儲存策略設定頁面開啟“生成器代理”功能擴充套件第三方儲存策略的縮圖能力。", "policyBuiltin": "儲存策略原生", - "policyBuiltinDes": "使用儲存提供方原生的圖像處理接口。對於本機和 S3 策略,這一生成器不可用,將會自動順沿其他生成器。對於其他儲存策略,支援的原始圖像格式和大小限制請參考 Cloudreve 文件。", - "cloudreveBuiltin":"Cloudreve 內建", - "cloudreveBuiltinDes": "使用 Cloudreve 內建的圖像處理能力,僅支援 PNG、JPEG、GIF 格式的圖片。", + "policyBuiltinDes": "使用儲存提供方原生的影象處理介面。對於本機和 S3 策略,這一生成器不可用,將會自動順沿其他生成器。對於其他儲存策略,請前往儲存策略設定頁面設定允許的副檔名。", + "cloudreveBuiltin": "Cloudreve 內建", + "cloudreveBuiltinDes": "使用 Cloudreve 內建的影象處理能力,僅支援 PNG、JPEG、GIF 格式的圖片。", "libreOffice": "LibreOffice", - "libreOfficeDes": "使用 LibreOffice 生成 Office 文件的縮圖。這一生成器依賴於任一其他圖像生成器(Cloudreve 內建 或 VIPS)。", + "libreOfficeDes": "使用 LibreOffice 生成 Office 檔案的縮圖。這一生成器依賴於任一其他影象生成器(Cloudreve 內建 或 VIPS)。", "vips": "VIPS", - "vipsDes": "使用 libvips 處理縮圖圖像,支援更多圖像格式,資源消耗更低。", - "thumbDependencyWarning": "LibreOffice 生成器依賴於 Cloudreve 內建 或 VIPS 生成器,請開啓其中任一生成器。", + "vipsDes": "使用 libvips 處理縮圖影象,支援更多影象格式,資源消耗更低。", + "thumbDependencyWarning": "LibreOffice 或歌曲封面生成器依賴於 Cloudreve 內建 或 VIPS 生成器,請開啟其中任一生成器。", "ffmpeg": "FFmpeg", - "ffmpegDes": "使用 FFmpeg 生成影片縮圖。", - "libRaw": "LibRaw", - "libRawDes": "使用 LibRaw 處理 RAW 圖像。", - "executable": "可執行文件", - "executableDes": "第三方生成器可執行文件的地址或命令", + "ffmpegDes": "使用 FFmpeg 生成視訊縮圖。", + "executable": "可執行檔案", + "executableDes": "第三方生成器可執行檔案的路徑或命令。", "executableTest": "測試", "executableTestSuccess": "生成器正常,版本:{{version}}", "generatorExts": "可用副檔名", - "generatorExtsDes": "此生成器可用的文件副檔名列表,多個請使用半形逗號 , 隔開", - "ffmpegSeek": "縮圖截取位置", - "ffmpegSeekDes": "定義縮圖截取的時間,推薦選擇較小值以加速生成過程。如果超出影片實際長度,會導致縮圖截取失敗", + "generatorExtsDes": "此生成器可用的副檔名列表,多個請使用半形逗號 , 隔開。", + "ffmpegSeek": "縮圖擷取位置", + "ffmpegSeekDes": "定義縮圖擷取的時間,推薦選擇較小值以加速生成過程。如果超出視訊實際長度,會導致縮圖擷取失敗", "generatorProxy": "生成器代理", "enableThumbProxy": "使用生成器代理", - "proxyPolicyList": "啓動代理的儲存策略", - "proxyPolicyListDes": "可多選。選中後,儲存策略不支援原生生成縮圖的類型會由 Cloudreve 代理生成", - "thumbWidth": "縮圖寬度", - "thumbHeight": "縮圖高度", - "thumbSuffix": "縮圖文件後綴", - "thumbConcurrent": "縮圖生成並行數量", - "thumbConcurrentDes": "-1 表示自動決定", + "proxyPolicyList": "啟動代理的儲存策略", + "proxyPolicyListDes": "可多選。選中後,儲存策略不支援原生生成縮圖的型別會由 Cloudreve 代理生成", + "thumbWidth": "最大寬度", + "thumbHeight": "最大高度", + "thumbSuffix": "Blob 檔案字尾", + "thumbSuffixDes": "生成的縮圖 Blob 相對於原始 Blob 增加的字尾,", "thumbFormat": "縮圖格式", "thumbFormatDes": "可選:png/jpg", - "thumbQuality": "圖像質量", - "thumbQualityDes": "壓縮質量百分比,只針對 jpg 編碼有效", + "thumbQuality": "影象質量", + "thumbQualityDes": "壓縮質量百分比,只針對 jpg 編碼有效。", "thumbGC": "生成完成後立即回收記憶體", "captcha": "驗證碼", - "captchaType": "驗證碼類型", - "plainCaptcha": "普通", + "captchaType": "驗證碼型別", + "captchaTypeDes": "選擇驗證碼型別和驗證碼服務提供商。", + "plainCaptcha": "圖形", "reCaptchaV2": "reCAPTCHA V2", - "tencentCloudCaptcha": "騰訊雲驗證碼", - "captchaProvider": "驗證碼類型", - "plainCaptchaTitle": "普通驗證碼", + "turnstile": "Cloudflare Turnstile", + "turnstileSiteKey": "站點金鑰", + "turnstileSiteKSecret": "金鑰", + "captchaProvider": "驗證碼型別", "captchaWidth": "寬度", "captchaHeight": "高度", "captchaLength": "長度", @@ -271,17 +416,17 @@ "captchaModeMath": "算數", "captchaModeNumberLetter": "數字+字母", "captchaElement": "驗證碼的形式", - "complexOfNoiseText": "加強干擾文字", - "complexOfNoiseDot": "加強干擾點", + "complexOfNoiseText": "加強幹擾文字", + "complexOfNoiseDot": "加強幹擾點", "showHollowLine": "使用空心線", "showNoiseDot": "使用噪點", - "showNoiseText": "使用干擾文字", + "showNoiseText": "使用幹擾文字", "showSlimeLine": "使用波浪線", "showSineLine": "使用正弦線", "siteKey": "Site KEY", - "siteKeyDes": "<0>應用管理頁面 獲取到的的 網站金鑰", + "siteKeyDes": "<0>應用管理頁面 獲取到的的 網站金鑰。", "siteSecret": "Secret", - "siteSecretDes": "<0>應用管理頁面 獲取到的的 秘鑰", + "siteSecretDes": "<0>應用管理頁面 獲取到的的 祕鑰。", "secretID": "SecretId", "secretIDDes": "<0>訪問金鑰頁面 獲取到的的 SecretId", "secretKey": "SecretKey", @@ -291,22 +436,341 @@ "tCaptchaSecretKey": "App Secret Key", "tCaptchaSecretKeyDes": "<0>圖形驗證頁面 獲取到的的 App Secret Key", "staticResourceCache": "靜態公共資源快取", - "staticResourceCacheDes": "公共可訪問的靜態資源(如:本機策略直鏈、文件下載連接)的快取有效期", - "wopiClient": "WOPI 客戶端", - "wopiClientDes": "透過對接支援 WOPI 協議的線上文件處理系統,擴展 Cloudreve 的文件線上預覽和編輯能力。詳情請參考 <0>官方文件。", - "wopiDocLink": "https://docs.cloudreve.org/use/wopi", - "enableWopi": "使用 WOPI", - "wopiEndpoint": "WOPI Discovery Endpoint", - "wopiEndpointDes": "WOPI 客戶端發現 API 的端點地址", - "wopiSessionTtl": "編輯會話有效期(秒)", - "wopiSessionTtlDes": "用戶打開線上編輯文件會話的有效期,超出此期限的會話無法繼續儲存新更改" + "staticResourceCacheDes": "公共可訪問的靜態資源(如:本機策略直鏈、檔案下載連結)的快取有效期", + "creditSystem": "積分系統", + "creditAndVAS": "積分與增值服務", + "enableCredit": "啟用積分系統", + "enableCreditDes": "啟用積分系統,允許使用者為分享連結設定價格。", + "creditPrice": "積分價格", + "creditPriceDes": "使用貨幣充值積分的價格(以最小貨幣單位計),填寫 0 表示禁止充值積分。", + "shareScoreRate": "分享者傭金比例", + "shareScoreRateDes": "分享連結被購買時,分享者獲得的積分百分比(1-100)", + "cronNotifyUser": "通知超額使用者掃描間隔", + "cronNotifyUserDes": "掃描並傳送郵件提醒超額使用者,", + "cronBanUser": "使用者封禁掃描間隔", + "cronBanUserDes": "掃描並封禁超出儲存且超出緩衝期的使用者", + "anonymousPurchase": "匿名購買", + "anonymousPurchaseDes": "允許未登入使用者直接購買分享連結。", + "shopNavEnabled": "顯示商店導航", + "shopNavEnabledDes": "在側邊欄導航中顯示“商店”條目。", + "paymentSettings": "支付設定", + "currencyCode": "貨幣程式碼", + "currencyCodeDes": "三字母貨幣程式碼(如 USD、CNY、EUR)", + "currencySymbol": "貨幣符號", + "currencySymbolDes": "顯示的貨幣符號(如 $、¥、€)", + "currencyUnit": "貨幣單位", + "currencyUnitDes": "最小貨幣單位(如美元/分為100)", + "paymentProviders": "支付提供商", + "providerName": "提供商名稱,用於展示給使用者。", + "providerType": "提供商型別", + "providerKey": "金鑰", + "selectCurrency": "選擇常用貨幣", + "addPaymentProvider": "新增支付提供商", + "stripeProvider": "Stripe", + "weixinProvider": "微信支付", + "customProvider": "自定義支付渠道", + "customProviderDes": "通過實現 Cloudreve 相容付款介面來對接其他第三方支付平臺,詳情請參考 <0>官方檔案。", + "providerKeyDes": "輸入 Stripe 的 API 金鑰。", + "storageProductSettings": "儲存產品", + "storageProductsDes": "配置使用者可以購買以擴充套件儲存空間的產品。", + "addStorageProduct": "新增產品", + "editStorageProduct": "編輯產品", + "storageSize": "儲存大小", + "storageSizeBytes": "此產品包含的儲存大小。", + "duration": "時長", + "durationSeconds": "時長(秒,例如:2592000 表示 30 天)。", + "price": "價格", + "priceInUnits": "價格(以最小貨幣單位計)", + "priceInUnitsDes": "價格將顯示為:", + "chipLabel": "標籤(可選)", + "chipLabelHelp": "顯示在產品名稱旁邊的短文字標籤。", + "usePoints": "允許使用積分", + "points": "積分", + "pointsHelp": "購買此產品所需的積分數量。", + "pointsUnit": "積分", + "groupProductSettings": "使用者組產品", + "groupProductsDes": "配置使用者可以購買以加入特定使用者組的產品。", + "addGroupProduct": "新增使用者組產品", + "editGroupProduct": "編輯使用者組產品", + "groupId": "使用者組 ID", + "groupIdHelp": "購買此產品後升級到的使用者組。", + "description": "描述", + "descriptionHelp": "輸入特性或優勢,每行一項", + "receiptEmailTemplate": "支付收據模板", + "receiptEmailTemplateDes": "當支付被確認時傳送給使用者的郵件模板。", + "activationEmailTemplate": "賬戶啟用模板", + "activationEmailTemplateDes": "當使用者啟用賬戶時傳送給使用者的郵件模板。", + "quotaExceededEmailTemplate": "儲存配額超出模板", + "quotaExceededEmailTemplateDes": "當使用者超出儲存配額時傳送給使用者的郵件模板。", + "resetPasswordEmailTemplate": "密碼重置模板", + "resetPasswordEmailTemplateDes": "當使用者請求重置密碼時傳送給使用者的郵件模板。", + "addLanguage": "新增語言", + "languageCodeDes": "請選擇要新增的語言。", + "emailSubject": "郵件主題", + "emailSubjectDes": "郵件的主題。", + "emailBody": "郵件內容", + "emailBodyDes": "郵件的內容。你可以使用 <0>魔法變數 來定製郵件內容。", + "orderTitle": "訂單標題", + "themeOptions": "主題選項", + "themeOptionsDes": "為你的站點配置自定義主題選項。這些主題將可供使用者在其偏好設定中選擇。", + "primaryColor": "主色調", + "secondaryColor": "次色調", + "primaryColorDark": "主色調(暗色模式)", + "secondaryColorDark": "次色調(暗色模式)", + "addThemeOption": "新增主題選項", + "editThemeOption": "編輯主題選項", + "invalidThemeConfig": "無效的主題配置。請檢查你的 JSON 語法。", + "themeConfiguration": "主題配置", + "themePreview": "主題預覽", + "lightTheme": "亮色主題", + "darkTheme": "暗色主題", + "previewTitle": "預覽標題", + "previewTextField": "輸入欄位", + "previewPrimary": "主色調", + "invalidThemePreview": "無效的主題配置,無法預覽", + "duplicateThemeColor": "已存在使用此主色調的主題。請選擇不同的顏色。", + "themeDes": "完整的可配置項請參考 <0>Material-UI Default theme viewer。", + "defaultTheme": "預設", + "auditLog": "事件", + "auditLogDes": "配置哪些事件應該被記錄。某些事件可能會被系統用於提供額外功能,例如檔案活動和登入活動。", + "systemEvents": "系統事件", + "systemEventsDes": "與系統操作和狀態相關的事件。", + "userEvents": "使用者事件", + "userEventsDes": "與使用者賬戶、認證和配置檔案更改相關的事件。", + "fileEvents": "檔案事件", + "fileEventsDes": "與檔案操作相關的事件,如上傳、下載和修改。", + "shareEvents": "分享事件", + "shareEventsDes": "與檔案分享和連結訪問相關的事件。", + "versionEvents": "版本事件", + "versionEventsDes": "與檔案版本管理相關的事件。", + "mediaEvents": "媒體事件", + "mediaEventsDes": "與媒體檔案處理相關的事件,如縮圖生成。", + "filesystemEvents": "檔案系統事件", + "filesystemEventsDes": "與檔案系統操作相關的事件,如掛載和歸檔處理。", + "webdavEvents": "WebDAV 事件", + "webdavEventsDes": "與 WebDAV 賬戶管理和訪問相關的事件。", + "paymentEvents": "支付事件", + "paymentEventsDes": "與支付交易和處理相關的事件。", + "emailEvents": "Email 事件", + "emailEventsDes": "與郵件傳送和通知相關的事件。", + "toggleAll": "啟用/禁用所有事件", + "toggleAllDes": "啟用或禁用此類別中的所有事件。", + "event": { + "server_start": "伺服器啟動", + "user_signup": "使用者注冊", + "email_sent": "郵件傳送", + "user_activated": "使用者啟用", + "user_login_failed": "登入失敗", + "user_login": "使用者登入", + "user_token_refresh": "令牌重新整理", + "file_create": "檔案建立", + "file_rename": "檔案重新命名", + "set_file_permission": "許可權更改", + "entity_uploaded": "檔案上傳或更新", + "entity_downloaded": "檔案下載", + "copy_from": "復制來源", + "copy_to": "復制到", + "move_to": "移動到", + "delete_file": "檔案刪除", + "move_to_trash": "移動到回收站", + "share": "分享建立", + "share_link_viewed": "分享連結檢視", + "set_current_version": "設定當前版本", + "delete_version": "刪除版本", + "thumb_generated": "縮圖生成", + "live_photo_uploaded": "上傳 Live Photo", + "update_metadata": "元資料更新", + "edit_share": "分享編輯", + "delete_share": "分享刪除", + "mount": "掛載", + "relocate": "轉移儲存策略", + "create_archive": "建立歸檔", + "extract_archive": "解壓歸檔", + "webdav_login_failed": "WebDAV 登入失敗", + "webdav_account_create": "WebDAV 賬戶建立", + "webdav_account_update": "WebDAV 賬戶更新", + "webdav_account_delete": "WebDAV 賬戶刪除", + "payment_created": "支付建立", + "points_change": "積分更改", + "payment_paid": "支付完成", + "payment_fulfilled": "履行訂單", + "payment_fulfill_failed": "履行訂單失敗", + "storage_added": "儲存擴容", + "group_changed": "使用者組更改", + "user_exceed_quota_notified": "超出配額通知", + "user_changed": "使用者狀態更改", + "get_direct_link": "獲取直鏈", + "link_account": "連結外部賬戶", + "unlink_account": "取消連結外部賬戶", + "change_nick": "更改暱稱", + "change_avatar": "更改頭像", + "membership_unsubscribe": "取消訂閱", + "change_password": "更改密碼", + "enable_2fa": "啟用 2FA", + "disable_2fa": "禁用 2FA", + "add_passkey": "新增通行金鑰", + "remove_passkey": "移除通行金鑰", + "redeem_gift_code": "兌換禮品碼" + }, + "server": "伺服器設定", + "tempPath": "臨時路徑", + "tempPathDes": "儲存臨時檔案的目錄,相對於 Cloudreve 資料目錄。修改前請確保沒有正在進行的佇列任務。", + "siteID": "站點 ID", + "siteIDDes": "用於標識站點的唯一 ID,一般無需修改。", + "siteSecretKey": "主金鑰", + "siteSecretKeyDes": "用於加密使用者令牌、籤名的主金鑰。輪轉後,所有使用者令牌、籤名都將失效。儲存後重啟 Cloudreve 生效。", + "rotateSecretKey": "輪轉主金鑰", + "hashidSalt": "HashID 鹽值", + "hashidSaltDes": "用於生成 HashID 的鹽值,請謹慎更改,更改後會導致現有的直鏈、分享連結等全部失效。", + "accessTokenTTL": "訪問令牌 TTL", + "accessTokenTTLDes": "訪問令牌的有效期,單位為秒。", + "refreshTokenTTL": "重新整理令牌 TTL", + "refreshTokenTTLDes": "重新整理令牌的有效期,單位為秒。影響使用者登入狀態的保持時間。", + "cronGarbageCollect": "垃圾回收掃描間隔", + "cronGarbageCollectDes": "設定多久掃描並回收臨時檔案和 KV 儲存中的過期資料", + "startWithProtocol": "必須以 http:// 或 https:// 開頭", + "tlsWarning": "當前站點使用 https,這裡填寫 http 的 URL 可能會導致異常。", + "blobUrlCache": "Blob URL 快取", + "clearBlobUrlCache": "清除 Blob URL 快取", + "clearBlobUrlCacheDes": "為了增加快取命中率,Cloudreve 會快取並復用 Blob URL。當 CDN 地址等設定發生變更時,請清除快取。", + "cacheCleared": "快取已清除" + }, + "giftCodes": { + "giftCodesSettings": "禮品碼", + "generateGiftCodes": "生成禮品碼", + "giftCodeQuantity": "數量", + "giftCodeQuantityHelp": "要生成的禮品碼數量。", + "giftCodeProductType": "產品型別", + "giftCodeTypePoints": "積分", + "giftCodeTypeStorage": "儲存空間", + "giftCodeTypeGroup": "使用者組", + "giftCodePointsAmount": "積分數量", + "giftCodePointsAmountHelp": "兌換碼被使用時將獲得的積分數量。", + "giftCodeProduct": "產品", + "selectStorageProduct": "選擇儲存產品", + "selectGroupProduct": "選擇使用者組產品", + "giftCodeType": "型別", + "giftCodeAmount": "數量", + "giftCode": "禮品碼", + "giftCodeStatus": "狀態", + "giftCodeUsed": "已使用", + "giftCodeUnused": "可用", + "giftCodeDeleted": "禮品碼已成功刪除", + "giftCodesGenerated": "禮品碼已成功生成", + "noGiftCodes": "暫無禮品碼", + "generatedCodesTitle": "已生成的禮品碼", + "generatedCodesDescription": "復制這些禮品碼以分享給使用者。每個禮品碼只能使用一次。", + "copyAndClose": "復制並關閉", + "duratonTimes": "時長倍數", + "duratonTimesDes": "每個禮品碼包含了多少份對應商品。", + "unknownProduct": "未知產品" }, "policy": { + "deletePolicyConfirmation": "確定要刪除儲存策略 {{name}} 嗎?", + "streamSaver": "由瀏覽器處理下載", + "streamSaverDes": "開啟後,使用者下載檔案時會強制由瀏覽器處理。因為 OneDrive 儲存策略的限制,使用者直接下載檔案時得到的檔名無法與 Cloudreve 內檔名一致,由瀏覽器處理下載可以解決此問題。", + "oauthCallbackFailed": "授權失敗", + "httpsRequired": "Entra ID 應用需要使用 HTTPS 重定向 URL,但是當前站點使用的是 HTTP,後續登入完成後可能會導致重定向失敗,屆時請手動將瀏覽器位址列中的 HTTPS 替換為 HTTP。", + "authorizeMicrosoft": "使用 Microsoft 登入", + "redirectUrl": "重定向 URL", + "redirectUrlDes": "當前展示的是最新的符合要求的重定向 URL,請確認應用設定中的重定向 URL 一致。", + "authorizeOneDrive": "確認 Entra ID 應用設定", + "authorizeOneDriveDes": "請確認以下 Entra ID 應用資訊是否仍然有效,如有需要請做出更改。", + "authorizeNow": "立即授權", + "authorizeAgain": "重新授權", + "notGranted": "無授權賬號,儲存策略無法使用。", + "granted": "已授權賬號,憑證重新整理於 <0>。", + "grantedNotRefresh": "已授權賬號,憑證自上次啟動後尚未重新整理。", + "batchDeleteSize": "最大批量刪除數量", + "batchDeleteSizeDes": "限制單次 API 請求的最大刪除數量,此設定不會影響使用者刪除批量檔案。不填寫會使用預設值 <0>1000,這是官方 S3 API 的最大允許值。", + "bucketPolicy": "桶策略", + "cdnOrCustomDomain": "CDN 或自定義源站域名", + "bucketDomain": "儲存空間域名", + "bucketDomainDes": "填寫你為儲存空間繫結的 CDN 加速域名或者自定義源站域名。", + "storageNodeInternal": "儲存節點(內網 Endpoint)", + "chunkSizeDesOssObs": "允許範圍:100 KB ~ 5 GB,", + "chunkSizeDesQiniuCos": "允許範圍:1 MB ~ 1 GB,", + "chunkSizeDesS3": "允許範圍:5 MB ~ 5 GB,", + "thisIsACustomDomain": "這是一個自定義域名", + "thisIsACustomDomainDes": "如果你為 Bucket 綁定了自定義域名,且需要通過自定義域名進行上傳等管理操作,請勾選此選項。勾選後,Cloudreve 不會在請求域名中嘗試補全 Bucket 名稱。", + "addedManually": "我已自行設定", + "origin": "來源", + "allowMethods": "允許 Methods", + "exposeHeaders": "暴露 Headers", + "allowHeaders": "允許 Headers", + "maxAge": "快取時間", + "accessCredential": "訪問憑證", + "downloadTrafficDiagram": "下載流量路徑演示圖", + "downloadRelay": "下載中轉", + "downloadRelayDes": "開啟後,使用者下載檔案時會通過 Cloudreve 代理。", + "download": "下載", + "downloadCdn": "下載 CDN", + "useDownloadCdn": "使用 CDN 加速下載", + "skipSign": "不為 CDN 簽名文件 URL", + "skipSignDes": "如果你在 COS 域名設置中開啟了 “回源鑑權”,請勾選此項。", + "cdnHost": "CDN 地址", + "downloadCdnDes": "使用者訪問檔案時的 URL 中的主機名、協議等部分會被替換為你指定的 CDN 域名。", + "mediaExtractorProxy": "代理提取媒體資訊", + "mediaExtractorProxyDes": "開啟後,對於儲存端提取器不支援的檔案,Cloudreve 會嘗試提取檔案媒體資訊。請在 <0>媒體處理 中配置 Cloudreve 媒體資訊提取器。", + "mediaExtractorNative": "原生提取器", + "mediaExtractorOss": "智慧媒體管理(IMM)", + "mediaExtractorQiniu": "智慧多媒體服務", + "mediaExtractorCos": "騰訊雲資料永珍", + "mediaExtractorObs": "圖片處理服務", + "mediaExtractorUpyun": "圖片處理服務", + "nativeMediaMetaExts": "使用<0>{{name}}的副檔名", + "nativeMediaMetaExtsGeneralDes": "半形逗號 , 隔開,留空表示不使用<0>{{name}}。", + "nativeMediaMetaExtsRemote": "對於從機儲存,預設情況下支援 EXIF 和音樂元資料,你可以通過配置覆寫在從機端啟用其他生成器。", + "nativeMediaMetaExtOss": "智慧媒體管理(IMM)服務支援處理音訊、視訊和圖片。處理圖片無需手動配置,但如果你需要處理音訊或視訊,需要手動開通 IMM 並繫結到 Bucket, 請參考 <0>檔案 繫結。繫結完成後請在上面加上你想要處理的音視訊的副檔名。", + "nativeMediaMetaExtQiniu": "智慧多媒體服務支援處理常見音訊、視訊和圖片,無需額外配置,在上方填寫你想要處理的媒體的副檔名即可。", + "nativeMediaMetaExtCos": "騰訊雲資料永珍服務支援處理音訊、視訊和圖片。處理圖片無需手動配置,但如果你需要處理音訊或視訊, 請先前往 <0>資料永珍 開通並繫結儲存桶,然後前往 儲存桶設定 - 媒體處理 中開通美圖處理服務。繫結完成後請在上面加上你想要處理的音視訊的副檔名。", + "nativeMediaMetaExtObs": "圖片處理服務支援<0>提取圖片 EXIF。無需手動配置,在上面加上你想要處理的圖片的副檔名即可。", + "nativeMediaMetaExtUpyun": "圖片處理服務支援<0>提取圖片 EXIF。無需手動配置,在上面加上你想要處理的圖片的副檔名即可。", + "thumbProxy": "代理生成縮圖", + "thumbProxyDes": "開啟後,對於不符合原生縮圖條件的檔案,Cloudreve 會嘗試為其生成縮圖檔案,並上傳到儲存端。請在 <0>媒體處理 中配置 Cloudreve 縮圖生成器。", + "nativeThumbnailMaxSize": "使用原生縮圖的最大檔案大小", + "nativeThumbnailMaxSizeDes": "填寫 0 表示不限制,超出此大小的檔案將不會使用原生縮圖。", + "nativeThumbNailsSupportAllExts": "對所有副檔名使用", + "nativeThumbNails": "使用原生縮圖的副檔名", + "nativeThumbNailsGeneralDes": "半形逗號 , 隔開,留空表示不使用原生縮圖。對於列表中列出的副檔名,Cloudreve 會使用儲存端的原生縮圖。", + "nativeThumbNailsGeneralRemote": "對於從機儲存,預設情況下只支援簡單影象和歌曲封面縮圖,你可以通過配置覆寫在從機端啟用其他生成器。", + "nativeThumbNailsGeneralOss": "對於阿里雲 OSS 儲存,<0>圖片處理服務會被用來生成縮圖。", + "nativeThumbNailsGeneralQiniu": "對於七牛雲端儲存,<0>圖片基本處理(imageView2)服務會被用來生成縮圖。", + "nativeThumbNailsGeneralCos": "對於騰訊雲 COS 儲存,<0>騰訊雲資料永珍服務會被用來生成縮圖。", + "nativeThumbNailsGeneralObs": "對於華為雲 OBS 儲存,<0>圖片處理服務會被用來生成縮圖。", + "nativeThumbNailsGeneralUpyun": "對於又拍雲端儲存,<0>圖片處理服務會被用來生成縮圖。", + "preallocate": "預分配硬碟空間", + "preallocateDes": "開啟後,使用者上傳檔案時會預先分配硬碟空間,只在 Linux 或 Darwin 下有效。", + "sourceWebEdit": "Web 線上編輯", + "uploadRelay": "中轉上傳", + "uploadRelayDes": "開啟後,使用者的上傳請求會通過 Cloudreve 中轉到儲存端,因為無法進行分片上傳,請注意調整 Web 伺服器端最大上傳大小限制。", + "customProxy": "自定義代理", + "storageNode": "儲存提供商", + "sourceWeb": "Web / 官方客戶端", + "sourceDav": "WebDAV", + "uploadTrafficDiagram": "上傳流量路徑演示圖", + "node": "儲存節點", + "nodeDes": "請選擇一個從機節點用於儲存檔案,你可以到 <0>儲存節點列表 中建立或管理從機節點。", + "noBindedGroupWarning": "當前儲存策略沒有被分配給任何使用者組,請前往 <0>使用者組列表 為當前儲存策略繫結使用者組。", + "nameRuleImmutable": "修改此設定不會影響儲存策略下已有檔案。Blob 路徑在建立後固定,即使其中魔法變數發生改變,路徑也不會更新。", + "uniqueVarRequired": "請至少包含一個唯一性變數:{{uuid}}、{{randomkey8}}、{{randomkey16}}。", + "storageAndUpload": "儲存與上傳", + "blobFolderNaming": "Blob 儲存目錄", + "blobFolderNamingDes": "檔案 Blob 的存放目錄,可以使用 <0>魔法變數 。", + "blobName": "Blob 名稱", + "blobNameDes": "檔案 Blob 的名稱,可以使用 <0>魔法變數,需要確保為絕對唯一,即使在短時間內多次上傳同一檔案。", + "basicInfo": "基本資訊", + "editX": "編輯 {{name}}", + "noGroupBinded": "沒有繫結任何使用者組", + "create": "建立", + "addXStoragePolicy": "新增 {{type}} 儲存策略", + "loadSummary": "載入統計資料", + "policySummary": "{{count}} 個檔案 Blob ({{size}})", "sharp": "#", "name": "名稱", - "type": "類型", - "childFiles": "下屬文件數", - "totalSize": "數據量", + "type": "型別", + "childFiles": "下屬檔案數", + "totalSize": "資料量", "actions": "操作", "authSuccess": "授權成功", "policyDeleted": "儲存策略已刪除", @@ -319,494 +783,486 @@ "oss": "阿里雲 OSS", "cos": "騰訊雲 COS", "onedrive": "OneDrive", - "s3": "AWS S3", + "s3": "S3 相容", + "obs": "華為雲 OBS", "refresh": "重新整理", "delete": "刪除", "edit": "編輯", - "editInProMode": "專家模式編輯", - "editInWizardMode": "嚮導模式編輯", "selectAStorageProvider": "選擇儲存方式", - "comparesStoragePolicies": "儲存策略對比", - "comparesStoragePoliciesLink": "https://docs.cloudreve.org/use/policy/compare", - "storagePathStep": "上傳路徑", - "sourceLinkStep": "直鏈設定", - "uploadSettingStep": "上傳設定", - "finishStep": "完成", - "policyAdded": "儲存策略已新增", - "policySaved": "儲存策略已儲存", - "editLocalStoragePolicy": "修改本機儲存策略", - "addLocalStoragePolicy": "新增本機儲存策略", - "optional": "可選", - "pathMagicVarDes": "請在下方輸入文件的儲存目錄路徑,可以為絕對路徑或相對路徑(相對於 Cloudreve)。路徑中可以使用魔法變數,文件在上傳時會自動替換這些變數為相應值; 可用魔法變數可參考 <0>路徑魔法變數列表。", - "pathOfFolderToStoreFiles": "儲存目錄", - "filePathMagicVarDes": "是否需要對儲存的物理文件進行重新命名?此處的重新命名不會影響最終呈現給用戶的 檔案名。檔案名也可使用魔法變數, 可用魔法變數可參考 <0>檔案名魔法變數列表。", - "autoRenameStoredFile": "開啟重新命名", - "keepOriginalFileName": "不開啟", - "renameRule": "命名規則", - "next": "下一步", - "enableGettingPermanentSourceLink": "是否允許獲取文件永久直鏈?", - "enableGettingPermanentSourceLinkDes": "開啟後,用戶可以請求獲得能直接訪問到文件內容的直鏈,適用於圖床應用或自用。您可能還需要在用戶組設定中開啟此功能,用戶才可以獲取直鏈。", - "allowed": "允許", - "forbidden": "禁止", - "useCDN": "是否要對下載/直鏈使用 CDN?", - "useCDNDes": "開啟後,用戶訪問文件時的 URL 中的域名部分會被替換為 CDN 域名。", - "use": "使用", - "notUse": "不使用", - "cdnDomain": "選擇協議並填寫 CDN 域名", - "cdnPrefix": "CDN 前綴", - "back": "上一步", - "limitFileSize": "是否限制上傳的單檔案大小?", - "limit": "限制", - "notLimit": "不限制", - "enterSizeLimit": "輸入限制:", - "maxSizeOfSingleFile": "單檔案大小限制", - "limitFileExt": "是否限制上傳文件副檔名?", - "enterFileExt": "輸入允許上傳的文件副檔名,多個請以半形逗號 , 隔開", - "extList": "副檔名列表", - "chunkSizeLabel": "請指定分片上傳時的分片大小,填寫為 0 表示不使用分片上傳。", - "chunkSizeDes": "啟用分片上傳後,用戶上傳的文件將會被切分成分片逐個上傳到儲存端,當上傳中斷後,用戶可以選擇從上次上傳的分片後繼續開始上傳。", - "chunkSize": "分片上傳大小", - "nameThePolicy": "最後一步,為此儲存策略命名:", - "policyName": "儲存策略名", - "finish": "完成", - "furtherActions": "要使用此儲存策略,請到用戶組管理頁面,為相應用戶組綁定此儲存策略。", - "backToList": "返回儲存策略列表", + "maxSizeOfSingleFile": "檔案大小限制", + "maxSizeOfSingleFileDes": "單個檔案的最大大小,輸入限制為 0 時表示不限制單檔案大小。", + "enterFileExt": "留空表示不限制副檔名,多個請以半形逗號 , 隔開。", + "extList": "允許的副檔名", + "chunkSizeDes": "請指定分片上傳時的分片大小,填寫為 0 表示不使用分片上傳,但最大上傳大小可能受限於 Web 伺服器。", + "chunkSizeDesSuffix": "{{prefix}}通過分片上傳,使用者上傳的檔案將會被切分成分片逐個上傳到儲存端,當上傳中斷後,使用者可以選擇從上次上傳的分片後繼續開始上傳。", + "chunkSize": "上傳分片大小", + "policyName": "儲存策略的展示名,也會用於向用戶展示。", "magicVar": { - "fileNameMagicVar": "檔案名魔法變數", + "fileNameMagicVar": "檔名魔法變數", "pathMagicVar": "路徑魔法變數", "variable": "魔法變數", "description": "描述", - "example": "範例", + "example": "示例", "16digitsRandomString": "16 位隨機字元", "8digitsRandomString": "8 位隨機字元", "secondTimestamp": "秒級時間戳", - "nanoTimestamp": "奈秒級時間戳", - "uid": "用戶 ID", - "originalFileName": "原始檔案名", - "originFileNameNoext": "原始檔案名無副檔名", - "extension": "文件副檔名", + "nanoTimestamp": "納秒級時間戳", + "uid": "使用者 ID", + "originalFileName": "原始檔名", + "originFileNameNoext": "無副檔名的原始檔名", + "extension": "副檔名", "uuidV4": "UUID V4", "date": "日期", "dateAndTime": "日期時間", + "randomNumber": "範圍內的隨機數", "year": "年份", "month": "月份", "day": "日", "hour": "小時", "minute": "分鐘", "second": "秒", - "userUploadPath": "用戶上傳路徑" + "path": "使用者上傳檔案時的初始路徑" }, - "storageNode": "儲存端配置", - "communicationOK": "通信正常", - "editRemoteStoragePolicy": "修改從機儲存策略", - "addRemoteStoragePolicy": "新增從機儲存策略", - "remoteDescription": "從機儲存策略允許你使用同樣執行了 Cloudreve 的伺服器作為儲存端, 用戶上傳下載流量透過 HTTP 直傳。", - "remoteCopyBinaryDescription": "將和主站相同版本的 Cloudreve 程序複製至要作為從機的伺服器上。", - "remoteSecretDescription": "下方為系統為您隨機生成的從機端金鑰,一般無需改動,如果有自訂需求,可將您的金鑰填入下方:", - "remoteSecret": "從機密鑰", - "modifyRemoteConfig": "修改從機配置文件。", - "addRemoteConfigDes": " 在從機端 Cloudreve 的同級目錄下新增 <0>conf.ini 文件,填入從機配置,啟動/重啟從機端 Cloudreve。以下為一個可供參考的配置例子,其中金鑰部分已幫您填寫為上一步所生成的。", - "remoteConfigDifference": "從機端配置檔案格式大致與主站端相同,區別在於:", - "remoteConfigDifference1": "<0>System 分區下的 <1>mode 欄位必須更改為 <2>slave。", - "remoteConfigDifference2": "必須指定 <0>Slave 分區下的 <1>Secret 欄位,其值為第二步裡填寫或生成的金鑰。", - "remoteConfigDifference3": "必須啟動跨域配置,即 <0>CORS 欄位的內容,具體可參考上文範例或官方文件。如果配置不正確,用戶將無法透過 Web 端向從機上傳文件。", - "inputRemoteAddress": "填寫從機地址。", - "inputRemoteAddressDes": "如果主站啟用了 HTTPS,從機也需要啟用,並在下方填入 HTTPS 協議的地址。", - "remoteAddress": "從機地址", - "testCommunicationDes": "完成以上步驟後,你可以點擊下方的測試按鈕測試通信是否正常。", - "testCommunication": "測試從機通信", - "pathMagicVarDesRemote": "請在下方輸入文件的儲存目錄路徑,可以為絕對路徑或相對路徑(相對於 從機的 Cloudreve)。路徑中可以使用魔法變數,文件在上傳時會自動替換這些變數為相應值; 可用魔法變數可參考 <0>路徑魔法變數列表。", "storageBucket": "儲存空間", - "editQiniuStoragePolicy": "修改七牛儲存策略", - "addQiniuStoragePolicy": "新增七牛儲存策略", - "wanSiteURLDes": "在使用此儲存策略前,請確保您在 參數設定 - 站點訊息 - 站點URL 中填寫的 地址與實際相符,並且 <0>能夠被外網正常訪問。", - "createQiniuBucket": "前往 <0>七牛控制面板 建立對象儲存資源。", - "enterQiniuBucket": "在下方填寫您在七牛建立儲存空間時指定的「儲存空間名稱」:", + "wanSiteURLDes": "在使用此儲存策略前,請確保你在 引數設定 - 站點資訊 - 站點URL 中填寫的 地址與實際相符,並且 <0>能夠被外網正常訪問。", + "enterQiniuBucket": "前往 <0>七牛控制面板 建立物件儲存資源。在填寫你在七牛建立儲存空間時指定的“儲存空間名稱”。", "qiniuBucketName": "儲存空間名稱", - "bucketTypeDes": "在下方選擇您建立的空間類型,推薦選擇「私有空間」以獲得更高的安全性。", + "cosObsBucketName": "儲存桶名稱", + "bucketType": "Bucket 讀寫許可權", + "bucketTypeDes": "請選擇你建立的儲存空間的讀寫許可權型別。", + "aclType": "訪問控制型別", + "accessTypePulic": "公有讀私有寫", + "accessTypePrivate": "私有讀寫", + "accessType": "訪問許可權", "privateBucket": "私有", - "publicBucket": "公有", - "bucketCDNDes": "填寫您為儲存空間綁定的 CDN 加速域名。", + "privateDes": "Cloudreve 會對檔案 URL 籤名。", + "publicBucket": "公共讀", + "publicStorage": "公開", + "publicDes": "不推薦選擇,Cloudreve 會直接返回檔案的直鏈,無法有效控制檔案的訪問許可權。", + "bucketCDNDes": "填寫你為儲存空間繫結的 CDN 加速域名。", "bucketCDNDomain": "CDN 加速域名", - "qiniuCredentialDes": "在七牛控制面板進入 個人中心 - 金鑰管理,在下方填寫獲得到的 AK、SK。", + "qiniuCredentialDes": "在七牛控制面板進入 個人中心 - 金鑰管理,填寫獲得到的 AK、SK。", "ak": "AK", "sk": "SK", - "cannotEnableForPrivateBucket": "私有空間開啟外鏈功能後,還需要在用戶組裡設定開啟「使用重定向的外鏈」,否則無法正常生成外鏈", - "limitMimeType": "是否限制上傳文件 MimeType?", - "mimeTypeDes": "輸入允許上傳的 MimeType,多個請以半形逗號 , 隔開。七牛伺服器會偵測文件內容以判斷 MimeType,再用判斷值跟指定值進行匹配,匹配成功則允許上傳。", - "mimeTypeList": "MimeType 列表", + "cannotEnableForPrivateBucket": "私有空間開啟外鏈功能後,還需要在使用者組裡設定開啟“使用重定向的外鏈”,否則無法正常生成外鏈", "chunkSizeLabelQiniu": "請指定分片上傳時的分片大小,範圍 1 MB - 1 GB。", - "createPlaceholderDes": "是否要再用戶開始上傳時就建立占位符文件並扣除用戶容量?開啟後,可以防止用戶惡意發起多個上傳請求但不完成上傳。", - "createPlaceholder": "建立占位符文件", - "notCreatePlaceholder": "不建立", "corsSettingStep": "跨域策略", - "corsPolicyAdded": "跨域策略已新增", - "editOSSStoragePolicy": "修改阿里雲 OSS 儲存策略", - "addOSSStoragePolicy": "新增阿里雲 OSS 儲存策略", - "createOSSBucketDes": "前往 <0>OSS 管理控制台 建立 Bucket。注意:建立空間類型只能選擇 <1>標準儲存 或 <2>低頻訪問,暫不支援 <3>歸檔儲存。", - "ossBucketNameDes": "在下方填寫您建立 Bucket 時指定的 <0>Bucket 名稱:", + "corsPolicyAdded": "跨域策略已新增。", + "createOSSBucketDes": "你可前往 <0>OSS 管理控制檯 建立 Bucket。只支援 <1>標準儲存 和 <2>低頻訪問 型別的 Bucket。", "bucketName": "Bucket 名稱", "publicReadBucket": "公共讀", "ossEndpointDes": "轉到所建立 Bucket 的概覽頁面,填寫 <0>訪問域名 欄目下 <1>外網訪問 一行中間的 <2>EndPoint(地域節點)。", + "ossEndpointDesInternalHint": "如需配置內網或自定義域名 Endpoint,可在建立儲存策略後設定。", + "obsEndpointCnameHint": "如需配置自定義域名 Endpoint,可在建立儲存策略後設定。", "endpoint": "EndPoint", - "endpointDomainOnly": "格式不合法,只需輸入域名部分即可", - "ossLANEndpointDes": "如果您的 Cloudreve 部署在阿里雲端計算服務中,並且與 OSS 處在同一可用區下,您可以額外指定使用內網 EndPoint 以節省流量開支。是否要在服務端發送請求時使用 OSS 內網 EndPoint?", + "ossLANEndpointDes": "留空為不使用。如果你的 Cloudreve 部署在阿里雲端計算服務中,並且與 OSS 處在同一可用區下,你可以額外指定使用內網 EndPoint 以節省流量開支, Cloudreve 會在條件滿足時切換到內網 EndPoint 傳送請求。", "intranetEndPoint": "內網 EndPoint", "ossCDNDes": "是否要使用配套的 阿里雲CDN 加速 OSS 訪問?", - "createOSSCDNDes": "前往 <0>阿里雲 CDN 管理控制台 建立 CDN 加速域名,並設定源站為剛建立的 OSS Bucket。在下方填寫 CDN 加速域名,並選擇是否使用 HTTPS:", - "ossAKDes": "在阿里雲 <0>安全訊息管理 頁面獲取 用戶 AccessKey,並填寫在下方。", + "createOSSCDNDes": "前往 <0>阿里雲 CDN 管理控制檯 建立 CDN 加速域名,並設定源站為剛建立的 OSS Bucket。在下方填寫 CDN 加速域名,並選擇是否使用 HTTPS:", + "ossAKDes": "在阿里雲 <0>安全資訊管理 頁面獲取 AccessKey。你也可以在 <1>RAM 訪問控制 中建立擁有 <2>AliyunOSSFullAccess 許可權的 AccessKey。", "shouldNotContainSpace": "不能含有空格", "nameThePolicyFirst": "為此儲存策略命名:", "chunkSizeLabelOSS": "請指定分片上傳時的分片大小,範圍 100 KB ~ 5 GB。", - "ossCORSDes": "此儲存策略需要正確配置跨域策略後才能使用 Web 端上傳文件,Cloudreve 可以幫您自動設定,您也可以參考文件步驟手動設定。如果您已設定過此 Bucket 的跨域策略,此步驟可以跳過。", + "ossCORSDes": "此儲存策略需要正確配置如上跨域策略後才能使用 Web 端上傳檔案,Cloudreve 可以幫你自動設定,你也可以手動設定。如果你已設定過此 Bucket 的跨域策略,此步驟可以跳過。", "letCloudreveHelpMe": "讓 Cloudreve 幫我設定", "skip": "跳過", - "editUpyunStoragePolicy": "修改又拍雲端儲存策略", - "addUpyunStoragePolicy": "新增又拍雲端儲存策略", - "createUpyunBucketDes": "前往 <0>又拍雲面板 建立雲端儲存服務。", - "storageServiceNameDes": "在下方填寫所建立的服務名稱:", + "createUpyunBucketDes": "填寫在 <0>又拍雲面板 建立雲端儲存服務名稱。", "storageServiceName": "服務名稱", - "operatorNameDes": "為此服務建立或授權有讀取、寫入、刪除權限的操作員,然後將操作員訊息填寫在下方:", "operatorName": "操作員名", "operatorPassword": "操作員密碼", - "upyunCDNDes": "填寫為雲端儲存服務綁定的域名,並根據實際情況選擇是否使用 HTTPS:", - "upyunOptionalDes": "此步驟可保持預設並跳過,但是強烈建議您跟隨此步驟操作。", - "upyunTokenDes": "前往所建立雲端儲存服務的 功能配置 面板,轉到 訪問配置 選項卡,開啟 Token 防盜鏈並設定密碼。", + "tokenStatus": "Token 防盜鏈", + "upyunTokenDes": "強烈建議開啟 Token 防盜鏈,前往所建立雲端儲存服務的 <0>功能配置 面板,轉到 <1>訪問控制 選項卡,開啟 Token 防盜鏈並設定密碼。", "tokenEnabled": "已開啟 Token 防盜鏈", "tokenDisabled": "未開啟 Token 防盜鏈", - "upyunTokenSecretDes": "填寫您所設定的 Token 防盜鏈 金鑰", - "upyunTokenSecret": "Token 防盜鏈 金鑰", - "cannotEnableForTokenProtectedBucket": "開啟 Token 防盜鏈後無法使用直鏈功能", - "callbackFunctionStep": "雲函數回調", - "callbackFunctionAdded": "回調雲函數已新增", - "editCOSStoragePolicy": "修改騰訊雲 COS 儲存策略", - "addCOSStoragePolicy": "新增騰訊雲 COS 儲存策略", - "createCOSBucketDes": "前往 <0>COS 管理控制台 建立儲存桶。", - "cosBucketNameDes": "轉到所建立儲存桶的基礎配置頁面,將 <0>空間名稱 填寫在下方:", - "cosBucketFormatError": "空間名格式不正確, 舉例:ccc-1252109809", - "cosBucketTypeDes": "在下方選擇您建立的空間的訪問權限類型,推薦選擇 <0>私有讀寫 以獲得更高的安全性,私有空間無法開啟「獲取直鏈」功能。", + "upyunTokenSecretDes": "填寫你所設定的 Token 防盜鏈金鑰。", + "upyunTokenSecret": "Token 防盜鏈金鑰", + "createCOSBucketDes": "前往 <0>COS 管理控制檯 建立儲存桶,轉到所建立儲存桶的基礎配置頁面,將 <1>儲存桶名稱 填寫到上方。", + "obsBucketDes": "前往 <0>OBS 管理控制檯 建立儲存桶,將 <1>桶名稱 填寫到上方。儲存桶類別只支援 <2>標準儲存 或 <3>低頻訪問儲存。", "cosPrivateRW": "私有讀寫", "cosPublicRW": "公共讀私有寫", - "cosAccessDomainDes": "轉到所建立 Bucket 的基礎配置,填寫 <0>基本訊息 欄目下 給出的 <1>訪問域名。", + "cosAccessDomainDes": "在所建立 Bucket 的概況頁面,填寫 <0>域名資訊 欄目下 給出的 <1>訪問域名。你也可以使用自己繫結的源站域名或 CDN 加速域名。", + "obsEndpointDes": "在所建立儲存桶的概覽頁面,填寫 <0>域名資訊 欄目下 給出的 <1>Endpoint(終端節點)。", "accessDomain": "訪問域名", - "cosCDNDes": "是否要使用配套的 騰訊雲CDN 加速 COS 訪問?", - "cosCDNDomainDes": "前往 <0>騰訊雲 CDN 管理控制台 建立 CDN 加速域名,並設定源站為剛建立的 COS 儲存桶。在下方填寫 CDN 加速域名,並選擇是否使用 HTTPS:", - "cosCredentialDes": "在騰訊雲 <0>訪問金鑰 頁面獲取一對訪問金鑰,並填寫在下方。請確保這對金鑰擁有 COS 和 SCF 服務的訪問權限。", - "secretId": "SecretId", - "secretKey": "SecretKey", - "cosCallbackDes": "COS 儲存桶 用戶端直傳需要借助騰訊雲的 <0>雲函數 產品以確保上傳回調可控。如果您打算將此儲存策略自用,或者分配給可信賴用戶組,此步驟可以跳過。如果是作為公有使用,請務必建立回調雲函數。", - "cosCallbackCreate": "Cloudreve 可以嘗試幫你自動建立回調雲函數,請選擇 COS 儲存桶 所在地域後繼續。建立可能會花費數秒鐘,請耐心等待。建立前請確保您的騰訊雲帳號已開啟雲函數服務。", - "cosBucketRegion": "儲存桶所在地區", - "ap-beijing": "華北地區(北京)", - "ap-chengdu": "西南地區(成都)", - "ap-guangzhou": "華南地區(廣州)", - "ap-guangzhou-open": "華南地區(廣州Open)", - "ap-hongkong": "港澳台地區(中國香港)", - "ap-mumbai": "亞太南部(孟買)", - "ap-shanghai": "華東地區(上海)", - "na-siliconvalley": "美國西部(矽谷)", - "na-toronto": "北美地區(多倫多)", - "applicationRegistration": "應用授權", - "grantAccess": "帳號授權", - "warning": "警告", - "odHttpsWarning": "您必須啟用 HTTPS 才能使用 OneDrive/SharePoint 儲存策略;啟用後同步更改 參數設定 - 站點訊息 - 站點URL。", - "editOdStoragePolicy": "修改 OneDrive/SharePoint 儲存策略", - "addOdStoragePolicy": "新增 OneDrive/SharePoint 儲存策略", - "creatAadAppDes": "前往 <0>Azure Active Directory 控制台 (國際版帳號) 或者 <1>Azure Active Directory 控制台 (世紀互聯帳號) 並登入,登入後進入<2>Azure Active Directory 管理面板,這裡登入使用的帳號和最終儲存使用的 OneDrive 所屬帳號可以不同。", - "createAadAppDes2": "進入左側 <0>應用註冊 選單,並點擊 <1>新註冊 按鈕。", - "createAadAppDes3": "填寫應用註冊表單。其中,名稱可任取;<0>受支援的帳戶類型 選擇為 <1>任何組織目錄(任何 Azure AD 目錄 - 多租戶)中的帳戶和個人 Microsoft 帳戶(例如,Skype、Xbox);<2>重定向 URI (可選) 請選擇 <3>Web,並填寫 <4>{{url}}; 其他保持預設即可", - "aadAppIDDes": "建立完成後進入應用管理的 <0>概覽 頁面,複製 <1>應用程式(用戶端) ID 並填寫在下方:", - "aadAppID": "應用程式(用戶端) ID", - "addAppSecretDes": "進入應用管理頁面左側的 <0>證書和密碼 選單,點擊 <1>新增用戶端密碼 按鈕,<2>截止期限 選擇為 <3>從不。建立完成後將用戶端密碼的值填寫在下方:", - "aadAppSecret": "用戶端密碼", - "aadAccountCloudDes": "選擇您的 Microsoft 365 帳號類型:", - "multiTenant": "國際版", - "gallatin": "世紀互聯版", - "sharePointDes": "是否將文件存放在 SharePoint 中?", - "saveToSharePoint": "存到指定 SharePoint 中", - "saveToOneDrive": "存到帳號預設 OneDrive 驅動器中", + "cosCDNDomainDes": "前往 <0>騰訊雲 CDN 管理控制檯 建立 CDN 加速域名,並設定源站為剛建立的 COS 儲存桶。在下方填寫 CDN 加速域名,並選擇是否使用 HTTPS:", + "cosCredentialDes": "填寫在騰訊雲 <0>訪問金鑰 頁面獲取一對訪問金鑰。請確保這對金鑰擁有 COS 服務的訪問許可權。你也可以建立帶有 <1>程式設計訪問 能力的<2>子使用者,為其賦予 COS 服務的訪問許可權。", + "obsCredentialDes": "填寫在華為雲 <0>訪問金鑰 頁面獲取一對訪問金鑰。你也可以建立帶有 <1>程式設計訪問 能力的<2>IAM 使用者,為其賦予 <3>OBS OperateAccess 許可權。", + "grantAccess": "賬號授權", + "grantAccessLater": "點選下方按鈕建立儲存策略後,還需要在儲存策略設定頁面進行賬號授權。", + "odHttpsWarning": "你必須啟用 HTTPS 才能使用 OneDrive/SharePoint 儲存策略;啟用後同步更改 引數設定 - 站點資訊 - 站點URL。", + "creatAadAppDes": "前往 <0>Microsoft Entra ID 控制檯 並登入,登入後進入<1>Microsoft Entra ID 管理面板,這裡登入使用的賬號和最終儲存使用的 OneDrive 所屬賬號可以不同。", + "createAadAppDes2": "進入左側 <0>應用注冊 選單,並點選 <1>新注冊 按鈕。填寫應用注冊表單。其中,名稱可任取;<2>受支援的帳戶型別 選擇為 <3>任何組織目錄(任何 Azure AD 目錄 - 多租戶)中的帳戶和個人 Microsoft 帳戶(例如,Skype、Xbox);<4>重定向 URI (可選) 請選擇 <5>Web,並填寫 <6>{{url}}; 其他保持預設即可。", + "aadAppIDDes": "進入應用管理的 <0>概覽 頁面,看到的 <1>應用程式(客戶端) ID 的值。", + "entraIdApp": "Entra ID 應用資訊", + "aadAppID": "應用程式(客戶端) ID", + "addAppSecretDes": "客戶端密碼的建立方式:進入應用管理頁面左側的 <0>證書和密碼 選單,點選 <1>新建客戶端密碼 按鈕,<2>截止期限 選擇為最長時間。客戶端密碼過期後,需要重新建立並將其填入儲存策略設定中。", + "aadAppSecret": "客戶端密碼", + "aadAccountCloud": "Microsoft Graph 端點", + "aadAccountCloudDes": "請根據你使用的 Microsoft 365 賬號型別選擇對應的端點。", + "multiTenant": "公有(國際版)", + "gallatin": "世紀互聯", + "sharePointDes": "是否將檔案存放在 SharePoint 中?", + "saveToOneDrive": "存到賬號預設 OneDrive 驅動器中", "spSiteURL": "SharePoint 站點地址", - "odReverseProxyURLDes": "是否要在文件下載時替換為使用自建的反代伺服器?", + "odReverseProxyURLDes": "是否要在檔案下載時替換為使用自建的反代伺服器?", "odReverseProxyURL": "反代伺服器地址", - "chunkSizeLabelOd": "請指定分片上傳時的分片大小,OneDrive 要求必須為 320 KiB (327,680 bytes) 的整數倍。", - "limitOdTPSDes": "是否限制服務端 OneDrive API 請求頻率?", + "chunkSizeDesOd": "允許範圍:5 MB ~ 5GB,OneDrive 要求必須為 320 KiB (327,680 bytes) 的整數倍。", + "limitOdTPSDes": "限制 OneDrive API 請求頻率", "tps": "TPS 限制", - "tpsDes": "限制此儲存策略每秒向 OneDrive 發送 API 請求最大數量。超出此頻率的請求會被限速。多個 Cloudreve 節點轉存文件時,它們會各自使用自己的限流桶,請根據情況按比例調低此數值。Web 端上傳請求並不受此限制。", + "tpsDes": "留空表示不限制。限制此儲存策略每秒向 OneDrive 傳送 API 請求最大數量。超出此頻率的請求會被限速。多個 Cloudreve 節點轉存檔案時,它們會各自使用自己的限流桶,請根據情況按比例調低此數值。Web 端上傳請求並不受此限制。", "tpsBurst": "TPS 突發請求", "tpsBurstDes": "請求空閒時,Cloudreve 可將指定數量的名額預留給未來的突發流量使用。", - "odOauthDes": "但是你需要點擊下方按鈕,並使用 OneDrive 登入授權以完成初始化後才能使用。日後你可以在儲存策略列表頁面重新進行授權。", + "odOauthDes": "但是你需要點選下方按鈕,並使用 OneDrive 登入授權以完成初始化後才能使用。日後你可以在儲存策略列表頁面重新進行授權。", "gotoAuthPage": "轉到授權頁面", - "s3SelfHostWarning": "S3 類型儲存策略目前僅可用於自己使用,或者是給受信任的用戶組使用。", - "editS3StoragePolicy": "修改 AWS S3 儲存策略", - "addS3StoragePolicy": "新增 AWS S3 儲存策略", - "s3BucketDes": "前往 AWS S3 控制台建立儲存桶,在下方填寫您建立儲存桶時指定的 <0>Bucket 名稱:", - "publicAccessDisabled": "阻止全部公共訪問權限", - "publicAccessEnabled": "允許公共讀取", - "s3EndpointDes": "(可選) 指定儲存桶的 EndPoint(地域節點),填寫為完整的 URL 格式,比如 <0>https://bucket.region.example.com。留空則將使用系統生成的預設接入點。", - "selectRegionDes": "選擇儲存桶所在的區域,或者手動輸入區域代碼", - "enterAccessCredentials": "獲取訪問金鑰,並填寫在下方。", - "accessKey": "AccessKey", + "s3BucketDes": "前往 AWS S3 控制檯建立儲存桶,在下方填寫你建立儲存桶時指定的 <0>Bucket 名稱:", + "s3EndpointDes": "指定儲存桶的 EndPoint(地域節點),填寫為完整的 URL 格式,比如 <0>https://bucket.region.example.com。", + "selectRegionDes": "輸入儲存桶所在的區域程式碼,如 <0>us-east-1。對於非 AWS 的 S3 相容儲存提供商,請在其檔案中查詢如何填寫此項。", "chunkSizeLabelS3": "請指定分片上傳時的分片大小,範圍 5 MB ~ 5 GB。", - "editPolicy": "編輯儲存策略", - "setting":"設置項", - "value": "值", - "description": "描述", - "id": "ID", - "policyID": "儲存策略編號", - "policyType": "儲存策略類型", - "server": "Server", - "policyEndpoint": "儲存端 Endpoint", - "bucketID": "儲存桶標識", - "yes": "是", - "no": "否", - "privateBucketDes": "是否為私有空間", - "resourceRootURL": "文件資源根 URL", - "resourceRootURLDes": "預覽/獲取文件外鏈時生成 URL 的前綴", - "akDes": "AccessKey / 更新 Token", - "maxSizeBytes": "最大單文件尺寸 (Bytes)", - "maxSizeBytesDes": "最大可上傳的文件尺寸,填寫為 0 表示不限制", - "autoRename": "自動重新命名", - "autoRenameDes": "是否根據規則對上傳物理文件重新命名", - "storagePath": "儲存路徑", - "storagePathDes": "文件物理儲存路徑", - "fileName": "儲存檔案名", - "fileNameDes": "文件物理儲存檔案名", - "allowGetSourceLink": "允許獲取外鏈", - "allowGetSourceLinkDes": "是否允許獲取外鏈。注意,某些儲存策略類型不支援,即使在此開啟,獲取的外鏈也無法使用", - "upyunToken": "又拍雲防盜鏈 Token", - "upyunOnly": "僅對又拍雲端儲存策略有效", - "allowedFileExtension": "允許文件副檔名", - "emptyIsNoLimit": "留空表示不限制", - "allowedMimetype": "允許的 MimeType", - "qiniuOnly": "僅對七牛儲存策略有效", - "odRedirectURL": "OneDrive 重定向地址", - "noModificationNeeded": "一般新增後無需修改", - "odReverseProxy": "OneDrive 反代伺服器地址", - "odOnly": "僅對 OneDrive 儲存策略有效", - "odDriverID": "OneDrive/SharePoint 驅動器資源標識", - "odDriverIDDes": "僅對 OneDrive 儲存策略有效,留空則使用用戶的預設 OneDrive 驅動器", - "s3Region": "Amazon S3 Region", - "s3Only": "僅對 Amazon S3 儲存策略有效", - "lanEndpoint": "內網 EndPoint", - "ossOnly": "僅對 OSS 儲存策略有效", - "chunkSizeBytes": "上傳分片大小 (Bytes)", - "chunkSizeBytesDes": "分片上傳時單個分片的大小,僅部分儲存策略支援", - "placeHolderWithSize": "上傳前預支用戶儲存", - "placeHolderWithSizeDes": "是否在上傳會話建立時就對用戶儲存進行預支,僅部分儲存策略支援", - "saveChanges": "儲存更改", - "s3EndpointPathStyle": "選擇 S3 Endpoint 地址的格式,如果您不知道該選什麼,保持預設即可。某些第三方 S3 相容儲存策略可能需要更改此選項。開啟後,將會強制使用路徑格式地址,比如 <0>http://s3.amazonaws.com/BUCKET/KEY。", - "usePathEndpoint": "強制路徑格式", - "useHostnameEndpoint": "主機名優先", - "thumbExt": "可生成縮圖的文件副檔名", - "thumbExtDes": "留空表示使用儲存策略預定義集合。對本機、S3儲存策略無效" + "policyEndpoint": "Endpoint", + "s3Region": "地區程式碼", + "s3EndpointPathStyle": "選擇是否強制使用路徑格式 Endpoint。某些第三方 S3 相容儲存可能需要勾選此選項。開啟後,將會強制使用路徑格式地址,比如 <0>http://s3.amazonaws.com/BUCKET/KEY。", + "usePathEndpoint": "強制路徑格式 Endpoint", + "thumbExt": "可生成縮圖的副檔名", + "thumbExtDes": "留空表示使用儲存策略預定義集合。對本機、S3儲存策略無效", + "driverRoot": "驅動器根目錄", + "driverRootDes": "選擇在 OneDrive 賬戶中儲存檔案的位置。更改此選項會導致儲存策略中已有檔案無法訪問。", + "saveToDefaultOneDrive": "儲存檔案到預設 OneDrive 驅動器", + "saveToSharePoint": "儲存檔案到 SharePoint", + "sharePointUrlDes": "輸入 SharePoint 站點 URL。失去焦點後,系統將自動轉換為正確的驅動器標識。" }, "node": { - "#": "#", - "name": "名稱", - "status": "當前狀態", - "features": "已啟用功能", - "action": "操作", - "remoteDownload": "離線下載", - "nodeDisabled": "節點已暫停使用", - "nodeEnabled": "節點已啟用", - "nodeDeleted": "節點已刪除", - "disabled": "未啟用", - "online": "線上", - "offline": "離線", - "addNewNode": "接入新節點", - "refresh": "重新整理", - "enableNode": "啟用節點", - "disableNode": "暫停使用節點", - "edit": "編輯", - "delete": "刪除", - "slaveNodeDes": "您可以新增同樣執行了 Cloudreve 的伺服器作為從機端,正常執行工作的從機端可以為主機分擔某些非同步任務(如離線下載)。請參考下面嚮導部署並配置連接 Cloudreve 從機節點。<0>如果你已經在目標伺服器上部署了從機儲存策略,您可以跳過本頁面的某些步驟,只將從機密鑰、伺服器地址在這裡填寫並保持與從機儲存策略中一致即可。 在後續版本中,從機儲存策略的相關配置會合併到這裡。", - "overwriteDes": "; 以下為可選的設定,對應主機節點的相關參數,可以透過配置文件應用到從機節點,請根據<0>; 實際情況調整。更改下面設定需要重啟從機節點後生效。", - "workerNumDes": "任務隊列最多並行執行的任務數", - "parallelTransferDes": "任務隊列中轉任務傳輸時,最大並行協程數", - "chunkRetriesDes": "中轉分片上傳失敗後重試的最大次數", - "multipleMasterDes": "一個從機 Cloudreve 實例可以對接多個 Cloudreve 主節點,只需在所有主節點中新增此從機節點並保持金鑰一致即可。", - "ariaSuccess": "連接成功,Aria2 版本為:{{version}}", "slave": "從機", "master": "主機", - "aria2Des": "Cloudreve 的離線下載功能由 <0>Aria2 驅動。如需使用,請在目標節點伺服器上以和執行 Cloudreve 相同的用戶身份啟動 Aria2, 並在 Aria2 的配置文件中開啟 RPC 服務,<1>Aria2 需要和{{mode}} Cloudreve 進程共用相同的文件系統。 更多訊息及指引請參考文件的 <2>離線下載 章節。", - "slaveTakeOverRemoteDownload": "是否需要此節點接管離線下載任務?", - "masterTakeOverRemoteDownload": "是否需要主機接管離線下載任務?", - "routeTaskSlave": "開啟後,用戶的離線下載請求可以被分流到此節點處理。", - "routeTaskMaster": "開啟後,用戶的離線下載請求可以被分流到主機處理。", - "enable": "啟用", - "disable": "關閉", - "slaveNodeTarget": "在目標節點伺服器上與節點", - "masterNodeTarget": "在與", - "aria2ConfigDes": "{{target}} Cloudreve 進程相同的文件系統環境下啟動 Aria2 進程。在啟動 Aria2 時,需要在其配置文件中啟用 RPC 服務,並設定 RPC Secret,以便後續使用。以下為一個供參考的配置:", - "enableRPCComment": "啟用 RPC 服務", - "rpcPortComment": "RPC 監聽埠", - "rpcSecretComment": "RPC 授權令牌,可自行設定", - "rpcConfigDes": "推薦在日常啟動流程中,先啟動 Aria2,再啟動節點 Cloudreve,這樣節點 Cloudreve 可以向 Aria2 訂閱事件通知,下載狀態變更處理更及時。當然,如果沒有這一流程,節點 Cloudreve 也會透過輪詢追蹤任務狀態。", - "rpcServerDes": "在下方填寫{{mode}} Cloudreve 與 Aria2 通信的 RPC 服務地址。一般可填寫為 <0>http://127.0.0.1:6800/,其中埠號 <1>6800 與上文配置文件中 <2>rpc-listen-port保持一致。", - "rpcServer": "RPC 伺服器地址", - "rpcServerHelpDes": "包含埠的完整 RPC 伺服器地址,例如:http://127.0.0.1:6800/,留空表示不啟用 Aria2 服務", - "rpcTokenDes": "RPC 授權令牌,與 Aria2 配置文件中 <0>rpc-secret 保持一致,未設定請留空。", - "aria2PathDes": "在下方填寫 Aria2 用作臨時下載目錄的 節點上的 <0>絕對路徑,節點上的 Cloudreve 進程需要此目錄的讀、寫、執行權限。", - "aria2SettingDes": "在下方按需要填寫一些 Aria2 額外參數訊息。", - "refreshInterval": "狀態更新間隔 (秒)", - "refreshIntervalDes": "Cloudreve 向 Aria2 請求更新任務狀態的間隔。", - "rpcTimeout": "RPC 調用超時 (秒)", - "rpcTimeoutDes": "調用 RPC 服務時最長等待時間", - "globalOptions": "全局任務參數", - "globalOptionsDes": "建立下載任務時攜帶的額外設定參數,以 JSON 編碼後的格式書寫,您可也可以將這些設定寫在 Aria2 配置文件裡,可用參數請查閱官方文件", - "testAria2Des": "完成以上步驟後,你可以點擊下方的測試按鈕測試{{mode}} Cloudreve 向 Aria2 通信是否正常。", - "testAria2DesSlaveAddition": "在進行測試前請先確保您已進行並透過上一頁面中的「從機通信測試」。", - "testAria2": "測試 Aria2 通信", - "aria2DocURL": "https://docs.cloudreve.org/use/aria2", - "nameNode": "為此節點命名:", - "loadBalancerRankDes": "為此節點指定負載均衡權重,數值為整數。某些負載均衡策略會根據此數值加權選擇節點", + "noCapabilities": "未啟用任何功能", + "active": "已啟用", + "suspended": "已禁用", + "deleteNodeConfirmation": "確定要刪除節點 {{name}} 嗎?", + "editNode": "編輯節點 {{node}}", + "thisIsMasterNodes": "你正在編輯一個主機節點,即正在服務當前站點的 Cloudreve 例項。", + "enableNode": "啟用節點", + "enableNodeDes": "啟用節點後,節點會接受處理已開啟的功能。", + "name": "名稱", + "nameNode": "節點名稱,也用於向用戶展示。", + "type": "型別", + "server": "節點地址", + "serverDes": "用於與節點通訊的地址。如果你要在此節點儲存檔案,此地址也會暴露給使用者端用於上傳檔案。", + "loadBalancerRankDes": "為此節點指定負載均衡權重,數值為整數, 權重越高,節點被選中的概率越大。", "loadBalancerRank": "負載均衡權重", - "nodeSaved": "節點已儲存!", - "nodeSavedFutureAction": "如果您新增了新節點,還需要在節點列表手動啟動節點才能正常使用。", - "backToNodeList": "返回節點列表", - "communication": "通信配置", - "otherSettings": "雜項訊息", - "finish": "完成", - "nodeAdded": "節點已新增", - "nodeSavedNow": "節點已儲存", - "editNode": "編輯節點", - "addNode": "新增節點" + "slaveSecret": "從機金鑰", + "slaveSecretDes": "用於從機節點與主機節點通訊的金鑰。需要與從機配置檔案中 <0>Slave 下的 <1>Secret 保持一致。", + "testNode": "測試節點通訊", + "testNodeSuccess": "節點通訊成功", + "createArchiveDes": "接受建立壓縮檔案的任務請求。", + "extractArchiveDes": "接受解壓檔案的任務請求。", + "remoteDownloadDes": "接受離線下載的任務請求。啟用後還需要在下方配置離線下載相關資訊。", + "downloader": "下載器", + "aria2Des": "請在目標節點伺服器上以和執行 Cloudreve 相同的使用者/許可權啟動 Aria2, 並在 Aria2 的配置檔案中開啟 RPC 服務,更多資訊及指引請參考檔案的“離線下載”章節。", + "qbittorrentDes": "請在目標節點伺服器上以和執行 Cloudreve 相同的使用者/許可權啟動 qBittorrent, 並在 qBittorrent 的設定中開啟“Web UI”服務,更多資訊及指引請參考檔案的“離線下載”章節。", + "rpcServer": "RPC 伺服器地址", + "rpcServerHelpDes": "包含埠的完整 RPC 伺服器地址,例如:<0>http://127.0.0.1:6800/。", + "rpcToken": "RPC 授權令牌", + "rpcTokenDes": "與 Aria2 配置檔案中 <0>rpc-secret 保持一致,未設定請留空。", + "downloaderOptionDes": "在建立下載任務時額外攜帶的下載器配置,以 JSON 鍵值對格式書寫,具體可參考<0>下載器官方檔案。", + "refreshInterval": "狀態重新整理間隔 (秒)", + "refreshIntervalDes": "Cloudreve 向下載器請求重新整理任務狀態的間隔,實際重新整理間隔也取決於“離線下載”佇列的配置和繁忙程度。", + "waitForSeeding": "等待做種完成", + "waitForSeedingDes": "啟用後,當離線下載任務完成後,會保留此任務在做種狀態,直到在下載器配置的做種結束條件滿足。等待做種發生在離線下載任務完成後,不會影響使用者使用下載的檔案。", + "webUIEndpoint": "Web UI 地址", + "webUIEndpointDes": "qBittorrent 的 Web UI 地址,比如 <0>http://127.0.0.1:8080/。", + "tempPath": "臨時下載目錄", + "tempPathDes": "節點上用於臨時存放離線下載檔案的目錄,節點上的 Cloudreve 程式需要此目錄的讀、寫、執行許可權,下載器也要能夠訪問此目錄。留空會使用預設的臨時檔案路徑。", + "webUIUsername": "Web UI 使用者名稱", + "webUIPassword": "Web UI 密碼", + "webUICredDes": "如果未啟用認證,此處請留空。", + "downloaderTestPass": "成功連線到下載器,版本:{{version}}", + "testDownloader": "測試下載器通訊", + "addNewNode": "新建節點", + "nameTheNode": "為節點命名:", + "runCrSlave": "在節點上執行和主站相同版本的 Cloudreve,並使用以下配置檔案啟動:", + "keepIfUpload": "如果你未來需要使用此節點儲存,請保留下面的跨域配置。", + "storeFiles": "儲存檔案", + "storeFilesDes": "使用此節點儲存使用者檔案。", + "storeFilesHint": "如果你想使用此節點儲存檔案,清前往 <0>儲存策略 頁面新建從機儲存策略,並選擇此節點。", + "runCrWithConfig": "將上述檔案儲存為 <0>config.ini 檔案,並使用此檔案啟動 Cloudreve:<0>./cloudreve -c config.ini。一個從機 Cloudreve 例項可以對接多個 Cloudreve 主節點,只需在所有主節點中新增此從機節點並保持金鑰一致即可。", + "inputServer": "輸入節點的地址:", + "testButton": "可以點選下面按鈕測試通訊是否正常。", + "hostHeaderHint": "如果有簽名錯誤,請檢查從機前置反代是否呈遞了 <0>Host 頭。", + "features": "已啟用功能", + "remoteDownload": "離線下載", + "refresh": "重新整理" }, "group": { + "countUser": "統計", + "anonymous": "未登入訪客使用者組", + "sysGroup": "系統使用者組", + "adminGroup": "管理員使用者組", "#": "#", "name": "名稱", "type": "儲存策略", - "count": "下屬用戶數", + "count": "下屬使用者數", "size": "最大容量", - "action": "操作", - "deleted": "用戶組已刪除", - "new": "新增用戶組", - "aria2FormatError": "Aria2 設置項格式錯誤", - "atLeastOnePolicy": "至少要為用戶組選擇一個儲存策略", - "added": "用戶組已新增", - "saved": "用戶組已儲存", - "editGroup": "編輯 {{group}}", - "nameOfGroup": "用戶組名", - "nameOfGroupDes": "用戶組的名稱", - "storagePolicy": "儲存策略", - "storageDes": "指定用戶組的儲存策略。", + "nameOfGroup": "使用者組名", + "nameOfGroupDes": "使用者組的名稱,用於向用戶展示。", + "availablePolicies": "可用儲存策略", + "availablePoliciesDes": "指定使用者組可用的儲存策略,。修改此設定不會影響使用者已上傳的檔案。", + "availablePolicyDesPro": "可多選,使用者可在選定範圍內自由切換儲存策略。", "initialStorageQuota": "初始容量", - "initialStorageQuotaDes": "用戶組下的用戶初始可用最大容量", - "downloadSpeedLimit": "下載限速", - "downloadSpeedLimitDes": "填寫為 0 表示不限制。開啟限制後,此用戶組下的用戶下載所有支援限速的儲存策略下的文件時,下載最大速度會被限制。", - "bathSourceLinkLimit": "批次生成外鏈數量限制", - "bathSourceLinkLimitDes": "對於支援的儲存策略下的文件,允許用戶單次批次獲取外鏈的最大文件數量,填寫為 0 表示不允許批次生成外鏈。", - "allowCreateShareLink": "允許建立分享", - "allowCreateShareLinkDes": "關閉後,用戶無法建立分享連結", - "allowDownloadShare": "允許下載分享", - "allowDownloadShareDes": "關閉後,用戶無法下載別人建立的文件分享", + "initialStorageQuotaDes": "使用者組下的使用者初始可用最大容量。", + "isAdmin": "管理員使用者組", + "isAdminDes": "開啟後,使用者組下的使用者將擁有管理員許可權。", + "share": "分享", + "allowCreateShareLink": "建立分享連結", + "allowCreateShareLinkDes": "關閉後,使用者無法建立分享連結。", + "shareFree": "無需購買分享連結", + "shareFreeDes": "開啟後,使用者無需購買即可訪問所有付費分享連結。", + "fileManagement": "檔案管理", "allowWabDAV": "WebDAV", - "allowWabDAVDes": "關閉後,用戶無法透過 WebDAV 協議連接至網路硬碟", + "allowWabDAVDes": "關閉後,使用者無法通過 WebDAV 協議連線至網盤。", "allowWabDAVProxy": "WebDAV 代理", - "allowWabDAVProxyDes": "啓用後, 用戶可以配置 WebDAV 代理下載文件的流量", - "disableMultipleDownload": "禁止多次下載請求", - "disableMultipleDownloadDes": "只針對本機儲存策略有效。開啟後,用戶無法使用多執行緒下載工具。", + "allowWabDAVProxyDes": "啟用後, 使用者可以配置 WebDAV 下載經由 Cloudreve 中轉。", + "compressTask": "壓縮/解壓縮檔案", + "compressTaskDes": "開啟後,使用者可以線上壓縮/解壓縮檔案。", + "compressSize": "待壓縮檔案最大大小", + "compressSizeDes": "使用者可建立的壓縮任務的檔案最大總大小,填寫為 0 表示不限制。這一限制在建立壓縮任務時不會檢查,當執行時已處理原始檔案總大小超過此限制時,任務會失敗。", + "decompressSize": "待解壓檔案最大大小", + "decompressSizeDes": "使用者可建立的解壓縮任務的檔案最大總大小,填寫為 0 表示不限制。", "allowRemoteDownload": "離線下載", - "allowRemoteDownloadDes": "是否允許用戶建立離線下載任務", - "aria2Options": "Aria2 任務參數", - "aria2OptionsDes": "此用戶組建立離線下載任務時額外攜帶的參數,以 JSON 編碼後的格式書寫,您可也可以將這些設定寫在 Aria2 配置文件裡,可用參數請查閱官方文件", - "aria2BatchSize": "Aria2 批次下載最大數量", - "aria2BatchSizeDes": "允許用戶同時進行的離線下載任務數量,填寫為 0 或留空表示不限制。", + "allowRemoteDownloadDes": "是否允許使用者建立離線下載任務。如需使用離線下載,還需要在 <0>節點列表 中有開啟離線下載功能的節點。", + "aria2Options": "下載器任務引數", + "aria2OptionsDes": "qBittorrent 或 Aria2 下載器的任務額外配置引數,以 JSON 編碼後的鍵-值格式書寫,可用引數請查閱官方檔案。", + "aria2BatchSize": "批量離線下載最大數量", + "aria2BatchSizeDes": "批量建立離線下載時的最大數量,填寫為 0 表示不限制。", + "migratePolicy": "儲存策略轉移", + "migratePolicyDes": "是否使用者建立儲存策略轉移任務。", + "advanceDelete": "高階檔案刪除選項", + "advanceDeleteDes": "開啟後,使用者在前臺刪除檔案時可以選擇是否保留物理檔案,請只開放給可信使用者組。", + "allowSelectNode": "允許選擇節點", + "allowSelectNodeDes": "開啟後,使用者可以在建立任務前選擇處理節點;關閉後,系統會在使用者組允許的節點下自動分配節點。", + "allowedNodes": "可用節點", + "allowedNodesDes": "指定使用者組可用的任務處理節點,留空表示全部節點都可用。使用者只能在此列表內選擇或被負載均衡分配節點。目前覆蓋的任務範圍是:離線下載、檔案壓縮或解壓縮。其他任務會分配給主機處理。", + "allNodes": "所有節點", + "esclateAnonymity": "提升匿名使用者許可權", + "esclateAnonymityDes": "開啟後,使用者可以為匿名使用者設定更高許可權(修改/建立/刪除);關閉後,使用者最高只能賦予匿名使用者只讀許可權。更改此設定不會影響已設定的分享連結或檔案。", + "allowDownloadShare": "訪問分享連結", + "allowDownloadShareDes": "關閉後,使用者無法檢視別人的分享連結。此項設定優先順序高於分享連結的許可權設定。", + "deletedNode": "已刪除節點 #{{id}}", + "maxWalkedFiles": "最大遍歷檔案數", + "maxWalkedFilesDes": "在某些需要深層遍歷檔案的操作中,最大允許遍歷的檔案數。", + "trashBinDuration": "回收站保留時間(秒)", + "trashBinDurationDes": "回收站中檔案的保留時長,超期後檔案將被徹底刪除。更改此設定不會影響已經在回收站中的檔案。", "serverSideBatchDownload": "服務端打包下載", - "serverSideBatchDownloadDes": "是否允許用戶多選文件使用服務端中轉打包下載,關閉後,用戶仍然可以使用純 Web 端打包下載功能。", - "compressTask": "壓縮/解壓縮 任務", - "compressTaskDes": "是否用戶建立 壓縮/解壓縮 任務", - "compressSize": "待壓縮文件最大大小", - "compressSizeDes": "用戶可建立的壓縮任務的文件最大總大小,填寫為 0 表示不限制", - "decompressSize": "待解壓文件最大大小", - "decompressSizeDes": "用戶可建立的解壓縮任務的文件最大總大小,填寫為 0 表示不限制", - "redirectedSource": "使用重定向的外鏈", - "redirectedSourceDes": "開啟後,用戶獲取的文件外鏈將由 Cloudreve 中轉,連結較短。關閉後,用戶獲取的文件外鏈會變成文件的原始連結。部分儲存策略獲取的非中轉外鏈無法保持永久有效,請參閱 <0>比較儲存策略。", - "advanceDelete": "允許使用高級文件刪除選項", - "advanceDeleteDes": "開啓後,用戶在前臺刪除文件時可以選擇是否強制刪除、是否僅解除物理連結。這些選項與後臺管理面板刪除文件時類似,請只開放給可信用戶組。" + "serverSideBatchDownloadDes": "是否允許使用者多選檔案使用服務端中轉打包下載,關閉後,使用者仍然可以使用純 Web 端打包下載功能。", + "uploadDownload": "上傳和下載", + "getDirectLink": "獲取直鏈", + "getDirectLinkDes": "是否允許使用者獲取檔案的直鏈。", + "bathSourceLinkLimit": "批量生成外直鏈量限制", + "bathSourceLinkLimitDes": "允許使用者單次批量獲取直鏈的最大檔案數量,填寫為 0 表示不允許獲取直鏈。", + "redirectedSource": "使用重定向的直鏈", + "redirectedSourceDes": "推荐開啟。開啟後,使用者獲取的檔案直鏈將由 Cloudreve 中轉,連結較短。關閉後,使用者獲取的檔案直鏈會變成檔案的原始連結,且與檔案版本綁定。部分儲存策略在某些設定下獲取的非中轉直鏈無法保持永久有效,請參閱 Cloudreve 檔案。", + "downloadSpeedLimit": "下載限速", + "downloadSpeedLimitDes": "填寫為 0 表示不限制。開啟限制後,使用者下載所有支援限速的儲存策略下的檔案時,下載最大速度會被限制。", + "anonymousHint": "此使用者組對應著未登入的匿名訪客。", + "create": "新建", + "copyFromExisting": "從現有使用者組復制?", + "notCopy": "不復制", + "confirmDelete": "確認要刪除使用者組 {{group}}?", + "new": "新建使用者組", + "editGroup": "編輯 {{group}}" }, "user": { - "deleted": "用戶已刪除", - "new": "新增用戶", + "createdAt": "建立日期", + "originUserGroup": "原始使用者組", + "originUserGroupDes": "使用者在購買使用者組前所屬的使用者組,當前使用者組到期後會回退到此使用者組。", + "noOriginUserGroup": "無", + "groupExpired": "使用者組過期日期", + "groupExpiredDes": "ISO8601 格式的使用者組到期日期,留空表示當前使用者組永久有效。", + "openUserFiles": "開啟使用者檔案", + "id": "ID", + "idValue": "{{id}} ({{hash_id}})", + "avatar": "頭像", + "removeAvatar": "移除頭像", + "userDialogTitle": "使用者詳情", + "2FAEnabled": "已啟用二步驗證", + "qqEnabled": "已繫結 QQ", + "logtoEnabled": "已繫結 Logto", + "deleted": "使用者已刪除", + "new": "新建使用者", "filter": "過濾", - "selectedObjects": "已選擇 {{num}} 個對象", + "emptyNoFilter": "留空表示不過濾此項。", + "selectedObjects": "已選擇 {{num}} 個物件", "nick": "暱稱", "email": "Email", - "group": "用戶組", + "group": "使用者組", "status": "狀態", "usedStorage": "已用空間", - "active": "正常", - "notActivated": "未啟用", - "banned": "被封禁", - "bannedBySys": "超額封禁", + "status_active": "正常", + "status_inactive": "未啟用", + "status_manual_banned": "手動封禁", + "status_sys_banned": "系統封禁", "toggleBan": "封禁/解封", "filterCondition": "過濾條件", "all": "全部", - "userStatus": "用戶狀態", - "searchNickUserName": "搜尋 暱稱 / 使用者名稱", + "userStatus": "使用者狀態", "apply": "應用", - "added": "用戶已新增", - "saved": "用戶已儲存", "editUser": "編輯 {{nick}}", "password": "密碼", "passwordDes": "留空表示不修改", - "groupDes": "用戶所屬用戶組", - "2FASecret": "二步驗證金鑰", - "2FASecretDes": "用戶二步驗證器的金鑰,清空表示未啟用。" + "groupDes": "使用者所屬使用者組", + "2FA": "二步驗證", + "notEnabled": "未啟用", + "reset2Fa": "關閉", + "reset": "重置", + "confirmDelete": "確認要刪除使用者 {{user}}?", + "deleteXUsers": "刪除 {{num}} 個使用者", + "confirmBatchDelete": "確認要刪除 {{num}} 個使用者?", + "calibrateStorage": "校准儲存空間", + "calibrateStorageSuccess": "儲存空間校准成功" }, "file": { - "name": "檔案名", - "deleteAsync": "刪除任務將在後台執行", - "import": "從外部匯入", + "deleteXFiles": "刪除 {{num}} 個檔案", + "confirmBatchDelete": "確定要刪除 {{num}} 個檔案?", + "confirmDelete": "確認要刪除檔案 {{file}}?", + "haveShares": "擁有分享連結", + "haveDirectLinks": "擁有中轉直鏈", + "directLinkId": "連結標識", + "directLinks": "中轉直鏈", + "noRecords": "沒有記錄", + "speed": "限速", + "downloads": "下載次數", + "shareLink": "分享連結", + "shareLinkNum": "{{num}} 個 (<0>檢視)", + "blobType": "型別", + "noEntities": "沒有 Blob", + "blobs": "Blobs", + "creator": "建立者", + "source": "源", + "key": "鍵", + "value": "值", + "isPublic": "公開", + "noMetadata": "沒有元資料", + "metadata": "元資料", + "id": "ID", + "primaryStoragePolicy": "首選儲存策略", + "fileDialogTitle": "檔案詳情", + "name": "檔名", + "deleteAsync": "刪除任務將在後臺執行", "forceDelete": "強制刪除", "size": "大小", - "uploader": "上傳者", + "sizeUsed": "佔用空間", + "uploader": "所有者", "createdAt": "建立於", "uploading": "上傳中", "unknownUploader": "未知", - "uploaderID": "上傳者 ID", - "searchFileName": "搜尋檔案名", + "uploaderID": "所有者 ID", + "searchFileName": "搜尋檔名", "storagePolicy": "儲存策略", - "selectTargetUser": "請先選擇目標用戶", - "importTaskCreated": "匯入任務已建立,您可以在「持久任務」中查看執行情況", + "selectTargetUser": "請先選擇目標使用者", + "importTaskCreated": "匯入任務已建立,你可以在“持久任務”中檢視執行情況", "manuallyPathOnly": "選擇的儲存策略只支援手動輸入路徑", "selectFolder": "選擇目錄", "importExternalFolder": "匯入外部目錄", - "importExternalFolderDes": "您可以將儲存策略中已有文件、目錄結構匯入到 Cloudreve 中,匯入操作不會額外占用物理儲存空間,但仍會正常扣除用戶已用容量空間,空間不足時將停止匯入。", - "storagePolicyDes": "選擇要匯入文件目前儲存所在的儲存策略", - "targetUser": "目標用戶", - "targetUserDes": "選擇要將文件匯入到哪個用戶的文件系統中,可透過暱稱、信箱搜尋用戶", + "importExternalFolderDes": "你可以將儲存策略中已有檔案、目錄結構匯入到 Cloudreve 中,匯入操作不會額外佔用物理儲存空間,但仍會正常扣除使用者已用容量空間,空間不足時將停止匯入。", + "storagePolicyDes": "選擇要匯入檔案目前儲存所在的儲存策略", + "targetUser": "目標使用者", + "targetUserDes": "選擇要將檔案匯入到哪個使用者的檔案系統中,可通過暱稱、郵箱搜尋使用者", "srcFolderPath": "原始目錄路徑", "select": "選擇", "selectSrcDes": "要匯入的目錄在儲存端的路徑", "dstFolderPath": "目的目錄路徑", - "dstFolderPathDes": "要將目錄匯入到用戶文件系統中的路徑", + "dstFolderPathDes": "要將目錄匯入到使用者檔案系統中的路徑", "recursivelyImport": "遞迴匯入子目錄", "recursivelyImportDes": "是否將目錄下的所有子目錄遞迴匯入", "createImportTask": "建立匯入任務", - "unlink": "解除關聯(保留物理文件)" + "unlink": "解除關聯(保留物理檔案)" + }, + "entity": { + "refenenceCount": "引用次數", + "waitForRecycle": "等待回收", + "entityDialogTitle": "Blob 詳情", + "uploadSessionID": "上傳會話 ID", + "referredFiles": "關聯檔案", + "confirmBatchDelete": "確認要刪除 {{num}} 個 Blob?", + "deleteXEntities": "刪除 {{num}} 個 Blob", + "forceDelete": "強制刪除", + "forceDeleteDes": "無論物理檔案是否刪除成功,都會刪除 Blob 記錄。" + }, + "event": { + "initiator": "發起者", + "event": "事件", + "userID": "使用者 ID", + "ip": "IP", + "type": "型別", + "correlationId": "請求 ID", + "fileID": "檔案 ID", + "emailSend": "傳送郵件 “{{title}}” 到 {{email}}", + "emailFailed": "郵件佇列啟動失敗", + "signinFailed": "登入失敗: {{reason}}", + "createDavAccount": "建立 WebDAV 賬戶: {{account}}", + "updateDavAccount": "更新 WebDAV 賬戶: {{account}}", + "deleteDavAccount": "刪除 WebDAV 賬戶: {{account}}", + "pointsChange": "積分變化: {{points}}", + "storageAdded": "購買了 {{size}} 容量", + "nickChange": "暱稱從 {{old}} 改為 {{new}}", + "eventDialogTitle": "事件詳情", + "userAgent": "使用者代理", + "linkedUser": "關聯使用者", + "datetime": "時間", + "linkedFile": "關聯檔案", + "linkedEntity": "關聯 Blob", + "linkedShare": "關聯分享", + "rawContent": "原始記錄", + "confirmDelete": "確認要刪除這個事件?", + "deleteXEvents": "刪除 {{num}} 個事件", + "confirmBatchDelete": "確認要刪除 {{num}} 個事件?" }, "share": { - "deleted": "分享已刪除", - "objectName": "對象名", + "confirmBatchDelete": "確認要刪除 {{num}} 個分享?", + "confirmDelete": "確認要刪除這個分享?", + "deleteXShares": "刪除 {{num}} 個分享", + "shareDialogTitle": "分享詳情", + "shareLink": "分享連結", + "deleted": "檔案已刪除", + "srcFileName": "原始檔", "views": "瀏覽", "downloads": "下載", "price": "積分", "autoExpire": "自動過期", "owner": "分享者", "createdAt": "分享於", - "public": "公開", - "private": "私密", - "afterNDownloads":"{{num}} 次下載後", + "private": "從個人主頁隱藏", + "yes": "是", + "no": "否", + "afterNDownloads": "{{num}} 次下載後", "none": "無", - "srcType": "源文件類型", + "srcType": "原始檔型別", "folder": "目錄", - "file": "文件" + "file": "檔案" }, "task": { - "taskDeleted": "任務已刪除", - "howToConfigAria2": "如何配置離線下載?", - "srcURL": "源地址", + "confirmDelete": "確認要刪除這個任務?", + "confirmBatchDelete": "確認要刪除 {{num}} 個任務?", + "deleteXTasks": "刪除 {{num}} 個任務", + "blobID": "Blob ID", + "retryIndex": "重試序號", + "entityError": "回收失敗的 Blob", + "updatedAt": "更新於", + "taskDialogTitle": "任務詳情", + "explicitEntityRecycle": "顯式回收檔案 Blob: {{blobs}}", + "entityRecycleRoutine": "定時掃描回收檔案 Blob", + "mediaMetadata": "提取 Blob <0>#{{entityID}} 的媒體資訊", + "uploadSentinelCheck": "檢查上傳會話 {{uploadSessionID}} 狀態", + "remoteDownload": "離線下載:", + "owner": "所有者", + "content": "內容", + "status": "狀態", + "create_archive": "建立壓縮檔案", + "extract_archive": "解壓檔案", + "relocate": "轉移儲存策略", + "remote_download": "離線下載", + "media_meta": "媒體資訊提取", + "entity_recycle_routine": "Blob 掃描回收", + "explicit_entity_recycle": "顯式 Blob 回收", + "upload_sentinel_check": "上傳哨兵檢查", + "type": "型別", "node": "處理節點", "createdBy": "建立者", "ready": "就緒", @@ -817,11 +1273,165 @@ "finished": "完成", "canceled": "取消/停止", "unknown": "未知", - "aria2Des": "Cloudreve 的離線下載支援主從分散模式。您可以配置多個 Cloudreve 從機節點,這些節點可以用來處理離線下載任務,分散主節點的壓力。當然,您也可以配置只在主節點上處理離線下載任務,這是最簡單的一種方式。", - "masterAria2Des": "如果您只需要為主機啟用離線下載功能,請 <0>點擊這裡 編輯主節點;", - "slaveAria2Des": "如果您想要在從機節點上分散處理離線下載任務,請 <0>點擊這裡 新增並配置新節點。", - "editGroupDes": "當你新增多個可用於離線下載的節點後,主節點會將離線下載請求輪流發送到這些節點處理。節點離線下載配置完成後,您可能還需要 <0>到這裡 編輯用戶組,為對應用戶組開啟離線下載權限。", - "lastProgress": "最後進度", - "errorMsg": "錯誤訊息" + "errorMsg": "錯誤資訊" + }, + "payment": { + "tradeNo": "交易單號", + "productType": "商品型別", + "providerID": "支付方式", + "status": "狀態", + "deleteXPayments": "刪除 {{num}} 個訂單" + }, + "vas": { + "confirmDelete": "確認要刪除這些訂單?", + "vas": "增值服務", + "reports": "舉報", + "orders": "訂單", + "initialFiles": "初始檔案", + "initialFilesDes": "指定使用者注冊後初始擁有的檔案。輸入檔案 ID 搜尋並新增現有檔案。", + "filterEmailProvider": "過濾注冊郵箱域", + "filterEmailProviderDisabled": "不啟用", + "filterEmailProviderWhitelist": "白名單", + "filterEmailProviderBlacklist": "黑名單", + "filterEmailProviderDes": "只允許使用特定的郵箱注冊站點,第三方 SSO 登入不受此限制。", + "filterEmailProviderRule": "郵箱域過濾規則", + "filterEmailProviderRuleDes": "多個域請使用半形逗號隔開。", + "qqConnect": "QQ 互聯", + "qqConnectHint": "在 <0>QQ 互聯開放平臺 建立應用時,回撥地址請填寫:{{url}}", + "enableQQConnect": "開啟QQ互聯", + "enableQQConnectDes": "是否允許繫結QQ、使用QQ登入本站", + "loginWithoutBinding": "未繫結時可直接登入", + "loginWithoutBindingDes": "開啟後,如果使用者使用了第三方登入,但是沒有已繫結的注冊使用者,系統會為其建立使用者並登入。這種方式建立的使用者日後只能使用第三方登入。", + "appid": "APP ID", + "appidDes": "應用管理頁面獲取到的的 APP ID", + "appKey": "APP KEY", + "appKeyDes": "應用管理頁面獲取到的的 APP KEY", + "overuseReminder": "超額提醒", + "overuseReminderDes": "使用者因增值服務過期,容量超出限制後傳送的提醒郵件模板", + "vasSetting": "支付/雜項設定", + "storagePack": "容量包", + "purchasableGroups": "可購使用者組", + "giftCodes": "兌換碼", + "enable": "開啟", + "appID": "App- ID", + "appIDDes": "當面付應用的 APPID", + "rsaPrivate": "RSA 應用私鑰", + "rsaPrivateDes": "當面付應用的 RSA2 (SHA256) 私鑰,一般是由你自己生成。詳情參考 <0>生成 RSA 金鑰。", + "alipayPublicKey": "支付寶公鑰", + "alipayPublicKeyDes": "由支付寶提供,可在 應用管理 - 應用資訊 - 介面加簽方式 中獲取。", + "wechatPay": "微信官方掃碼支付", + "applicationID": "應用 ID", + "applicationIDDes": "直連商戶申請的公眾號或移動應用 appid", + "merchantID": "直連商戶號", + "merchantIDDes": "直連商戶的商戶號,由微信支付生成並下發。", + "apiV3Secret": "API v3 金鑰", + "apiV3SecretDes": "商戶需先在【商戶平臺】-【API安全】的頁面設定該金鑰,請求才能通過微信支付的籤名校驗。金鑰的長度為 32 個位元組。", + "mcCertificateSerial": "商戶證書序列號", + "mcCertificateSerialDes": "登入商戶平臺【API安全】-【API證書】-【檢視證書】,可檢視商戶 API 證書序列號。", + "mcAPISecret": "商戶API 私鑰", + "mcAPISecretDes": "私鑰檔案 apiclient_key.pem 的內容。", + "payjs": "PAYJS 微信支付", + "payjsWarning": "此服務由第三方平臺 <0>PAYJS 提供, 產生的任何糾紛與 Cloudreve 開發者無關。", + "mcNumber": "商戶號", + "mcNumberDes": "可在 PAYJS 管理面板首頁看到", + "communicationSecret": "通訊金鑰", + "otherSettings": "雜項設定", + "banBufferPeriod": "封禁緩衝期 (秒)", + "banBufferPeriodDes": "使用者保持容量超額狀態的最長時長,超出時長該使用者會被系統凍結。", + "allowSellShares": "允許為分享定價", + "allowSellSharesDes": "開啟後,使用者可為分享設定積分價格,下載需要扣除積分。", + "creditPriceRatio": "積分到賬比率 (%)", + "creditPriceRatioDes": "購買下載設定價格的分享,分享者實際到賬的積分比率。", + "creditPrice": "積分價格 (分)", + "creditPriceDes": "充值積分時的價格", + "add": "新增", + "name": "名稱", + "price": "單價", + "duration": "時長", + "size": "大小", + "actions": "操作", + "orCredits": " 或 {{num}} 積分", + "highlight": "突出展示", + "yes": "是", + "no": "否", + "productName": "商品名", + "qyt": "數量", + "code": "兌換碼", + "status": "狀態", + "invalidProduct": "已失效商品", + "used": "已使用", + "notUsed": "未使用", + "generatingResult": "生成結果", + "addStoragePack": "新增容量包", + "editStoragePack": "編輯容量包", + "productNameDes": "商品展示名稱", + "packSizeDes": "容量包的大小", + "durationDay": "有效期 (天)", + "durationDayDes": "每個容量包的有效期", + "priceYuan": "單價 (元)", + "packPriceDes": "容量包的單價", + "priceCredits": "單價 (積分)", + "priceCreditsDes": "使用積分購買時的價格,填寫為 0 表示不能使用積分購買", + "editMembership": "編輯可購使用者組", + "addMembership": "新增可購使用者組", + "group": "使用者組", + "groupDes": "購買後升級的使用者組", + "durationGroupDes": "購買後升級的使用者組單位購買時間的有效期", + "groupPriceDes": "使用者組的單價", + "productDescription": "商品描述 (一行一個)", + "productDescriptionDes": "購買頁面展示的商品描述", + "highlightDes": "開啟後,在商品選擇頁面會被突出展示", + "generateGiftCode": "生成兌換碼", + "numberOfCodes": "生成數量", + "numberOfCodesDes": "啟用碼批量生成數量", + "linkedProduct": "對應商品", + "productQyt": "商品數量", + "productQytDes": "對於積分類商品,此處為積分數量,其他商品為時長倍數", + "freeDownload": "免積分下載分享", + "freeDownloadDes": "開啟後,使用者可以免費下載需付積分的分享", + "credits": "積分", + "markSuccessful": "標記成功", + "markAsResolved": "標記為已處理", + "reportedContent": "舉報物件", + "reason": "原因", + "description": "補充描述", + "reportTime": "舉報時間", + "invalid": "[已失效]", + "deleteShare": "刪除分享", + "orderDeleted": "訂單記錄已刪除", + "orderName": "訂單名", + "product": "商品", + "orderNumber": "訂單號", + "paidBy": "支付方式", + "orderOwner": "建立者", + "amount": "金額", + "unpaid": "未支付", + "paid": "已支付", + "shareLink": "分享連結", + "mobileApp": "移動客戶端", + "showAppPromotion": "展示客戶端引導頁面", + "showAppPromotionDes": "開啟後,使用者可以在 “連線與掛載” 頁面中看到移動客戶端的使用引導。", + "customPaymentName": "付款方式名稱", + "customPaymentNameDes": "用於展示給使用者的付款方式名稱", + "customPaymentSecretDes": "Cloudreve 用於籤名付款請求的金鑰。", + "customPaymentEndpoint": "支付介面地址", + "customPaymentEndpointDes": "建立支付訂單時請求的介面 URL", + "appFeedback": "反饋頁面 URL", + "appForum": "使用者論壇 URL", + "appLinkDes": "用於在 App 設定頁面展示,留空即不展示連結按鈕,僅當 VOL 授權有效時此項設定才會生效。" + }, + "pro": { + "title": "Pro 版本專屬功能", + "description": "您嘗試訪問的功能僅在 Cloudreve Pro 版本中可用,升級以解鎖所有高級功能。", + "proInclude": "Pro 版本包含:", + "shareLinkCollabration": "分享連結協同編輯", + "filePermission": "文件權限管理", + "multipleStoragePolicy": "多儲存策略和目錄儲存策略切換", + "auditAndActivity": "文件和系統活動日誌", + "vasService": "增值服務和積分系統", + "sso": "SSO 單點登入", + "more": "......", + "later": "稍後再說", + "learnMore": "了解 Pro 版本詳情" } -} +} \ No newline at end of file diff --git a/public/locales/zh-TW/image_editor.json b/public/locales/zh-TW/image_editor.json new file mode 100644 index 0000000..04b70c4 --- /dev/null +++ b/public/locales/zh-TW/image_editor.json @@ -0,0 +1,113 @@ +{ + "name": "名稱", + "save": "保存", + "saveAs": "另存為", + "back": "返回", + "loading": "加載中...", + "resetOperations": "重置/刪除所有操作", + "changesLoseWarningHint": "如果您按下\"重置\"按鈕,您的更改將丟失。是否要繼續?", + "discardChangesWarningHint": "如果關閉模態,則不會保存最後的更改。", + "cancel": "取消", + "apply": "申請", + "warning": "警告", + "confirm": "確認", + "discardChanges": "放棄更改", + "undoTitle": "撤消上次操作", + "redoTitle": "重做上次操作", + "showImageTitle": "顯示原始圖像", + "zoomInTitle": "放大", + "zoomOutTitle": "縮小", + "toggleZoomMenuTitle": "切換縮放菜單", + "adjustTab": "調整", + "finetuneTab": "微調", + "filtersTab": "濾鏡", + "watermarkTab": "水印", + "annotateTabLabel": "繪圖", + "resize": "調整大小", + "resizeTab": "調整大小", + "imageName": "圖像名稱", + "invalidImageError": "提供的圖像無效。", + "uploadImageError": "上傳圖片時出錯。", + "areNotImages": "冇有圖像", + "isNotImage": "不是一個圖像", + "toBeUploaded": "待上傳", + "cropTool": "裁剪", + "original": "原始", + "custom": "自定義", + "square": "正方形", + "landscape": "風景", + "portrait": "肖像", + "ellipse": "人像", + "classicTv": "經典電視", + "cinemascope": "電影比例", + "arrowTool": "箭頭", + "blurTool": "模糊", + "brightnessTool": "亮度", + "contrastTool": "對比", + "ellipseTool": "橢圓", + "unFlipX": "取消橫嚮翻轉", + "flipX": "橫嚮翻轉", + "unFlipY": "取消縱嚮翻轉", + "flipY": "縱嚮翻轉", + "hsvTool": "色相/飽和度", + "hue": "色相", + "brightness": "亮度", + "saturation": "飽和度", + "value": "值", + "imageTool": "圖像", + "importing": "正在導入...", + "addImage": "+ 添加圖片", + "uploadImage": "上傳圖片", + "fromGallery": "從圖庫", + "lineTool": "線條", + "penTool": "畫筆", + "polygonTool": "多邊形", + "sides": "側麵", + "rectangleTool": "矩形", + "cornerRadius": "拐角半徑", + "resizeWidthTitle": "寬度(以像素為單位)", + "resizeHeightTitle": "高度(以像素為單位)", + "toggleRatioLockTitle": "鎖定比例", + "resetSize": "重置為原始圖像大小", + "rotateTool": "旋轉", + "textTool": "文本", + "textSpacings": "文本間距", + "textAlignment": "文本對齊", + "fontFamily": "字體家族", + "size": "尺寸", + "letterSpacing": "字母間距", + "lineHeight": "行高", + "warmthTool": "溫暖", + "addWatermark": "添加水印", + "addTextWatermark": "添加文本水印", + "addWatermarkTitle": "選擇水印類型", + "uploadWatermark": "上傳水印", + "addWatermarkAsText": "添加為文本", + "padding": "填充", + "paddings": "填充物", + "shadow": "影子", + "horizontal": "水平", + "vertical": "垂直", + "blur": "模糊", + "opacity": "不透明度", + "transparency": "透明度", + "position": "位置", + "stroke": "中風", + "saveAsModalTitle": "另存為", + "extension": "擴展", + "format": "格式", + "nameIsRequired": "名稱為必填項。", + "quality": "質量", + "imageDimensionsHoverTitle": "保存的圖像大小(寬 x 高)", + "cropSizeLowerThanResizedWarning": "請註意,所選裁剪麵積小於應用的調整大小,這可能會導緻質量下降", + "actualSize": "實際大小 (100%)", + "fitSize": "適合尺寸", + "addImageTitle": "選擇要添加的圖片...", + "mutualizedFailedToLoadImg": "無法加載圖像。", + "tabsMenu": "菜單", + "download": "下載", + "width": "寬度", + "height": "高度", + "plus": "+", + "cropItemNoEffect": "此裁剪項冇有可用的預覽" +} \ No newline at end of file diff --git a/public/locales/zh-TW/markdown_editor.json b/public/locales/zh-TW/markdown_editor.json new file mode 100644 index 0000000..8e6b1d5 --- /dev/null +++ b/public/locales/zh-TW/markdown_editor.json @@ -0,0 +1,107 @@ +{ + "frontmatterEditor": { + "title": "編輯中繼資料", + "key": "鍵", + "value": "值", + "addEntry": "新增項目" + }, + "dialogControls": { + "save": "儲存", + "cancel": "取消" + }, + "uploadImage": { + "dialogTitle": "上傳圖片", + "uploadInstructions": "從您的裝置上傳圖片:", + "addViaUrlInstructions": "或從網址新增圖片:", + "autoCompletePlaceholder": "選擇或貼上圖片網址", + "alt": "替代文字:", + "title": "標題:" + }, + "imageEditor": { + "deleteImage": "刪除圖片", + "editImage": "編輯圖片" + }, + "createLink": { + "url": "網址", + "urlPlaceholder": "選擇或貼上網址", + "title": "標題", + "saveTooltip": "設定網址", + "cancelTooltip": "取消修改" + }, + "linkPreview": { + "open": "在新視窗中開啟 {{url}}", + "edit": "編輯連結網址", + "copyToClipboard": "複製到剪貼簿", + "copied": "已複製!", + "remove": "移除連結" + }, + "table": { + "deleteTable": "刪除表格", + "columnMenu": "欄選單", + "textAlignment": "文字對齊", + "alignLeft": "靠左對齊", + "alignCenter": "置中對齊", + "alignRight": "靠右對齊", + "insertColumnLeft": "在此左側插入一欄", + "insertColumnRight": "在此右側插入一欄", + "deleteColumn": "刪除此欄", + "rowMenu": "列選單", + "insertRowAbove": "在上方插入一列", + "insertRowBelow": "在下方插入一列", + "deleteRow": "刪除此列" + }, + "toolbar": { + "blockTypes": { + "paragraph": "段落", + "quote": "引用", + "heading": "標題 {{level}}" + }, + "blockTypeSelect": { + "selectBlockTypeTooltip": "選擇塊類型", + "placeholder": "塊類型" + }, + "toggleGroup": "切換群組", + "removeBold": "移除粗體", + "bold": "粗體", + "removeItalic": "移除斜體", + "italic": "斜體", + "underline": "移除底線", + "removeUnderline": "底線", + "removeInlineCode": "移除程式碼格式", + "inlineCode": "內聯程式碼格式", + "link": "建立連結", + "richText": "富文字", + "diffMode": "差異模式", + "source": "原始碼模式", + "admonition": "插入註解區塊", + "codeBlock": "插入程式碼區塊", + "editFrontmatter": "編輯中繼資料", + "insertFrontmatter": "插入中繼資料", + "image": "插入圖片", + "insertSandpack": "插入 Sandpack", + "table": "插入表格", + "thematicBreak": "插入主題斷行", + "bulletedList": "項目清單", + "numberedList": "編號清單", + "checkList": "核取清單", + "deleteSandpack": "刪除 Sandpack", + "undo": "復原 {{shortcut}}", + "redo": "重做 {{shortcut}}" + }, + "admonitions": { + "note": "注意", + "tip": "提示", + "danger": "危險", + "info": "資訊", + "caution": "警告", + "changeType": "選擇註解區塊類型", + "placeholder": "註解區塊類型" + }, + "codeBlock": { + "language": "程式碼區塊語言", + "selectLanguage": "選擇程式碼區塊語言" + }, + "contentArea": { + "editableMarkdown": "可編輯的 Markdown" + } +} \ No newline at end of file diff --git a/public/pdfviewer.html b/public/pdfviewer.html new file mode 100644 index 0000000..6a7ff9d --- /dev/null +++ b/public/pdfviewer.html @@ -0,0 +1,752 @@ + + + + + + + + + PDF.js viewer + + + + + + + + + + + + +
+ +
+
+
+
+ + + + +
+
+ +
+
+
+ + +
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+ +
+ +
+
+ + + + +
+
+
+
+ +
+ +
+ + + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + + +
+ +
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+ +
+ +
+
+ +
+
+ + +
+
+ +
+ File name: +

-

+
+
+ File size: +

-

+
+
+
+ Title: +

-

+
+
+ Author: +

-

+
+
+ Subject: +

-

+
+
+ Keywords: +

-

+
+
+ Creation Date: +

-

+
+
+ Modification Date: +

-

+
+
+ Creator: +

-

+
+
+
+ PDF Producer: +

-

+
+
+ PDF Version: +

-

+
+
+ Page Count: +

-

+
+
+ Page Size: +

-

+
+
+
+ Fast Web View: +

-

+
+
+ +
+
+ +
+
+ Choose an option + + Alt text (alternative text) helps when people can’t see the image or when it doesn’t load. + +
+
+
+
+ + +
+
+ + Aim for 1-2 sentences that describe the subject, setting, or actions. + +
+
+
+ +
+
+
+
+
+ + +
+
+ + This is used for ornamental images, like borders or watermarks. + +
+
+
+
+ + +
+
+
+ +
+
+ Edit alt text (image description) +
+
+
+
+
+
+ +
+ Short description for people who can’t see the image or when the image doesn’t load. +
This alt text was created automatically and may be inaccurate. Learn more
+
+
+ + +
+ +
+
+
+
+
+
+ Couldn’t create alt text automatically + Please write your own alt text or try again later. +
+ +
+
+
+ + + +
+
+
+ + +
+
+ Image alt text settings +
+
+ Automatic alt text +
+
+
+ + +
+
+ Suggests descriptions to help people who can’t see the image or when the image doesn’t load. Learn more +
+
+
+
+ Alt text AI model (180MB) +
+ Runs locally on your device so your data stays private. Required for automatic alt text. +
+
+ + +
+
+
+
+
+ Alt text editor +
+
+ + +
+
+ Helps you make sure all your images have alt text. +
+
+
+
+ +
+
+
+ + + + This modal allows the user to create a signature to add to a PDF document. The user can edit the name (which also serves as the alt text), and optionally save the signature for repeated use. + +
+
+ Add a signature +
+
+ + + +
+
+
+ +
+
+ + Draw your signature +
+
+ + +
+
+
+
+ +
+ Drag a file here to upload + + +
+
+
+
+
+ + + + + +
+ +
+
+ + + + You’ve reached the limit of 5 saved signatures. Remove one to save more. +
+
+ +
+ + +
+
+
+
+ + +
+
+ Edit description +
+
+
+ + + + + +
+ +
+
+ + +
+
+
+ + +
+ Preparing document for printing… +
+
+ + 0% +
+
+ +
+
+
+ + + +
+
+ + diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..a3833b4 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,14 @@ +self.addEventListener("install", function (e) { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (e) { + self.registration + .unregister() + .then(function () { + return self.clients.matchAll(); + }) + .then(function (clients) { + clients.forEach((client) => client.navigate(client.url)); + }); +}); diff --git a/public/static/img/logo.svg b/public/static/img/logo.svg new file mode 100644 index 0000000..de7c93d --- /dev/null +++ b/public/static/img/logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/img/logo_light.svg b/public/static/img/logo_light.svg new file mode 100644 index 0000000..7bdb432 --- /dev/null +++ b/public/static/img/logo_light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/img/obs.png b/public/static/img/obs.png new file mode 100644 index 0000000..57fd7d4 Binary files /dev/null and b/public/static/img/obs.png differ diff --git a/public/static/img/viewers/artplayer.png b/public/static/img/viewers/artplayer.png new file mode 100644 index 0000000..1d9d15e Binary files /dev/null and b/public/static/img/viewers/artplayer.png differ diff --git a/public/static/img/viewers/drawio.svg b/public/static/img/viewers/drawio.svg new file mode 100644 index 0000000..454a30d --- /dev/null +++ b/public/static/img/viewers/drawio.svg @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/public/static/img/viewers/gdrive.png b/public/static/img/viewers/gdrive.png new file mode 100644 index 0000000..2b64d3c Binary files /dev/null and b/public/static/img/viewers/gdrive.png differ diff --git a/public/static/img/viewers/m365.svg b/public/static/img/viewers/m365.svg new file mode 100644 index 0000000..aa6e0f0 --- /dev/null +++ b/public/static/img/viewers/m365.svg @@ -0,0 +1,46 @@ + + +Microsoft 365 logo (2022) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/img/viewers/monaco.svg b/public/static/img/viewers/monaco.svg new file mode 100644 index 0000000..cc61f81 --- /dev/null +++ b/public/static/img/viewers/monaco.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/static/img/viewers/photopea.png b/public/static/img/viewers/photopea.png new file mode 100644 index 0000000..d5b0afe Binary files /dev/null and b/public/static/img/viewers/photopea.png differ diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 58c5447..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict'; - -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = 'production'; -process.env.NODE_ENV = 'production'; - -// Makes the script crash on unhandled rejections instead of silently -// ignoring them. In the future, promise rejections that are not handled will -// terminate the Node.js process with a non-zero exit code. -process.on('unhandledRejection', err => { - throw err; -}); - -// Ensure environment variables are read. -require('../config/env'); - - -const path = require('path'); -const chalk = require('react-dev-utils/chalk'); -const fs = require('fs-extra'); -const webpack = require('webpack'); -const configFactory = require('../config/webpack.config'); -const paths = require('../config/paths'); -const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); -const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); -const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); -const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); -const printBuildError = require('react-dev-utils/printBuildError'); - -const measureFileSizesBeforeBuild = - FileSizeReporter.measureFileSizesBeforeBuild; -const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; -const useYarn = fs.existsSync(paths.yarnLockFile); - -// These sizes are pretty large. We'll warn for bundles exceeding them. -const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; -const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; - -const isInteractive = process.stdout.isTTY; - -// Warn and crash if required files are missing -if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { - process.exit(1); -} - -// Generate configuration -const config = configFactory('production'); - -// We require that you explicitly set browsers and do not fall back to -// browserslist defaults. -const { checkBrowsers } = require('react-dev-utils/browsersHelper'); -checkBrowsers(paths.appPath, isInteractive) - .then(() => { - // First, read the current file sizes in build directory. - // This lets us display how much they changed later. - return measureFileSizesBeforeBuild(paths.appBuild); - }) - .then(previousFileSizes => { - // Remove all content but keep the directory so that - // if you're in it, you don't end up in Trash - fs.emptyDirSync(paths.appBuild); - // Merge with the public folder - copyPublicFolder(); - // Start the webpack build - return build(previousFileSizes); - }) - .then( - ({ stats, previousFileSizes, warnings }) => { - if (warnings.length) { - console.log(chalk.yellow('Compiled with warnings.\n')); - console.log(warnings.join('\n\n')); - console.log( - '\nSearch for the ' + - chalk.underline(chalk.yellow('keywords')) + - ' to learn more about each warning.' - ); - console.log( - 'To ignore, add ' + - chalk.cyan('// eslint-disable-next-line') + - ' to the line before.\n' - ); - } else { - console.log(chalk.green('Compiled successfully.\n')); - } - - console.log('File sizes after gzip:\n'); - printFileSizesAfterBuild( - stats, - previousFileSizes, - paths.appBuild, - WARN_AFTER_BUNDLE_GZIP_SIZE, - WARN_AFTER_CHUNK_GZIP_SIZE - ); - console.log(); - - const appPackage = require(paths.appPackageJson); - const publicUrl = paths.publicUrl; - const publicPath = config.output.publicPath; - const buildFolder = path.relative(process.cwd(), paths.appBuild); - printHostingInstructions( - appPackage, - publicUrl, - publicPath, - buildFolder, - useYarn - ); - }, - err => { - const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; - if (tscCompileOnError) { - console.log(chalk.yellow( - 'Compiled with the following type errors (you may want to check these before deploying your app):\n' - )); - printBuildError(err); - } else { - console.log(chalk.red('Failed to compile.\n')); - printBuildError(err); - process.exit(1); - } - } - ) - .catch(err => { - if (err && err.message) { - console.log(err.message); - } - process.exit(1); - }); - -// Create the production build and print the deployment instructions. -function build(previousFileSizes) { - // We used to support resolving modules according to `NODE_PATH`. - // This now has been deprecated in favor of jsconfig/tsconfig.json - // This lets you use absolute paths in imports inside large monorepos: - if (process.env.NODE_PATH) { - console.log( - chalk.yellow( - 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' - ) - ); - console.log(); - } - - console.log('Creating an optimized production build...'); - - const compiler = webpack(config); - return new Promise((resolve, reject) => { - compiler.run((err, stats) => { - let messages; - if (err) { - if (!err.message) { - return reject(err); - } - messages = formatWebpackMessages({ - errors: [err.message], - warnings: [], - }); - } else { - messages = formatWebpackMessages( - stats.toJson({ all: false, warnings: true, errors: true }) - ); - } - if (messages.errors.length) { - // Only keep the first error. Others are often indicative - // of the same problem, but confuse the reader with noise. - if (messages.errors.length > 1) { - messages.errors.length = 1; - } - return reject(new Error(messages.errors.join('\n\n'))); - } - if ( - process.env.CI && - (typeof process.env.CI !== 'string' || - process.env.CI.toLowerCase() !== 'false') && - messages.warnings.length - ) { - console.log( - chalk.yellow( - '\nTreating warnings as errors because process.env.CI = true.\n' + - 'Most CI servers set it automatically.\n' - ) - ); - return reject(new Error(messages.warnings.join('\n\n'))); - } - - return resolve({ - stats, - previousFileSizes, - warnings: messages.warnings, - }); - }); - }); -} - -function copyPublicFolder() { - fs.copySync(paths.appPublic, paths.appBuild, { - dereference: true, - filter: file => file !== paths.appHtml, - }); -} diff --git a/scripts/start.js b/scripts/start.js deleted file mode 100644 index dd89084..0000000 --- a/scripts/start.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = 'development'; -process.env.NODE_ENV = 'development'; - -// Makes the script crash on unhandled rejections instead of silently -// ignoring them. In the future, promise rejections that are not handled will -// terminate the Node.js process with a non-zero exit code. -process.on('unhandledRejection', err => { - throw err; -}); - -// Ensure environment variables are read. -require('../config/env'); - - -const fs = require('fs'); -const chalk = require('react-dev-utils/chalk'); -const webpack = require('webpack'); -const WebpackDevServer = require('webpack-dev-server'); -const clearConsole = require('react-dev-utils/clearConsole'); -const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); -const { - choosePort, - createCompiler, - prepareProxy, - prepareUrls, -} = require('react-dev-utils/WebpackDevServerUtils'); -const openBrowser = require('react-dev-utils/openBrowser'); -const paths = require('../config/paths'); -const configFactory = require('../config/webpack.config'); -const createDevServerConfig = require('../config/webpackDevServer.config'); - -const useYarn = fs.existsSync(paths.yarnLockFile); -const isInteractive = process.stdout.isTTY; - -// Warn and crash if required files are missing -if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { - process.exit(1); -} - -// Tools like Cloud9 rely on this. -const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; -const HOST = process.env.HOST || '0.0.0.0'; - -if (process.env.HOST) { - console.log( - chalk.cyan( - `Attempting to bind to HOST environment variable: ${chalk.yellow( - chalk.bold(process.env.HOST) - )}` - ) - ); - console.log( - `If this was unintentional, check that you haven't mistakenly set it in your shell.` - ); - console.log( - `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` - ); - console.log(); -} - -// We require that you explicitly set browsers and do not fall back to -// browserslist defaults. -const { checkBrowsers } = require('react-dev-utils/browsersHelper'); -checkBrowsers(paths.appPath, isInteractive) - .then(() => { - // We attempt to use the default port but if it is busy, we offer the user to - // run on a different port. `choosePort()` Promise resolves to the next free port. - return choosePort(HOST, DEFAULT_PORT); - }) - .then(port => { - if (port == null) { - // We have not found a port. - return; - } - const config = configFactory('development'); - const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; - const appName = require(paths.appPackageJson).name; - const useTypeScript = fs.existsSync(paths.appTsConfig); - const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; - const urls = prepareUrls(protocol, HOST, port); - const devSocket = { - warnings: warnings => - devServer.sockWrite(devServer.sockets, 'warnings', warnings), - errors: errors => - devServer.sockWrite(devServer.sockets, 'errors', errors), - }; - // Create a webpack compiler that is configured with custom messages. - const compiler = createCompiler({ - appName, - config, - devSocket, - urls, - useYarn, - useTypeScript, - tscCompileOnError, - webpack, - }); - // Load proxy config - const proxySetting = require(paths.appPackageJson).proxy; - const proxyConfig = prepareProxy(proxySetting, paths.appPublic); - // Serve webpack assets generated by the compiler over a web server. - const serverConfig = createDevServerConfig( - proxyConfig, - urls.lanUrlForConfig - ); - const devServer = new WebpackDevServer(compiler, serverConfig); - // Launch WebpackDevServer. - devServer.listen(port, HOST, err => { - if (err) { - return console.log(err); - } - if (isInteractive) { - clearConsole(); - } - - // We used to support resolving modules according to `NODE_PATH`. - // This now has been deprecated in favor of jsconfig/tsconfig.json - // This lets you use absolute paths in imports inside large monorepos: - if (process.env.NODE_PATH) { - console.log( - chalk.yellow( - 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' - ) - ); - console.log(); - } - - console.log(chalk.cyan('Starting the development server...\n')); - openBrowser(urls.localUrlForBrowser); - }); - - ['SIGINT', 'SIGTERM'].forEach(function(sig) { - process.on(sig, function() { - devServer.close(); - process.exit(); - }); - }); - }) - .catch(err => { - if (err && err.message) { - console.log(err.message); - } - process.exit(1); - }); diff --git a/scripts/test.js b/scripts/test.js deleted file mode 100644 index b57cb38..0000000 --- a/scripts/test.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = 'test'; -process.env.NODE_ENV = 'test'; -process.env.PUBLIC_URL = ''; - -// Makes the script crash on unhandled rejections instead of silently -// ignoring them. In the future, promise rejections that are not handled will -// terminate the Node.js process with a non-zero exit code. -process.on('unhandledRejection', err => { - throw err; -}); - -// Ensure environment variables are read. -require('../config/env'); - - -const jest = require('jest'); -const execSync = require('child_process').execSync; -let argv = process.argv.slice(2); - -function isInGitRepository() { - try { - execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -function isInMercurialRepository() { - try { - execSync('hg --cwd . root', { stdio: 'ignore' }); - return true; - } catch (e) { - return false; - } -} - -// Watch unless on CI or explicitly running all tests -if ( - !process.env.CI && - argv.indexOf('--watchAll') === -1 && - argv.indexOf('--watchAll=false') === -1 -) { - // https://github.com/facebook/create-react-app/issues/5210 - const hasSourceControl = isInGitRepository() || isInMercurialRepository(); - argv.push(hasSourceControl ? '--watch' : '--watchAll'); -} - - -jest.run(argv); diff --git a/src/Admin.js b/src/Admin.js deleted file mode 100644 index c11666a..0000000 --- a/src/Admin.js +++ /dev/null @@ -1,219 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { CssBaseline, makeStyles } from "@material-ui/core"; -import AlertBar from "./component/Common/Snackbar"; -import Dashboard from "./component/Admin/Dashboard"; -import { useHistory } from "react-router"; -import Auth from "./middleware/Auth"; -import { Route, Switch } from "react-router-dom"; -import { ThemeProvider } from "@material-ui/styles"; -import createTheme from "@material-ui/core/styles/createMuiTheme"; -import { zhCN } from "@material-ui/core/locale"; - -import Index from "./component/Admin/Index"; -import SiteInformation from "./component/Admin/Setting/SiteInformation"; -import Access from "./component/Admin/Setting/Access"; -import Mail from "./component/Admin/Setting/Mail"; -import UploadDownload from "./component/Admin/Setting/UploadDownload"; -import Theme from "./component/Admin/Setting/Theme"; -import ImageSetting from "./component/Admin/Setting/Image"; -import Policy from "./component/Admin/Policy/Policy"; -import AddPolicy from "./component/Admin/Policy/AddPolicy"; -import EditPolicyPreload from "./component/Admin/Policy/EditPolicy"; -import Group from "./component/Admin/Group/Group"; -import GroupForm from "./component/Admin/Group/GroupForm"; -import EditGroupPreload from "./component/Admin/Group/EditGroup"; -import User from "./component/Admin/User/User"; -import UserForm from "./component/Admin/User/UserForm"; -import EditUserPreload from "./component/Admin/User/EditUser"; -import File from "./component/Admin/File/File"; -import Share from "./component/Admin/Share/Share"; -import Download from "./component/Admin/Task/Download"; -import Task from "./component/Admin/Task/Task"; -import Import from "./component/Admin/File/Import"; -import Captcha from "./component/Admin/Setting/Captcha"; -import Node from "./component/Admin/Node/Node"; -import AddNode from "./component/Admin/Node/AddNode"; -import EditNode from "./component/Admin/Node/EditNode"; - -const useStyles = makeStyles((theme) => ({ - root: { - display: "flex", - }, - content: { - flexGrow: 1, - padding: 0, - minWidth: 0, - }, - toolbar: theme.mixins.toolbar, -})); - -const theme = createTheme( - { - palette: { - background: {}, - }, - shape:{ - borderRadius:12, - }, - overrides: { - MuiButton: { - root: { - textTransform: "none", - }, - }, - MuiTab: { - root: { - textTransform: "none", - }, - }, - }, - }, - zhCN -); - -export default function Admin() { - const classes = useStyles(); - const history = useHistory(); - const [show, setShow] = useState(false); - - useEffect(() => { - const user = Auth.GetUser(); - if (user && user.group) { - if (user.group.id !== 1) { - history.push("/home"); - return; - } - setShow(true); - return; - } - history.push("/login"); - // eslint-disable-next-line - }, []); - - return ( - - -
- - - {show && ( - ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - /> - )} -
-
-
- ); -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index d8bb657..0000000 --- a/src/App.js +++ /dev/null @@ -1,255 +0,0 @@ -import React, { Suspense } from "react"; -import AuthRoute from "./middleware/AuthRoute"; -import NoAuthRoute from "./middleware/NoAuthRoute"; -import Navbar from "./component/Navbar/Navbar.js"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import AlertBar from "./component/Common/Snackbar"; -import { createMuiTheme, lighten } from "@material-ui/core/styles"; -import { useSelector } from "react-redux"; -import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom"; -import Auth from "./middleware/Auth"; -import { CssBaseline, makeStyles, ThemeProvider } from "@material-ui/core"; -import { changeThemeColor } from "./utils"; -import NotFound from "./component/Share/NotFound"; -// Lazy loads -import LoginForm from "./component/Login/LoginForm"; -import FileManager from "./component/FileManager/FileManager.js"; -import VideoPreview from "./component/Viewer/Video.js"; -import SearchResult from "./component/Share/SearchResult"; -import MyShare from "./component/Share/MyShare"; -import Download from "./component/Download/Download"; -import SharePreload from "./component/Share/SharePreload"; -import DocViewer from "./component/Viewer/Doc"; -import TextViewer from "./component/Viewer/Text"; -import WebDAV from "./component/Setting/WebDAV"; -import Tasks from "./component/Setting/Tasks"; -import Profile from "./component/Setting/Profile"; -import UserSetting from "./component/Setting/UserSetting"; -import Register from "./component/Login/Register"; -import Activation from "./component/Login/Activication"; -import ResetForm from "./component/Login/ResetForm"; -import Reset from "./component/Login/Reset"; -import PageLoading from "./component/Placeholder/PageLoading"; -import CodeViewer from "./component/Viewer/Code"; -import MusicPlayer from "./component/FileManager/MusicPlayer"; -import EpubViewer from "./component/Viewer/Epub"; -import { useTranslation } from "react-i18next"; - -const PDFViewer = React.lazy(() => - import(/* webpackChunkName: "pdf" */ "./component/Viewer/PDF") -); - -export default function App() { - const themeConfig = useSelector((state) => state.siteConfig.theme); - const isLogin = useSelector((state) => state.viewUpdate.isLogin); - const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); - const { t } = useTranslation(); - - const theme = React.useMemo(() => { - themeConfig.palette.type = prefersDarkMode ? "dark" : "light"; - const prefer = Auth.GetPreference("theme_mode"); - if (prefer) { - themeConfig.palette.type = prefer; - } - const theme = createMuiTheme({ - ...themeConfig, - palette: { - ...themeConfig.palette, - primary: { - ...themeConfig.palette.primary, - main: - themeConfig.palette.type === "dark" - ? lighten(themeConfig.palette.primary.main, 0.3) - : themeConfig.palette.primary.main, - }, - }, - shape: { - ...themeConfig.shape, - borderRadius: 12, - }, - overrides: { - MuiButton: { - root: { - textTransform: "none", - }, - }, - MuiTab: { - root: { - textTransform: "none", - }, - }, - }, - }); - changeThemeColor( - themeConfig.palette.type === "dark" - ? theme.palette.background.default - : theme.palette.primary.main - ); - return theme; - }, [prefersDarkMode, themeConfig]); - - const useStyles = makeStyles((theme) => ({ - root: { - display: "flex", - }, - content: { - flexGrow: 1, - padding: theme.spacing(0), - minWidth: 0, - }, - toolbar: theme.mixins.toolbar, - })); - - const classes = useStyles(); - - const { path } = useRouteMatch(); - return ( - - -
- - - -
-
- - - - - - - - - - - - - - - - - - - - - - - }> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }> - - - - - - - - - - - - - - - - -
- -
-
-
- ); -} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..fe636ca --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,352 @@ +import { createTheme, CssBaseline, GlobalStyles, styled, ThemeProvider, useMediaQuery, useTheme } from "@mui/material"; +import { grey } from "@mui/material/colors"; +import { ThemeOptions } from "@mui/material/styles/createTheme"; +import i18next from "i18next"; +import { MaterialDesignContent, SnackbarProvider } from "notistack"; +import { Suspense, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Outlet } from "react-router-dom"; +import { useRegisterSW } from "virtual:pwa-register/react"; +import LoadingSnackbar from "./component/Common/Snackbar/LoadingSnackbar.tsx"; +import GlobalDialogs from "./component/Dialogs/GlobalDialogs.tsx"; +import { GrowDialogTransition } from "./component/FileManager/Search/SearchPopup.tsx"; +import Warning from "./component/Icons/Warning.tsx"; +import { useAppSelector } from "./redux/hooks.ts"; +import { changeThemeColor } from "./util"; + +export const applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions => { + return { + ...themeConfig, + shape: { + ...themeConfig.shape, + borderRadius: 12, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + overscrollBehavior: "none", + }, + }, + }, + MuiTooltip: { + defaultProps: { + enterDelay: 500, + }, + }, + MuiToggleButton: { + styleOverrides: { + root: { + textTransform: "none", + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + textTransform: "none", + }, + }, + defaultProps: { + disableElevation: true, + }, + }, + MuiAlert: { + defaultProps: { + iconMapping: { + warning: , + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + borderRadius: 12, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: "none", + }, + }, + }, + MuiSkeleton: { + defaultProps: { + animation: "wave", + }, + }, + MuiMenu: { + styleOverrides: { + paper: { + borderRadius: "8px", + }, + list: { + padding: "4px 0", + }, + }, + defaultProps: { + slotProps: { + paper: { + elevation: 3, + }, + }, + }, + }, + MuiDialogContent: { + styleOverrides: { + root: { + paddingTop: 0, + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: "8px", + margin: "0px 4px", + paddingLeft: "8px", + paddingRight: "8px", + }, + }, + }, + MuiDialog: { + defaultProps: { + TransitionComponent: GrowDialogTransition, + }, + }, + MuiFilledInput: { + styleOverrides: { + root: { + "&::before, &::after": { + borderBottom: "none", + }, + "&:hover:not(.Mui-disabled, .Mui-error):before": { + borderBottom: "none", + }, + borderRadius: 12, + // '&:hover:not(.Mui-disabled, .Mui-error):before': { + // borderBottom: '2px solid var(--TextField-brandBorderHoverColor)', + // }, + // '&.Mui-focused:after': { + // borderBottom: '2px solid var(--TextField-brandBorderFocusedColor)', + // }, + }, + }, + }, + }, + }; +}; + +export const useGeneratedTheme = (preferedDark?: boolean, subTheme?: boolean) => { + const themes = useAppSelector((state) => state.siteConfig.basic.config.themes); + const defaultTheme = useAppSelector((state) => state.siteConfig.basic.config.default_theme); + const preferredTheme = useAppSelector((state) => state.globalState.preferredTheme); + let darkMode = useAppSelector((state) => state.globalState.darkMode); + darkMode = darkMode; + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const mode = + preferedDark !== undefined + ? preferedDark + ? "dark" + : "light" + : darkMode === undefined + ? prefersDarkMode + ? "dark" + : "light" + : darkMode + ? "dark" + : "light"; + const theme = useMemo(() => { + // Determine preferred theme + var themeConfig = {} as ThemeOptions; + if (themes) { + try { + const themeOptions = JSON.parse(themes) as themeOptions; + themeConfig = getPreferredTheme(themeOptions, mode, preferredTheme, defaultTheme); + } catch (e) { + console.log("failed to parse theme config, using default", e); + } + } + + themeConfig = { + ...themeConfig, + palette: { + ...themeConfig.palette, + mode: mode, + }, + }; + + const t = createTheme(applyThemeWithOverrides(themeConfig)); + if (!subTheme) { + changeThemeColor(themeConfig?.palette?.mode === "light" ? t.palette.grey[100] : t.palette.grey[900]); + } + return t; + }, [prefersDarkMode, preferredTheme, defaultTheme, themes, darkMode]); + + return theme; +}; + +const removeI18nCache = () => { + Object.keys(localStorage).forEach(function (key) { + if (key && key.startsWith("i18next_res_")) { + localStorage.removeItem(key); + } + }); +}; + +export const App = () => { + const theme = useGeneratedTheme(); + const { t } = useTranslation(); + + const { + offlineReady: [offlineReady, setOfflineReady], + needRefresh: [needRefresh, setNeedRefresh], + updateServiceWorker, + } = useRegisterSW({ + onRegisteredSW(swUrl, r) {}, + onRegisterError(error) { + console.log("SW registration error", error); + }, + }); + + useEffect(() => { + if (needRefresh) { + alert(i18next.t("newVersionRefresh", { ns: "common" })); + removeI18nCache(); + updateServiceWorker(true); + } + }, [needRefresh]); + + return ( + Loading...}> + + + + + ); +}; + +interface themeOptions { + [key: string]: singleThemeOption; +} + +interface singleThemeOption { + light: ThemeOptions; + dark?: ThemeOptions; +} + +const getPreferredTheme = ( + opts: themeOptions, + mode: "dark" | "light", + preferredTheme?: string, + defaultTheme?: string, +): ThemeOptions => { + let themeConfig = {} as singleThemeOption; + if (defaultTheme && opts[defaultTheme]) { + themeConfig = opts[defaultTheme]; + } + if (preferredTheme && opts[preferredTheme]) { + themeConfig = opts[preferredTheme]; + } + + if (!themeConfig?.light) { + themeConfig = Object.values(opts)[0]; + } + + if (mode === "dark" && themeConfig.dark) { + return themeConfig.dark; + } + + return themeConfig.light; +}; + +const StyledMaterialDesignContent = styled(MaterialDesignContent)(({ theme }) => ({ + "&.notistack-MuiContent": { + borderRadius: 12, + }, + "&.notistack-MuiContent-success": { + backgroundColor: theme.palette.success.main, + }, + "&.notistack-MuiContent-error": { + backgroundColor: theme.palette.error.main, + }, + "&.notistack-MuiContent-warning": { + backgroundColor: theme.palette.warning.main, + }, +})); + +const AppContent = () => { + const title = useAppSelector((state) => state.siteConfig.basic.config.title); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const scrollBar = { + "&::-webkit-scrollbar-button": { + width: 0, + height: 0, + }, + "&::-webkit-scrollbar-corner": { + background: "0 0", + }, + "&::-webkit-scrollbar-thumb": { + borderRadius: 4, + backgroundColor: "transparent", + }, + "&::-webkit-scrollbar-track": { + borderRadius: 4, + }, + "&::-webkit-scrollbar-track:hover": { + backgroundColor: theme.palette.mode == "light" ? grey[200] : grey[800], + }, + "&::-webkit-scrollbar-thumb:hover": { + backgroundColor: theme.palette.primary.main + "!important", + }, + "& :hover::-webkit-scrollbar-thumb,:hover>:first-child::-webkit-scrollbar-thumb": { + backgroundColor: theme.palette.mode == "light" ? grey[400] : grey[600], + }, + "&::-webkit-scrollbar ": { + width: 8, + height: 8, + }, + }; + + return ( + <> + + ({ + html: { + scrollbarWidth: isMobile ? "initial" : "thin", + //scrollbarColor: theme.palette.action.selected + " transparent", + }, + ...(isMobile ? undefined : scrollBar), + body: { + overflowY: isMobile ? "initial" : "hidden", + }, + ".highlight-marker": { + backgroundColor: "#ffc1079e", + borderRadius: "4px", + boxShadow: "0 0 0 2px #ffc1079e", + }, + })} + /> + + + + + + ); +}; diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..d3f222a --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,1952 @@ +import { AxiosProgressEvent, CancelToken } from "axios"; +import i18n from "../i18n.ts"; +import { + AdminListGroupResponse, + AdminListService, + BatchIDService, + CreateStoragePolicyCorsService, + Entity, + FetchWOPIDiscoveryService, + File as FileEnt, + FinishOauthCallbackService, + GetOauthRedirectService, + GetSettingService, + GroupEnt, + HomepageSummary, + ListEntityResponse, + ListFileResponse, + ListNodeResponse, + ListShareResponse as AdminListShareResponse, + ListStoragePolicyResponse, + ListTaskResponse, + ListUserResponse, + Node, + OauthCredentialStatus, + QueueMetric, + SetSettingService, + Share as ShareEnt, + StoragePolicy as AdminStoragePolicy, + Task, + TestNodeDownloaderService, + TestNodeService, + TestSMTPService, + ThumbGeneratorTestService, + UpsertFileService, + UpsertGroupService, + UpsertNodeService, + UpsertStoragePolicyService, + UpsertUserService, + User as UserEnt, +} from "./dashboard.ts"; +import { + CreateFileService, + CreateViewerSessionService, + DeleteFileService, + DeleteUploadSessionService, + DirectLink, + FileResponse, + FileThumbResponse, + FileUpdateService, + FileURLResponse, + FileURLService, + GetFileInfoService, + ListFileService, + ListResponse, + MoveFileService, + MultipleUriService, + PatchMetadataService, + PinFileService, + RenameFileService, + Share, + ShareCreateService, + UnlockFileService, + UploadCredential, + UploadSessionRequest, + VersionControlService, + ViewerGroup, + ViewerSessionResponse, +} from "./explorer.ts"; +import { AppError, Code, CrHeaders, defaultOpts, send, ThunkResponse } from "./request.ts"; +import { CreateDavAccountService, DavAccount, ListDavAccountsResponse, ListDavAccountsService } from "./setting.ts"; +import { ListShareResponse, ListShareService } from "./share.ts"; +import { CaptchaResponse, SiteConfig } from "./site.ts"; +import { + Capacity, + FinishPasskeyLoginService, + FinishPasskeyRegistrationService, + Group, + LoginResponse, + Passkey, + PasskeyCredentialOption, + PasswordLoginRequest, + PatchUserSetting, + PrepareLoginResponse, + PreparePasskeyLoginResponse, + RefreshTokenRequest, + ResetPasswordService, + SendResetEmailService, + SignUpService, + Token, + TwoFALoginRequest, + User, + UserSettings, +} from "./user.ts"; +import { + ArchiveWorkflowService, + DownloadWorkflowService, + ListTaskService, + SetDownloadFilesService, + TaskListResponse, + TaskProgresses, + TaskResponse, +} from "./workflow.ts"; + +export function getSiteConfig(section: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/site/config/" + section, + { + method: "GET", + }, + { + ...defaultOpts, + noCredential: section != "basic", + errorSnackbarMsg: (e) => i18n.t("errLoadingSiteConfig", { ns: "common" }) + e.message, + }, + ), + ); + }; +} + +export function sendPrepareLogin(email: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/session/prepare", + { + params: { + email: email, + }, + method: "GET", + }, + { + ...defaultOpts, + noCredential: true, + bypassSnackbar: (e) => e instanceof AppError && e.code == Code.NodeFound, + }, + ), + ); + }; +} + +export function getCaptcha(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/site/captcha", + { + method: "GET", + }, + { + ...defaultOpts, + noCredential: true, + errorSnackbarMsg: (e) => i18n.t("captchaError", { ns: "common" }) + e.message, + }, + ), + ); + }; +} + +export function sendLogin(req: PasswordLoginRequest): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/session/token", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + noCredential: true, + bypassSnackbar: (e) => e instanceof AppError && e.code == Code.Continue, + }, + ), + ); + }; +} + +export function send2FALogin(req: TwoFALoginRequest): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/session/token/2fa", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function getUserMe(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/user/me", + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendRefreshToken(req: RefreshTokenRequest): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/session/token/refresh", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + noCredential: true, + }, + ), + ); + }; +} + +export function getFileList(req: ListFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file", + { + params: req, + method: "GET", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function getFileThumb(path: string, contextHint?: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/thumb", + { + params: { uri: path }, + method: "GET", + headers: contextHint + ? { + [CrHeaders.context_hint]: contextHint, + } + : {}, + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function getUserInfo(uid: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/user/info/" + uid, + { + method: "GET", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function getUserCapacity(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/user/capacity", + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendDeleteFiles(req: DeleteFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file", + { + data: req, + method: "DELETE", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + }, + ), + ); + }; +} + +export function sendUnlockFiles(req: UnlockFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/lock", + { + data: req, + method: "DELETE", + }, + { + ...defaultOpts, + skipLockConflict: true, + }, + ), + ); + }; +} + +export function sendRenameFile(req: RenameFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/rename", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendPinFile(req: PinFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/pin", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUnpinFile(req: PinFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/pin", + { + data: req, + method: "DELETE", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendMoveFile(req: MoveFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/move", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + }, + ), + ); + }; +} + +export function sendRestoreFile(req: DeleteFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/restore", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + }, + ), + ); + }; +} + +export function sendMetadataPatch(req: PatchMetadataService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/metadata", + { + data: req, + method: "PATCH", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + }, + ), + ); + }; +} + +export function getSearchUser(keyword: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/user/search?keyword=" + encodeURIComponent(keyword), + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getAllGroups(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/group/list", + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCreateShare(req: ShareCreateService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/share", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUpdateShare(req: ShareCreateService, id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/share/" + id, + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendDeleteShare(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/share/" + id, + { + method: "DELETE", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getShareInfo( + id: string, + password?: string, + count_views?: boolean, + owner_extended?: boolean, +): ThunkResponse { + return async (dispatch, _getState) => { + let uri = "/share/info/" + id; + const query = new URLSearchParams(); + if (password && password != "") { + query.set("password", password); + } + if (count_views) { + query.set("count_views", "true"); + } + if (owner_extended) { + query.set("owner_extended", "true"); + } + if (query.toString() != "") { + uri += "?" + query.toString(); + } + return await dispatch( + send( + uri, + { + method: "GET", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendCreateFile(req: CreateFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/create", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getFileEntityUrl(req: FileURLService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/url", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + }, + ), + ); + }; +} + +export function getFileInfo(req: GetFileInfoService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/info", + { + method: "GET", + params: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function setCurrentVersion(req: VersionControlService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/version/current", + { + method: "POST", + data: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function deleteVersion(req: VersionControlService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/version", + { + method: "DELETE", + data: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUpdateFile(req: FileUpdateService, data: any): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/content", + { + data, + params: req, + method: "PUT", + headers: { + "Content-Type": "application/octet-stream", + }, + }, + { + bypassSnackbar: (e) => e instanceof AppError && e.code == Code.StaleVersion, + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCreateViewerSession(req: CreateViewerSessionService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/viewerSession", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCreateUploadSession(req: UploadSessionRequest): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/upload", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendUploadChunk( + sessionID: string, + chunk: Blob, + index: number, + cancel?: CancelToken, + onProgress?: (progressEvent: AxiosProgressEvent) => void, +): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/file/upload/${sessionID}/${index}`, + { + data: chunk, + cancelToken: cancel, + onUploadProgress: onProgress, + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + }, + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendDeleteUploadSession(req: DeleteUploadSessionService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/file/upload`, + { + data: req, + method: "DELETE", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendS3LikeCompleteUpload(policyType: string, sessionId: string, sessionKey: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/callback/${policyType}/${sessionId}/${sessionKey}`, + { + method: "GET", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendOneDriveCompleteUpload(sessionId: string, sessionKey: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/callback/onedrive/${sessionId}/${sessionKey}`, + { + method: "POST", + }, + { + ...defaultOpts, + bypassSnackbar: (_e) => true, + }, + ), + ); + }; +} + +export function sendCreateArchive(req: ArchiveWorkflowService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/archive", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendExtractArchive(req: ArchiveWorkflowService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/extract", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getTasks(req: ListTaskService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow", + { + params: req, + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getTasksPhaseProgress(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/progress/" + id, + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCreateRemoteDownload(req: DownloadWorkflowService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/download", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + skipBatchError: (req.src?.length ?? 0) <= 1, + }, + ), + ); + }; +} + +export function sendSetDownloadTarget(id: string, req: SetDownloadFilesService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/download/" + id, + { + data: req, + method: "PATCH", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCancelDownloadTask(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/workflow/download/" + id, + { + method: "DELETE", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getShares(req: ListShareService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/share", + { + method: "GET", + params: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getDavAccounts(req: ListDavAccountsService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/devices/dav", + { + method: "GET", + params: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCreateDavAccounts(req: CreateDavAccountService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/devices/dav", + { + method: "PUT", + data: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUpdateDavAccounts(id: string, req: CreateDavAccountService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/devices/dav/${id}`, + { + method: "PATCH", + data: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendDeleteDavAccount(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/devices/dav/${id}`, + { + method: "DELETE", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getFileDirectLinks(req: MultipleUriService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/file/source", + { + data: req, + method: "PUT", + }, + { + ...defaultOpts, + skipBatchError: req.uris.length == 1, + acceptBatchPartialSuccess: true, + }, + ), + ); + }; +} + +export function getUserShares(req: ListShareService, uid: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/shares/${uid}`, + { + method: "GET", + params: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getUserSettings(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/setting`, + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUploadAvatar(avatar?: Blob, contentType?: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/setting/avatar`, + { + method: "PUT", + data: avatar, + headers: contentType + ? { + "Content-Type": contentType, + } + : undefined, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendUpdateUserSetting(settings: PatchUserSetting): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/setting`, + { + method: "PATCH", + data: settings, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function get2FAInitSecret(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/setting/2fa`, + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendPreparePasskeyRegistration(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/authn`, + { + method: "PUT", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendFinishPasskeyRegistration(req: FinishPasskeyRegistrationService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/authn`, + { + method: "POST", + data: req, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendDeletePasskey(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/authn?id=${encodeURIComponent(id)}`, + { + method: "DELETE", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendFinishPasskeyLogin(req: FinishPasskeyLoginService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/session/authn`, + { + method: "POST", + data: req, + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function sendPreparePasskeyLogin(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/session/authn`, + { + method: "PUT", + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function sendSinUp(req: SignUpService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + "/user", + { + data: req, + method: "POST", + }, + { + ...defaultOpts, + noCredential: true, + bypassSnackbar: (e) => e instanceof AppError && e.code == Code.Continue, + }, + ), + ); + }; +} + +export function sendEmailActivate(id: string, sign: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/activate/${id}?sign=${encodeURIComponent(sign)}`, + { + method: "GET", + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function sendResetEmail(req: SendResetEmailService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/reset`, + { + method: "POST", + data: req, + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function sendReset(uid: string, req: ResetPasswordService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/user/reset/${uid}`, + { + method: "PATCH", + data: req, + }, + { + ...defaultOpts, + noCredential: true, + }, + ), + ); + }; +} + +export function getDashboardSummary(generateCharts?: boolean): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/summary?generate=${!!generateCharts}`, + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getSettings(keys: GetSettingService): ThunkResponse<{ + [key: string]: string; +}> { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/settings`, + { + method: "POST", + data: keys, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendSetSetting(keys: SetSettingService): ThunkResponse<{ + [key: string]: string; +}> { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/settings`, + { + method: "PATCH", + data: keys, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getGroupList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/group`, + { + method: "POST", + data: args, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getWopiDiscovery(args: FetchWOPIDiscoveryService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/tool/wopi`, + { + method: "GET", + params: args, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendTestThumbGeneratorExecutable(args: ThumbGeneratorTestService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/tool/thumbExecutable`, + { + method: "POST", + data: args, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendTestSMTP(args: TestSMTPService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/tool/mail`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getQueueMetrics(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/queue/metrics`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getStoragePolicyList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getStoragePolicyDetail(id: number, countEntity?: boolean): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/${id}`, + { method: "GET", params: { countEntity: countEntity ? true : undefined } }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function upsertStoragePolicy(args: UpsertStoragePolicyService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy${args.policy.id ? `/${args.policy.id}` : ""}`, + { method: "PUT", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getNodeList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getNodeDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node/${id}`, + { + method: "GET", + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function upsertNode(args: UpsertNodeService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node${args.node.id ? `/${args.node.id}` : ""}`, + { + method: "PUT", + data: args, + }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendClearBlobUrlCache(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/tool/entityUrlCache`, + { method: "DELETE" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function createStoragePolicyCors(args: CreateStoragePolicyCorsService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/cors`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getPolicyOauthRedirectUrl(): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/oauth/redirect`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getPolicyOauthCredentialRefreshTime(id: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/oauth/status/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getPolicyOauthUrl(args: GetOauthRedirectService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/oauth/signin`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function finishOauthCallback(args: FinishOauthCallbackService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/oauth/callback`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getOneDriveDriverRoot(id: number, url: string): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/oauth/root/${id}`, + { method: "GET", params: { url } }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function deleteStoragePolicy(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/policy/${id}`, + { method: "DELETE" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getGroupDetail(id: number, countUser?: boolean): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/group/${id}`, + { method: "GET", params: { countUser: countUser ? true : undefined } }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function upsertGroup(args: UpsertGroupService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/group${args.group.id ? `/${args.group.id}` : ""}`, + { method: "PUT", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function deleteGroup(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/group/${id}`, + { method: "DELETE" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function deleteNode(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node/${id}`, + { method: "DELETE" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function testNode(args: TestNodeService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node/test`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function testNodeDownloader(args: TestNodeDownloaderService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/node/test/downloader`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getUserList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/user`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getUserDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/user/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function upsertUser(args: UpsertUserService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/user${args.user.id ? `/${args.user.id}` : ""}`, + { method: "PUT", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function batchDeleteUser(args: BatchIDService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/user/batch/delete`, + { method: "POST", data: args }, + { + ...defaultOpts, + skipBatchError: args.ids.length === 1, + }, + ), + ); + }; +} + +export function getFlattenFileList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/file`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getFileDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/file/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function upsertFile(args: UpsertFileService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/file${args.file.id ? `/${args.file.id}` : ""}`, + { method: "PUT", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getFileUrl(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/file/url/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function batchDeleteFiles(args: BatchIDService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/file/batch/delete`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getEntityList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/entity`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getEntityDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/entity/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getEntityUrl(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/entity/url/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function batchDeleteEntities(args: BatchIDService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/entity/batch/delete`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getTaskList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/queue`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getTaskDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/queue/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function batchDeleteTasks(args: BatchIDService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/queue/batch/delete`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getShareList(args: AdminListService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/share`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function getShareDetail(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/share/${id}`, + { method: "GET" }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function batchDeleteShares(args: BatchIDService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/share/batch/delete`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} + +export function sendCalibrateUserStorage(id: number): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/user/${id}/calibrate`, + { method: "POST" }, + { + ...defaultOpts, + }, + ), + ); + }; +} diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts new file mode 100644 index 0000000..899397c --- /dev/null +++ b/src/api/dashboard.ts @@ -0,0 +1,540 @@ +import { EntityType, PaginationResults, PolicyType } from "./explorer.ts"; +import { Capacity } from "./user.ts"; +import { TaskStatus, TaskSummary } from "./workflow.ts"; + +export interface MetricsSummary { + dates: string[]; + files: number[]; + users: number[]; + shares: number[]; + file_total: number; + user_total: number; + share_total: number; + entities_total: number; + generated_at: string; +} + +export interface Version { + version: string; + pro: boolean; + commit: string; +} + +export interface HomepageSummary { + metrics_summary?: MetricsSummary; + site_urls: string[]; + version: Version; +} + +export interface ManualRefreshLicenseService { + license: string; +} + +export interface GetSettingService { + keys: string[]; +} + +export interface SetSettingService { + settings: { + [key: string]: string; + }; +} + +export interface GroupEnt extends CommonMixin { + name: string; + max_storage?: number; + speed_limit?: number; + permissions?: string; + edges: { + storage_policies?: StoragePolicy; + }; + total_users?: number; + settings?: GroupSetting; +} + +export interface GroupSetting { + compress_size?: number; + decompress_size?: number; + remote_download_options?: Record; + source_batch?: number; + aria2_batch?: number; + max_walked_files?: number; + trash_retention?: number; + redirected_source?: boolean; +} + +export interface AdminListGroupResponse { + groups: GroupEnt[]; + pagination: PaginationResults; +} + +export interface QQConnectConfig { + app_id?: string; + app_secret?: string; + direct_sign_in?: boolean; +} + +export interface LogtoConfig { + endpoint?: string; + app_id?: string; + app_secret?: string; + direct_sign_in?: boolean; + display_name?: string; + direct_sso?: string; +} + +export interface FetchWOPIDiscoveryService { + endpoint: string; +} + +export interface ThumbGeneratorTestService { + name: string; + executable: string; +} + +export interface TestSMTPService { + to: string; + settings: { + [key: string]: string; + }; +} + +export enum QueueType { + IO_INTENSE = "io_intense", + MEDIA_META = "media_meta", + RECYCLE = "recycle", + THUMB = "thumb", + REMOTE_DOWNLOAD = "remote_download", +} + +export interface QueueMetric { + name: QueueType; + busy_workers: number; + success_tasks: number; + failure_tasks: number; + submitted_tasks: number; + suspending_tasks: number; +} + +export interface CommonMixin { + id: number; + created_at?: string; + updated_at?: string; + deleted_at?: string; +} + +export interface StoragePolicy extends CommonMixin { + name: string; + type: PolicyType; + server?: string; + bucket_name?: string; + is_private?: boolean; + access_key?: string; + secret_key?: string; + max_size?: number; + auto_rename?: boolean; + dir_name_rule?: string; + file_name_rule?: string; + settings?: PolicySetting; + node_id?: number; + edges: { + users?: User[]; + groups?: GroupEnt[]; + node?: Node; + }; + entities_count?: number; + entities_size?: number; +} + +export enum NodeType { + master = "master", + slave = "slave", +} + +export interface ListNodeResponse { + nodes: Node[]; + pagination: PaginationResults; +} + +export interface Node extends CommonMixin { + name?: string; + status?: NodeStatus; + type?: NodeType; + server?: string; + slave_key?: string; + capabilities?: string; + weight?: number; + settings?: NodeSetting; + edges: { + storage_policy?: StoragePolicy[]; + }; +} + +export enum DownloaderProvider { + qbittorrent = "qbittorrent", + aria2 = "aria2", +} + +export interface QBittorrentSetting { + server?: string; + user?: string; + password?: string; + options?: Record; + temp_path?: string; +} + +export interface Aria2Setting { + server?: string; + token?: string; + options?: Record; + temp_path?: string; +} + +export interface NodeSetting { + provider?: DownloaderProvider; + qbittorrent?: QBittorrentSetting; + aria2?: Aria2Setting; + interval?: number; + wait_for_seeding?: boolean; +} + +export enum NodeStatus { + active = "active", + suspended = "suspended", +} + +export interface PolicySetting { + token?: string; + file_type?: string[]; + od_redirect?: string; + custom_proxy?: boolean; + proxy_server?: string; + internal_proxy?: boolean; + od_driver?: string; + region?: string; + server_side_endpoint?: string; + chunk_size?: number; + tps_limit?: number; + tps_limit_burst?: number; + s3_path_style?: boolean; + thumb_exts?: string[]; + thumb_support_all_exts?: boolean; + thumb_max_size?: number; + relay?: boolean; + pre_allocate?: boolean; + media_meta_exts?: string[]; + media_meta_generator_proxy?: boolean; + thumb_generator_proxy?: boolean; + native_media_processing?: boolean; + s3_delete_batch_size?: number; + stream_saver?: boolean; + use_cname?: boolean; + source_auth?: boolean; +} + +export interface User extends CommonMixin { + email: string; + nick: string; + password?: string; + settings?: UserSetting; + status?: UserStatus; + storage?: number; + avatar?: string; + credit?: number; + group_expires?: string; + notify_date?: string; + group_users?: number; + previous_group?: number; + unmanaged_email?: boolean; + edges: { + group?: GroupEnt; + storage_policy?: StoragePolicy; + openid?: OpenID[]; + passkey?: Passkey[]; + }; + + hash_id?: string; + two_fa_enabled?: boolean; + capacity?: Capacity; +} + +export interface OpenID extends CommonMixin { + provider?: number; +} + +export interface Passkey extends CommonMixin { + name?: string; +} + +export enum UserStatus { + active = "active", + inactive = "inactive", + manual_banned = "manual_banned", + sys_banned = "sys_banned", +} + +export interface UserSetting { + profile_off?: boolean; + preferred_policy?: number; + preferred_theme?: string; + version_retention?: boolean; + version_retention_ext?: string[]; + version_retention_max?: number; + pined?: PinedFile[]; + language?: string; +} + +export interface PinedFile { + uri: string; + name?: string; +} + +export interface ListStoragePolicyResponse { + policies: StoragePolicy[]; + pagination: PaginationResults; +} + +export interface AdminListService { + page: number; + page_size: number; + order_by: string; + order_direction: string; + conditions?: Record; + searches?: Record; +} + +export interface UpsertStoragePolicyService { + policy: StoragePolicy; +} + +export interface CreateStoragePolicyCorsService { + policy: StoragePolicy; +} + +export interface OauthCredentialStatus { + last_refresh_time: string; + valid: boolean; +} + +export interface GetOauthRedirectService { + id: number; + secret: string; + app_id: string; +} + +export interface FinishOauthCallbackService { + code: string; + state: string; +} + +export interface UpsertGroupService { + group: GroupEnt; +} + +export interface TestNodeService { + node: Node; +} + +export interface TestNodeDownloaderService extends TestNodeService {} + +export interface UpsertNodeService extends TestNodeService {} + +export interface ListUserResponse { + users: User[]; + pagination: PaginationResults; +} + +export interface UpsertUserService { + user: User; + password?: string; + two_fa?: string; +} + +export interface BatchIDService { + ids: number[]; + force?: boolean; +} + +/* +FilePermissions struct { + Groups map[int]*boolset.BooleanSet `json:"groups,omitempty"` + Users map[int]*boolset.BooleanSet `json:"users,omitempty"` + } + +*/ + +export interface File extends CommonMixin { + type?: number; + name?: string; + owner_id?: number; + size?: number; + primary_entity?: number; + file_children?: number; + is_symbolic?: boolean; + storage_policy_files?: number; + edges: { + owner?: User; + storage_policies?: StoragePolicy; + metadata?: Metadata[]; + entities?: Entity[]; + direct_links?: DirectLink[]; + shares?: Share[]; + }; + + user_hash_id?: string; + direct_link_map?: Record; +} + +export interface DirectLink extends CommonMixin { + name?: string; + downloads?: number; + speed?: number; +} + +export interface Entity extends CommonMixin { + type?: EntityType; + source?: string; + size?: number; + reference_count?: number; + storage_policy_entities?: number; + upload_session_id?: string; + edges: { + user?: User; + storage_policy?: StoragePolicy; + file?: File[]; + }; + + user_hash_id?: string; + user_hash_id_map?: Record; +} + +export interface Metadata extends CommonMixin { + name?: string; + value?: string; + is_public?: boolean; +} + +export interface ListFileResponse { + files: File[]; + pagination: PaginationResults; +} + +export interface UpsertFileService { + file: File; +} + +export interface ListEntityResponse { + entities: Entity[]; + pagination: PaginationResults; +} + +export interface LogEntry { + category: number; + failed?: boolean; + error?: string; + user_agent?: string; + is_system?: boolean; + reason?: string; + email_to?: string; + email_title?: string; + original_name?: string; + new_name?: string; + from?: string; + to?: string; + entity_create_time?: string; + storage_policy_id?: string; + storage_policy_name?: string; + account_id?: number; + account?: string; + account_uri?: string; + payment_id?: number; + points_change?: number; + sku?: string; + storage_size?: number; + expire?: string; + group_id?: number; + group_id_from?: number; + direct_link_id?: string; + openid_provider?: number; + sub?: string; + name?: string; + passkey_id?: number; + exts?: Record; +} + +export interface AuditLog extends CommonMixin { + type?: number; + correlation_id?: string; + ip?: string; + content?: LogEntry; + edges: { + user?: User; + file?: File; + entity?: Entity; + share?: Share; + }; + + user_hash_id?: string; +} + +export interface ListAuditLogResponse { + logs: AuditLog[]; + pagination: PaginationResults; +} + +export interface TaskPublicState { + error?: string; + error_history?: string[]; + executed_duration?: number; + retry_count?: number; + resume_time?: number; + slave_task_props?: SlaveTaskProps; +} + +export interface SlaveTaskProps { + node_id?: number; + master_site_url?: string; + master_site_id?: string; + master_site_version?: string; +} + +export interface Task extends CommonMixin { + type?: string; + status?: TaskStatus; + public_state?: TaskPublicState; + private_state?: string; + correlation_id?: string; + user_tasks?: number; + edges: { + user?: User; + }; + + user_hash_id?: string; + summary?: TaskSummary; + node?: Node; +} + +export interface ListTaskResponse { + tasks: Task[]; + pagination: PaginationResults; +} + +export interface Share extends CommonMixin { + password?: string; + views?: number; + downloads?: number; + expire?: string; + remain_downloads?: number; + edges: { + user?: User; + file?: File; + }; + + user_hash_id?: string; + share_link?: string; +} + +export interface ListShareResponse { + shares: Share[]; + pagination: PaginationResults; +} diff --git a/src/api/explorer.ts b/src/api/explorer.ts new file mode 100644 index 0000000..86a6ddb --- /dev/null +++ b/src/api/explorer.ts @@ -0,0 +1,472 @@ +import { User } from "./user.ts"; + +export interface PaginationArgs { + page?: number; + page_size?: number; + order_by?: string; + order_direction?: string; + next_page_token?: string; +} + +export interface ListFileService extends PaginationArgs { + uri: string; +} + +export const FileType = { + file: 0, + folder: 1, +}; + +export interface FileResponse { + type: number; + id: string; + name: string; + created_at: string; + updated_at: string; + size: number; + metadata?: { + [key: string]: string; + }; + path: string; + shared?: boolean; + capability?: string; + owned?: boolean; + folder_summary?: FolderSummary; + extended_info?: ExtendedInfo; + primary_entity?: string; +} + +export interface FolderSummary { + size: number; + files: number; + folders: number; + completed: boolean; + calculated_at: string; +} + +export interface ExtendedInfo { + storage_policy?: StoragePolicy; + storage_used: number; + shares?: Share[]; + entities?: Entity[]; +} + +export interface Entity { + id: string; + type: number; + created_at: string; + storage_policy?: StoragePolicy; + size: number; + created_by?: User; +} + +export interface Share { + id: string; + name?: string; + expires?: string; + is_private?: boolean; + remain_downloads?: number; + created_at?: string; + url: string; + visited: number; + downloaded: number; + expired?: boolean; + unlocked: boolean; + source_type?: number; + owner: User; + source_uri?: string; + password?: string; +} + +export enum PolicyType { + local = "local", + remote = "remote", + oss = "oss", + qiniu = "qiniu", + onedrive = "onedrive", + cos = "cos", + upyun = "upyun", + s3 = "s3", + obs = "obs", +} + +export interface StoragePolicy { + id: string; + name: string; + allowed_suffix?: string[]; + max_size: number; + type: PolicyType; + relay?: boolean; +} + +export interface PaginationResults { + page: number; + page_size: number; + total_items?: number; + next_token?: string; + is_cursor?: boolean; +} + +export interface NavigatorProps { + capability?: string; + max_page_size: number; + order_by_options: string[]; + order_direction_options: string[]; +} + +export interface ListResponse { + files: FileResponse[]; + pagination: PaginationResults; + props: NavigatorProps; + context_hint?: string; + recursion_limit_reached?: boolean; + mixed_type?: boolean; + single_file_view?: boolean; + parent?: FileResponse; + storage_policy?: StoragePolicy; +} + +export const Metadata = { + share_redirect: "sys:shared_redirect", + share_owner: "sys:shared_owner", + upload_session_id: "sys:upload_session_id", + icon_color: "customize:icon_color", + emoji: "customize:emoji", + tag_prefix: "tag:", + thumbDisabled: "thumb:disabled", + restore_uri: "sys:restore_uri", + expected_collect_time: "sys:expected_collect_time", + + // Exif + gps_lng: "exif:longitude", + gps_lat: "exif:latitude", + gps_attitude: "exif:altitude", + artist: "exif:artist", + copy_right: "exif:copy_right", + camera_model: "exif:camera_model", + camera_make: "exif:camera_make", + camera_owner_name: "exif:camera_owner_name", + body_serial_number: "exif:body_serial_number", + lens_make: "exif:lens_make", + lens_model: "exif:lens_model", + software: "exif:software", + exposure_time: "exif:exposure_time", + f_number: "exif:f", + aperture_value: "exif:aperture_value", + focal_length: "exif:focal_length", + iso_speed_ratings: "exif:iso", + pixel_x_dimension: "exif:x", + pixel_y_dimension: "exif:y", + orientation: "exif:orientation", + taken_at: "exif:taken_at", + flash: "exif:flash", + image_description: "exif:image_description", + projection_type: "exif:projection_type", + exposure_bias_value: "exif:exposure_bias", + + // Music + music_title: "music:title", + music_artist: "music:artist", + music_album: "music:album", + + // Stream + stream_title: "stream:title", + stream_duration: "stream:duration", + stream_format_name: "stream:format", + stream_format_long: "stream:formatLong", + stream_bit_rate: "stream:bitrate", + stream_description: "stream:description", + stream_indexed_codec: "codec", + stream_indexed_bitrate: "bitrate", + stream_indexed_width: "width", + stream_indexed_height: "height", +}; + +export interface FileThumbResponse { + url: string; + expires?: string; +} + +export interface DeleteFileService { + uris: string[]; + unlink?: boolean; + skip_soft_delete?: boolean; +} + +export interface LockApplication { + type: string; + inner_xml?: string; + viewer_id?: string; +} + +export interface LockOwner { + application: LockApplication; +} + +export interface ConflictDetail { + path?: string; + token?: string; + owner?: LockOwner; + type: number; +} + +export interface UnlockFileService { + tokens: string[]; +} + +export interface RenameFileService { + uri: string; + new_name: string; +} + +export const NavigatorCapability = { + create_file: 0, + rename_file: 1, + upload_file: 6, + download_file: 7, + update_metadata: 8, + list_children: 9, + generate_thumb: 10, + delete_file: 14, + lock_file: 15, + soft_delete: 16, + restore: 17, + share: 18, + info: 19, + version_control: 20, + enter_folder: 23, +}; + +export interface PinFileService { + uri: string; + name?: string; +} + +export interface MoveFileService extends MultipleUriService { + dst: string; + copy?: boolean; +} + +export interface MetadataPatch { + key: string; + value?: string; + remove?: boolean; +} + +export interface PatchMetadataService extends MultipleUriService { + patches: MetadataPatch[]; +} + +export interface ShareCreateService { + uri: string; + downloads?: number; + is_private?: boolean; + expire?: number; +} + +export interface CreateFileService { + uri: string; + type: "file" | "folder"; + err_on_conflict?: boolean; + metadata?: { + [key: string]: string; + }; +} + +export interface FileURLService extends MultipleUriService { + download?: boolean; + redirect?: boolean; + entity?: string; + no_cache?: boolean; + skip_error?: boolean; + use_primary_site_url?: boolean; + archive?: boolean; +} + +export interface FileURLResponse { + urls: string[]; + expires: string; +} + +export interface GetFileInfoService { + uri: string; + extended?: boolean; + folder_summary?: boolean; +} + +export enum EntityType { + version = 0, + thumbnail = 1, + live_photo = 2, +} + +export interface VersionControlService { + uri: string; + version: string; +} + +export const AuditLogType = { + server_start: 0, + user_signup: 1, + email_sent: 2, + user_activated: 3, + user_login_failed: 4, + user_login: 5, + user_token_refresh: 6, + file_create: 7, + file_rename: 8, + set_file_permission: 9, + entity_uploaded: 10, + entity_downloaded: 11, + copy_from: 12, + copy_to: 13, + move_to: 14, + delete_file: 15, + move_to_trash: 16, + share: 17, + share_link_viewed: 18, + set_current_version: 19, + delete_version: 20, + thumb_generated: 21, + live_photo_uploaded: 22, + update_metadata: 23, + edit_share: 24, + delete_share: 25, + mount: 26, + relocate: 27, + create_archive: 28, + extract_archive: 29, + webdav_login_failed: 30, + webdav_account_create: 31, + webdav_account_update: 32, + webdav_account_delete: 33, + payment_created: 34, + points_change: 35, + payment_paid: 36, + payment_fulfilled: 37, + payment_fulfill_failed: 38, + storage_added: 39, + group_changed: 40, + user_exceed_quota_notified: 41, + user_changed: 42, + get_direct_link: 43, + link_account: 44, + unlink_account: 45, + change_nick: 46, + change_avatar: 47, + membership_unsubscribe: 48, + change_password: 49, + enable_2fa: 50, + disable_2fa: 51, + add_passkey: 52, + remove_passkey: 53, + redeem_gift_code: 54, +}; + +export interface MultipleUriService { + uris: string[]; +} + +export const ViewerAction = { + edit: "edit", + view: "view", +}; + +export const ViewerType = { + builtin: "builtin", + wopi: "wopi", + custom: "custom", +}; + +export interface Viewer { + id: string; + type: string; + display_name: string; + exts: string[]; + icon: string; + url?: string; + max_size?: number; + disabled?: boolean; + props?: { + [key: string]: string; + }; + wopi_actions?: { + [key: string]: { + [key: string]: string; + }; + }; + templates?: NewFileTemplate[]; +} + +export interface NewFileTemplate { + ext: string; + display_name: string; +} + +export interface ViewerGroup { + viewers: Viewer[]; +} + +export interface FileUpdateService { + uri: string; + previous?: string; +} + +export interface ViewerSession { + id: string; + access_token: string; + expires: number; +} + +export interface ViewerSessionResponse { + session: ViewerSession; + wopi_src?: string; +} + +export interface CreateViewerSessionService { + uri: string; + viewer_id: string; + preferred_action: string; + version?: string; +} + +export interface UploadSessionRequest { + uri: string; + size: number; + policy_id: string; + last_modified?: number; + entity_type?: string; + metadata?: { + [key: string]: string; + }; + mime_type?: string; +} + +export interface UploadCredential { + session_id: string; + expires: number; + chunk_size: number; + upload_urls: string[]; + credential: string; + uploadID: string; + callback: string; + ak: string; + keyTime: string; + path: string; + completeURL: string; + storage_policy?: StoragePolicy; + uri: string; + callback_secret: string; + mime_type?: string; + upload_policy?: string; +} + +export interface DeleteUploadSessionService { + id: string; + uri: string; +} + +export interface DirectLink { + file_url: string; + link: string; +} diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 0000000..5733c1e --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,278 @@ +import axios, { AxiosRequestConfig } from "axios"; +import { enqueueSnackbar, SnackbarAction } from "notistack"; +import { DefaultCloseAction, ErrorListDetailAction } from "../component/Common/Snackbar/snackbar.tsx"; +import i18n from "../i18n.ts"; +import { AppThunk } from "../redux/store.ts"; +import { openLockConflictDialog } from "../redux/thunks/dialog.ts"; +import { router } from "../router"; +import SessionManager from "../session"; +import { ErrNames } from "../session/errors.ts"; +import { sendRefreshToken } from "./api.ts"; + +export interface requestOpts { + errorSnackbarMsg: (e: Error) => string; + bypassSnackbar?: (e: Error) => boolean; + noCredential: boolean; + skipBatchError?: boolean; + skipLockConflict?: boolean; + withHost?: boolean; + acceptBatchPartialSuccess?: boolean; +} + +export const defaultOpts: requestOpts = { + errorSnackbarMsg: (e) => e.message, + noCredential: false, + skipBatchError: false, +}; + +export const ApiPrefix = "/api/v4"; + +const instance = axios.create({ + baseURL: ApiPrefix, +}); +export interface AggregatedError { + [key: string]: Response; +} + +export interface Response { + data: T; + code: number; + msg: string; + error?: string; + correlation_id?: string; + aggregated_error?: AggregatedError; +} + +export class AppError extends Error { + public code: any; + public cid: string | undefined = undefined; + public aggregatedError: AggregatedError | undefined = undefined; + public rawMessage: string = ""; + public error?: string = undefined; + public response: Response; + + constructor(resp: Response) { + const message = resp.msg; + const code = resp.code; + super(message); + + this.response = resp; + this.code = code; + const error = resp.error; + this.cid = resp.correlation_id; + this.aggregatedError = resp.aggregated_error; + this.rawMessage = message; + this.error = error; + + if (i18n.exists(`errors.${code}`, { ns: "common" })) { + this.message = i18n.t(`errors.${code}`, { + ns: "common", + message, + }); + } else if (i18n.exists(`errors.${code}`, { ns: "dashboard" })) { + this.message = i18n.t(`errors.${code}`, { + ns: "dashboard", + message, + }); + } else { + this.message = message || i18n.t("unknownError", { ns: "common" }); + } + + this.message += error && !this.message.includes(error) ? ` (${error})` : ""; + this.stack = new Error().stack; + } + + ErrorResponse = (): Response => { + return this.response; + }; +} +// +// instance.interceptors.response.use( +// function (response: any) { +// response.rawData = response.data; +// response.data = response.data.data; +// if ( +// response.rawData.code !== undefined && +// response.rawData.code !== 0 && +// response.rawData.code !== 203 +// ) { +// // Login expired +// if (response.rawData.code === 401) { +// Auth.signout(); +// window.location.href = +// "/login?redirect=" + +// encodeURIComponent( +// window.location.pathname + window.location.search +// ); +// } +// +// // Non-admin +// if (response.rawData.code === 40008) { +// window.location.href = "/home"; +// } +// +// // Not binding mobile phone +// if (response.rawData.code === 40010) { +// window.location.href = "/setting?modal=phone"; +// } +// throw new AppError( +// response.rawData.msg, +// response.rawData.code, +// response.rawData.error +// ); +// } +// return response; +// }, +// function (error) { +// return Promise.reject(error); +// } +// ); + +export type ThunkResponse = AppThunk>; + +export const Code = { + Success: 0, + Continue: 203, + CredentialInvalid: 40020, + IncorrectPassword: 40069, + LockConflict: 40073, + StaleVersion: 40076, + BatchOperationNotFullyCompleted: 40081, + DomainNotLicensed: 40087, + CodeLoginRequired: 401, + PermissionDenied: 403, + NodeFound: 404, +}; + +export const CrHeaderPrefix = "X-Cr-"; +export const CrHeaders = { + context_hint: CrHeaderPrefix + "Context-Hint", +}; + +function getAccessToken(): AppThunk> { + return async (dispatch, _getState) => { + try { + const accessToken = await SessionManager.getAccessToken(); + return `Bearer ${accessToken}`; + } catch (e) { + if (!(e instanceof Error)) { + throw e; + } + switch (e.name) { + case ErrNames.ErrNoAvailableSession: + case ErrNames.ErrRefreshTokenExpired: + case ErrNames.ErrNoSesssionSelected: + return undefined; + case ErrNames.ErrAccessTokenExpired: + // try to refresh token + console.log("refresh access token"); + const newToken = await dispatch(refreshToken()); + return `Bearer ${newToken}`; + } + + throw e; + } + }; +} + +function refreshToken(): AppThunk> { + return async (dispatch, _getState) => { + const user = SessionManager.currentLogin(); + const token = await dispatch(sendRefreshToken({ refresh_token: user.token.refresh_token })); + SessionManager.refreshToken(user.user.id, token); + return token.access_token; + }; +} + +let signOutLock = false; + +export function send( + url: string, + config?: AxiosRequestConfig, + opts: requestOpts = defaultOpts, +): ThunkResponse { + return async (dispatch, _getState) => { + try { + let axiosConf: AxiosRequestConfig = { + ...config, + headers: { + ...config?.headers, + }, + url, + }; + + if (!opts.noCredential) { + const token = await dispatch(getAccessToken()); + if (token && axiosConf.headers) { + axiosConf.headers["Authorization"] = token; + } + } + + const resp = await instance.request>(axiosConf); + + if (resp.data.code !== undefined && resp.data.code !== Code.Success) { + switch (resp.data.code) { + case Code.CredentialInvalid: + case Code.CodeLoginRequired: + if (!signOutLock) { + SessionManager.signOutCurrent(); + router.navigate( + "/session?redirect=" + encodeURIComponent(window.location.pathname + window.location.search), + ); + } + signOutLock = true; + } + + throw new AppError(resp.data); + } + return resp.data.data; + } catch (e) { + let partialSuccessResponse: any = undefined; + if (e instanceof Error) { + // Handle lock conflict error + if (e instanceof AppError && e.code == Code.LockConflict && !opts.skipLockConflict) { + let rejected = false; + try { + await dispatch(openLockConflictDialog(e.response)); + } catch (e) { + rejected = true; + } + if (!rejected) { + return await dispatch(send(url, config, opts)); + } + } + + if (opts.bypassSnackbar && opts.bypassSnackbar(e)) { + throw e; + } + + let action: SnackbarAction = DefaultCloseAction; + // Handle aggregated error + if (e instanceof AppError && e.code == Code.BatchOperationNotFullyCompleted) { + if (!opts.skipBatchError) { + action = ErrorListDetailAction(e.ErrorResponse()); + if (opts.acceptBatchPartialSuccess) { + partialSuccessResponse = e.response.data; + } + } else { + const inner = e.aggregatedError?.[Object.keys(e.aggregatedError)[0]]; + e = inner?.code ? new AppError(inner) : e; + } + } + + if (e instanceof Error) { + enqueueSnackbar({ + message: opts.errorSnackbarMsg(e), + variant: "error", + action, + }); + } + } + + if (partialSuccessResponse) { + return partialSuccessResponse; + } + throw e; + } + }; +} diff --git a/src/api/setting.ts b/src/api/setting.ts new file mode 100644 index 0000000..2b71283 --- /dev/null +++ b/src/api/setting.ts @@ -0,0 +1,32 @@ +import { PaginationResults } from "./explorer.ts"; + +export interface ListDavAccountsService { + page_size: number; + next_page_token?: string; +} + +export interface DavAccount { + id: string; + created_at: string; + name: string; + uri: string; + password: string; + options?: string; +} + +export interface ListDavAccountsResponse { + accounts: DavAccount[]; + pagination?: PaginationResults; +} + +export const DavAccountOption = { + readonly: 0, + proxy: 1, +}; + +export interface CreateDavAccountService { + name: string; + uri: string; + readonly?: boolean; + proxy?: boolean; +} diff --git a/src/api/share.ts b/src/api/share.ts new file mode 100644 index 0000000..1463672 --- /dev/null +++ b/src/api/share.ts @@ -0,0 +1,28 @@ +import { User } from "./user.ts"; +import { PaginationResults, Share } from "./explorer.ts"; + +export interface ShareInfo { + id: string; + name?: string; + source_type?: number; + remain_downloads?: number; + visited?: number; + downloaded?: number; + expires?: string; + created_at?: string; + unlocked: boolean; + owner: User; + expired?: boolean; +} + +export interface ListShareService { + page_size: number; + order_by?: string; + order_direction?: string; + next_page_token?: string; +} + +export interface ListShareResponse { + shares: Share[]; + pagination: PaginationResults; +} diff --git a/src/api/site.ts b/src/api/site.ts new file mode 100644 index 0000000..331bf4a --- /dev/null +++ b/src/api/site.ts @@ -0,0 +1,44 @@ +import { ViewerGroup } from "./explorer.ts"; +import { User } from "./user.ts"; + +export enum CaptchaType { + NORMAL = "normal", + RECAPTCHA = "recaptcha", + // Deprecated + TCAPTCHA = "tcaptcha", + TURNSTILE = "turnstile", +} + +export interface SiteConfig { + instance_id?: string; + title?: string; + login_captcha?: boolean; + reg_captcha?: boolean; + forget_captcha?: boolean; + themes?: string; + default_theme?: string; + authn?: boolean; + user?: User; + captcha_ReCaptchaKey?: string; + captcha_type?: CaptchaType; + turnstile_site_id?: string; + register_enabled?: boolean; + logo?: string; + logo_light?: string; + tos_url?: string; + privacy_policy_url?: string; + icons?: string; + emoji_preset?: string; + map_provider?: string; + google_map_tile_type?: string; + file_viewers?: ViewerGroup[]; + max_batch_size?: number; + app_promotion?: boolean; + thumbnail_width?: number; + thumbnail_height?: number; +} + +export interface CaptchaResponse { + ticket: string; + image: string; +} diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..a5cd616 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,189 @@ +/** + * UserLoginService 管理用户登录的服务 + */ +export interface UserLoginService { + email: string; + password: string; + otp?: string; +} + +/** + * User 用户序列化器 + */ +export interface User { + id: string; + email?: string; + nickname: string; + status?: any /* user.Status */; + avatar?: string; + created_at: any /* time.Time */; + preferred_theme?: string; + anonymous?: boolean; + group?: Group; + pined?: PinedFile[]; + language?: string; +} +export interface Group { + id: string; + name: string; + permission?: string; + direct_link_batch_size?: number; + trash_retention?: number; +} + +export interface PinedFile { + uri: string; + name?: string; +} + +export interface PrepareLoginResponse { + webauthn_enabled: boolean; + sso_enabled: boolean; + password_enabled: boolean; + qq_enabled: boolean; +} + +export interface CaptchaRequest { + [key: string]: any; +} + +export interface PasswordLoginRequest extends CaptchaRequest { + email: string; + password: string; +} + +export interface Token { + access_token: string; + refresh_token: string; + access_expires: string; + refresh_expires: string; +} + +export interface LoginResponse { + user: User; + token: Token; +} + +export interface TwoFALoginRequest { + otp: string; + session_id: string; +} + +export interface RefreshTokenRequest { + refresh_token: string; +} + +export interface Capacity { + total: number; + used: number; +} + +export const GroupPermission = { + is_admin: 0, + is_anonymous: 1, + share: 2, + webdav: 3, + archive_download: 4, + archive_task: 5, + webdav_proxy: 6, + share_download: 7, + remote_download: 9, + redirected_source: 11, + advance_delete: 12, +}; + +export interface UserSettings { + version_retention_enabled: boolean; + version_retention_ext?: string[]; + version_retention_max?: number; + passwordless: boolean; + two_fa_enabled: boolean; + passkeys?: Passkey[]; +} + +export interface PatchUserSetting { + nick?: string; + language?: string; + preferred_theme?: string; + version_retention_enabled?: boolean; + version_retention_ext?: string[]; + version_retention_max?: number; + current_password?: string; + new_password?: string; + two_fa_enabled?: boolean; + two_fa_code?: string; +} + +export interface PasskeyCredentialOption { + publicKey: { + rp: { + name: string; + id: string; + }; + user: { + name: string; + displayName: string; + id: string; + }; + challenge: string; + pubKeyCredParams: { + type: "public-key"; + alg: number; + }[]; + timeout: number; + excludeCredentials: { + type: "public-key"; + id: string; + }[]; + authenticatorSelection: { + requireResidentKey: boolean; + userVerification: UserVerificationRequirement; + }; + }; +} + +export interface PasskeyCredentialLoginOption { + publicKey: { + challenge: string; + timeout: number; + rpId: string; + }; +} + +export interface PreparePasskeyLoginResponse { + options: PasskeyCredentialLoginOption; + session_id: string; +} + +export interface FinishPasskeyRegistrationService { + response: string; + name: string; + ua: string; +} + +export interface Passkey { + id: string; + name: string; + created_at: string; + used_at: string; +} + +export interface FinishPasskeyLoginService { + response: string; + session_id: string; +} + +export interface SignUpService extends CaptchaRequest { + email: string; + password: string; + language: string; +} + +export interface SendResetEmailService extends CaptchaRequest { + email: string; +} + +export interface ResetPasswordService { + password: string; + secret: string; +} diff --git a/src/api/workflow.ts b/src/api/workflow.ts new file mode 100644 index 0000000..1647b8e --- /dev/null +++ b/src/api/workflow.ts @@ -0,0 +1,151 @@ +import { PaginationResults } from "./explorer.ts"; + +export interface ArchiveWorkflowService { + src: string[]; + dst: string; + encoding?: string; +} + +export interface TaskListResponse { + tasks: TaskResponse[]; + pagination: PaginationResults; +} + +export interface TaskResponse { + created_at: string; + updated_at: string; + id: string; + status: string; + type: string; + node?: NodeSummary; + summary?: TaskSummary; + error?: string; + error_history?: string[]; + duration?: number; + resume_time?: number; + retry_count?: number; +} + +export interface TaskSummary { + phase?: string; + props: { + src?: string; + src_str?: string; + dst?: string; + src_multiple?: string[]; + dst_policy_id?: string; + failed?: number; + download?: DownloadTaskStatus; + }; +} + +export enum DownloadTaskState { + seeding = "seeding", + downloading = "downloading", + error = "error", + completed = "completed", + unknown = "unknown", +} + +export interface DownloadTaskStatus { + name: string; + state: DownloadTaskState; + total: number; + downloaded: number; + download_speed: number; + upload_speed: number; + uploaded: number; + files?: DownloadTaskFile[]; + hash?: string; + pieces?: string; + num_pieces?: number; +} + +export interface DownloadTaskFile { + index: number; + name: string; + size: number; + progress: number; + selected: boolean; +} + +export interface NodeSummary { + id: string; + name: string; + type: NodeTypes; + capabilities: string; +} + +export enum NodeTypes { + master = "master", + slave = "slave", +} + +export const NodeCapability = { + none: 0, + create_archive: 1, + extract_archive: 2, + remote_download: 3, + //relocate: 4, +}; + +export interface RelocateWorkflowService { + src: string[]; + dst_policy_id: string; +} + +export interface DownloadWorkflowService { + src?: string[]; + src_file?: string; + dst: string; +} + +export interface ListTaskService { + page_size: number; + category: ListTaskCategory; + next_page_token?: string; +} + +export enum ListTaskCategory { + general = "general", + downloading = "downloading", + downloaded = "downloaded", +} + +export enum TaskType { + create_archive = "create_archive", + extract_archive = "extract_archive", + remote_download = "remote_download", + media_metadata = "media_meta", + entity_recycle_routine = "entity_recycle_routine", + explicit_entity_recycle = "explicit_entity_recycle", + upload_sentinel_check = "upload_sentinel_check", +} + +export enum TaskStatus { + queued = "queued", + processing = "processing", + suspending = "suspending", + error = "error", + canceled = "canceled", + completed = "completed", +} + +export interface TaskProgress { + total: number; + current: number; + identifier: string; +} + +export interface TaskProgresses { + [key: string]: TaskProgress; +} + +export interface SetFileToDownloadArgs { + index: number; + download: boolean; +} + +export interface SetDownloadFilesService { + files: SetFileToDownloadArgs[]; +} diff --git a/src/component/Admin/AdminBundle.tsx b/src/component/Admin/AdminBundle.tsx new file mode 100644 index 0000000..a292c4f --- /dev/null +++ b/src/component/Admin/AdminBundle.tsx @@ -0,0 +1,31 @@ +import EntitySetting from "./Entity/EntitySetting"; +import FileSetting from "./File/FileSetting"; +import EditGroup from "./Group/EditGroup/EditGroup"; +import GroupSetting from "./Group/GroupSetting"; +import Home from "./Home/Home"; +import EditNode from "./Node/EditNode"; +import NodeSetting from "./Node/NodeSetting"; +import Settings from "./Settings/Settings"; +import ShareList from "./Share/ShareList"; +import EditStoragePolicy from "./StoragePolicy/EditStoragePolicy/EditStoragePolicy"; +import OauthCallback from "./StoragePolicy/OauthCallback"; +import StoragePolicySetting from "./StoragePolicy/StoragePolicySetting"; +import TaskList from "./Task/TaskList"; +import UserSetting from "./User/UserSetting"; + +export { + EditGroup, + EditNode, + EditStoragePolicy, + EntitySetting, + FileSetting, + GroupSetting, + Home, + NodeSetting, + OauthCallback, + Settings, + ShareList, + StoragePolicySetting, + TaskList, + UserSetting, +}; diff --git a/src/component/Admin/Common/AdminCard.tsx b/src/component/Admin/Common/AdminCard.tsx new file mode 100644 index 0000000..66a325a --- /dev/null +++ b/src/component/Admin/Common/AdminCard.tsx @@ -0,0 +1,41 @@ +import { Box, styled } from "@mui/material"; + +export const BorderedCard = styled(Box)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(2), + backgroundColor: theme.palette.background.paper, +})); + +export const BorderedCardClickable = styled(BorderedCard)(({ theme }) => ({ + cursor: "pointer", + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + transition: "background-color 0.3s ease", +})); + +export const BorderedCardClickableBaImg = styled(BorderedCardClickable)<{ img?: string }>(({ theme, img }) => ({ + position: "relative", + overflow: "hidden", + "&::before": { + content: '""', + position: "absolute", + top: "-20px", + right: "-20px", + width: "150px", + height: "150px", + backgroundImage: `url(${img})`, + backgroundSize: "cover", + backgroundPosition: "center", + transform: "rotate(-10deg)", + opacity: 0.1, + maskImage: "radial-gradient(circle at center, black 30%, transparent 80%)", + pointerEvents: "none", + zIndex: 0, + }, + "& > *": { + position: "relative", + zIndex: 1, + }, +})); diff --git a/src/component/Admin/Common/Code.tsx b/src/component/Admin/Common/Code.tsx new file mode 100644 index 0000000..e5d04fa --- /dev/null +++ b/src/component/Admin/Common/Code.tsx @@ -0,0 +1,18 @@ +import { Box, styled } from "@mui/material"; +import { grey } from "@mui/material/colors"; + +const StyledCode = styled(Box)(({ theme }) => ({ + backgroundColor: grey[100], + ...theme.applyStyles("dark", { + backgroundColor: grey[900], + }), + border: `1px solid ${theme.palette.divider}`, + borderRadius: "4px", + padding: "1px", + paddingLeft: "4px", + paddingRight: "4px", +})); + +export const Code = ({ children }: { children?: React.ReactNode }) => { + return {children}; +}; diff --git a/src/component/Admin/Common/DomainInput.js b/src/component/Admin/Common/DomainInput.js deleted file mode 100644 index dc8e2ff..0000000 --- a/src/component/Admin/Common/DomainInput.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useEffect, useState } from "react"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import Select from "@material-ui/core/Select"; -import MenuItem from "@material-ui/core/MenuItem"; -import FormHelperText from "@material-ui/core/FormHelperText"; - -export default function DomainInput({ onChange, value, required, label }) { - const [domain, setDomain] = useState(""); - const [protocol, setProtocol] = useState("https://"); - const [error, setError] = useState(); - - useState(() => { - value = value ? value : ""; - if (value.startsWith("https://")) { - setDomain(value.replace("https://", "")); - setProtocol("https://"); - } else { - if (value !== "") { - setDomain(value.replace("http://", "")); - setProtocol("http://"); - } - } - }, [value]); - - useEffect(() => { - if (protocol === "http://" && window.location.protocol === "https:") { - setError( - "您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。" - ); - } else { - setError(""); - } - }, [protocol]); - - return ( - - {label} - { - setDomain(e.target.value); - onChange({ - target: { - value: protocol + e.target.value, - }, - }); - }} - required={required} - startAdornment={ - - - - } - /> - {error !== "" && ( - {error} - )} - - ); -} diff --git a/src/component/Admin/Common/EndpointInput.tsx b/src/component/Admin/Common/EndpointInput.tsx new file mode 100644 index 0000000..caa6258 --- /dev/null +++ b/src/component/Admin/Common/EndpointInput.tsx @@ -0,0 +1,44 @@ +import { TextFieldProps } from "@mui/material"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseFilledTextField } from "../../Common/StyledComponents"; + +export interface EndpointInputProps extends TextFieldProps<"outlined"> { + enforceProtocol?: boolean; + enforcePrefix?: boolean; +} + +export const EndpointInput = (props: EndpointInputProps) => { + const { t } = useTranslation("dashboard"); + const { enforceProtocol, enforcePrefix = true, onChange, ...rest } = props; + const [value, setValue] = useState((props.value as string) ?? ""); + const handleChange = useCallback( + (e: React.ChangeEvent) => { + setValue(e.target.value); + onChange?.(e); + }, + [onChange], + ); + + const showError = useMemo(() => { + if (!enforceProtocol) return false; + return value.startsWith("http://") && window.location.protocol == "https:"; + }, [enforceProtocol, value]); + + return ( + + ); +}; diff --git a/src/component/Admin/Common/GroupSelectionInput.tsx b/src/component/Admin/Common/GroupSelectionInput.tsx new file mode 100644 index 0000000..04edd06 --- /dev/null +++ b/src/component/Admin/Common/GroupSelectionInput.tsx @@ -0,0 +1,96 @@ +import { ListItemText } from "@mui/material"; +import FormControl from "@mui/material/FormControl"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getGroupList } from "../../../api/api"; +import { GroupEnt } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; + +export interface GroupSelectionInputProps { + value: string; + onChange: (value: string) => void; + onChangeGroup?: (group?: GroupEnt) => void; + emptyValue?: string; + emptyText?: string; + fullWidth?: boolean; + required?: boolean; +} + +const AnonymousGroupId = 3; + +const GroupSelectionInput = ({ + value, + onChange, + onChangeGroup, + emptyValue, + emptyText, + fullWidth, + required, +}: GroupSelectionInputProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [groups, setGroups] = useState([]); + + useEffect(() => { + setLoading(true); + dispatch( + getGroupList({ + page_size: 1000, + page: 1, + order_by: "id", + order_direction: "asc", + }), + ) + .then((res) => { + setGroups(res.groups); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const handleChange = (value: string) => { + onChange(value); + onChangeGroup?.(groups.find((g) => g.id === parseInt(value))); + }; + + return ( + + handleChange(e.target.value as string)} + required={required} + > + {groups + .filter((g) => g.id != AnonymousGroupId) + .map((g) => ( + + + {g.name} + + + ))} + {emptyValue !== undefined && emptyText && ( + + {t(emptyText)}} + slotProps={{ + primary: { variant: "body2" }, + }} + /> + + )} + + + ); +}; + +export default GroupSelectionInput; diff --git a/src/component/Admin/Common/MagicVarDialog.tsx b/src/component/Admin/Common/MagicVarDialog.tsx new file mode 100644 index 0000000..2bc2c2d --- /dev/null +++ b/src/component/Admin/Common/MagicVarDialog.tsx @@ -0,0 +1,60 @@ +import { DialogContent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { NoWrapTableCell } from "../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; + +export interface MagicVar { + name: string; + value: string; + example?: string; +} + +export interface MagicVarDialogProps { + open: boolean; + onClose: () => void; + vars: MagicVar[]; +} + +const MagicVarDialog = ({ open, onClose, vars }: MagicVarDialogProps) => { + const { t } = useTranslation("dashboard"); + + return ( + + + + + + + {t("policy.magicVar.variable")} + {t("policy.magicVar.description")} + {t("policy.magicVar.example")} + + + + {vars.map((v, i) => ( + + + {v.name} + + {t(v.value)} + {v.example} + + ))} + +
+
+
+
+ ); +}; + +export default MagicVarDialog; diff --git a/src/component/Admin/Common/NodeSelectionInput.tsx b/src/component/Admin/Common/NodeSelectionInput.tsx new file mode 100644 index 0000000..613a386 --- /dev/null +++ b/src/component/Admin/Common/NodeSelectionInput.tsx @@ -0,0 +1,89 @@ +import { Alert, Box, OutlinedSelectProps, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getNodeList } from "../../../api/api"; +import { Node, NodeStatus, NodeType } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; + +export interface NodeSelectionInputProps extends OutlinedSelectProps { + value: number; + onChange: (value: number) => void; +} + +export const NodeStatusCondition = "node_status"; + +const NodeSelectionInput = ({ value, onChange, ...rest }: NodeSelectionInputProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [nodes, setNodes] = useState([]); + + useEffect(() => { + setLoading(true); + dispatch( + getNodeList({ + page_size: 1000, + page: 1, + order_by: "id", + order_direction: "desc", + conditions: { + [NodeStatusCondition]: NodeStatus.active, + }, + }), + ) + .then((res) => { + const filteredNodes = res.nodes.filter((n) => n.type != NodeType.master); + setNodes(filteredNodes); + if (!value && filteredNodes.length > 0) { + onChange(filteredNodes[0].id); + } + }) + .finally(() => { + setLoading(false); + }); + }, []); + + if (!loading && nodes.length == 0) { + return {t("settings.noNodes")}; + } + return ( + onChange(e.target.value as number)} + MenuProps={{ + PaperProps: { sx: { maxWidth: 230 } }, + MenuListProps: { + sx: { + "& .MuiMenuItem-root": { + whiteSpace: "normal", + }, + }, + }, + }} + {...rest} + > + {nodes.map((g) => ( + + + + {g.name} + + + {g.server} + + + + ))} + + ); +}; + +export default NodeSelectionInput; diff --git a/src/component/Admin/Common/PolicySelector.js b/src/component/Admin/Common/PolicySelector.js deleted file mode 100644 index d6eb7d8..0000000 --- a/src/component/Admin/Common/PolicySelector.js +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import InputLabel from "@material-ui/core/InputLabel"; -import Select from "@material-ui/core/Select"; -import Input from "@material-ui/core/Input"; -import Chip from "@material-ui/core/Chip"; -import MenuItem from "@material-ui/core/MenuItem"; -import { getSelectItemStyles } from "../../../utils"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import { FormControl } from "@material-ui/core"; -import API from "../../../middleware/Api"; -import { useTheme } from "@material-ui/core/styles"; - -export default function PolicySelector({ - onChange, - value, - label, - helperText, - filter, -}) { - const [policies, setPolicies] = useState({}); - const theme = useTheme(); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/policy/list", { - page: 1, - page_size: 10000, - order_by: "id asc", - conditions: {}, - }) - .then((response) => { - const res = {}; - let data = response.data.items; - if (filter) { - data = data.filter(filter); - } - - data.forEach((v) => { - res[v.ID] = v.Name; - }); - setPolicies(res); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - return ( - - {label} - - - {helperText} - - - ); -} diff --git a/src/component/Admin/Common/ProDialog.tsx b/src/component/Admin/Common/ProDialog.tsx new file mode 100644 index 0000000..e3f5627 --- /dev/null +++ b/src/component/Admin/Common/ProDialog.tsx @@ -0,0 +1,96 @@ +import { Button, DialogContent, List, ListItem, ListItemIcon, ListItemText, Typography, styled } from "@mui/material"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import DraggableDialog, { StyledDialogActions } from "../../Dialogs/DraggableDialog"; +import CheckmarkCircleFilled from "../../Icons/CheckmarkCircleFilled"; + +export interface ProDialogProps { + open: boolean; + onClose: () => void; +} + +const features = [ + "shareLinkCollabration", + "filePermission", + "multipleStoragePolicy", + "auditAndActivity", + "vasService", + "sso", + "more", +]; + +const StyledButton = styled(Button)(({ theme }) => ({ + background: `linear-gradient(45deg, ${theme.palette.primary.main} 30%, ${theme.palette.primary.light} 90%)`, + color: theme.palette.primary.contrastText, + "&:hover": { + background: `linear-gradient(45deg, ${theme.palette.primary.dark} 30%, ${theme.palette.primary.main} 90%)`, + }, + transition: "all 300ms cubic-bezier(0.4, 0, 0.2, 1) !important", +})); + +const ProDialog = ({ open, onClose }: ProDialogProps) => { + const { t } = useTranslation("dashboard"); + const openMore = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + window.open("https://cloudreve.org/pro", "_blank"); + }, []); + return ( + + + + {t("pro.description")} + + + + {t("pro.proInclude")} + + + + {features.map((feature) => ( + + + + + + {t(`pro.${feature}`)} + + + ))} + + + + + + {t("pro.learnMore")} + + + + ); +}; + +export default ProDialog; diff --git a/src/component/Admin/Common/SinglePolicySelectionInput.tsx b/src/component/Admin/Common/SinglePolicySelectionInput.tsx new file mode 100644 index 0000000..1645b40 --- /dev/null +++ b/src/component/Admin/Common/SinglePolicySelectionInput.tsx @@ -0,0 +1,122 @@ +import { Box, FormControl, ListItemText, SelectChangeEvent, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getStoragePolicyList } from "../../../api/api"; +import { StoragePolicy } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; + +export interface SinglePolicySelectionInputProps { + value: number; + onChange: (value: number) => void; + emptyValue?: number; + emptyText?: string; + simplified?: boolean; +} + +const SinglePolicySelectionInput = ({ + value, + onChange, + emptyValue, + emptyText, + simplified, +}: SinglePolicySelectionInputProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(false); + const [policyMap, setPolicyMap] = useState>({}); + + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event; + onChange(value as number); + }; + + useEffect(() => { + setLoading(true); + dispatch(getStoragePolicyList({ page: 1, page_size: 1000, order_by: "id", order_direction: "asc" })) + .then((res) => { + setPolicies(res.policies); + setPolicyMap( + res.policies.reduce( + (acc, policy) => { + acc[policy.id] = policy; + return acc; + }, + {} as Record, + ), + ); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( + + ( + + ) + : undefined + } + disabled={loading} + MenuProps={{ + PaperProps: { sx: { maxWidth: 230 } }, + MenuListProps: { + sx: { + "& .MuiMenuItem-root": { + whiteSpace: "normal", + }, + }, + }, + }} + > + {policies.length > 0 && + policies.map((policy) => ( + + + + {policy.name} + + + {t(`policy.${policy.type}`)} + + + + ))} + {emptyValue !== undefined && emptyText && ( + + {t(emptyText)}} + slotProps={{ + primary: { variant: "body2" }, + }} + /> + + )} + + + ); +}; + +export default SinglePolicySelectionInput; diff --git a/src/component/Admin/Common/SizeInput.js b/src/component/Admin/Common/SizeInput.js deleted file mode 100644 index d05cdc2..0000000 --- a/src/component/Admin/Common/SizeInput.js +++ /dev/null @@ -1,101 +0,0 @@ -import FormControl from "@material-ui/core/FormControl"; -import Input from "@material-ui/core/Input"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import FormHelperText from "@material-ui/core/FormHelperText"; - -const unitTransform = (v) => { - if (!v || v.toString() === "0") { - return [0, 1024 * 1024]; - } - for (let i = 4; i >= 0; i--) { - const base = Math.pow(1024, i); - if (v % base === 0) { - return [v / base, base]; - } - } -}; - -export default function SizeInput({ - onChange, - min, - value, - required, - label, - max, - suffix, -}) { - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const [unit, setUnit] = useState(1); - const [val, setVal] = useState(value); - const [err, setError] = useState(""); - - useEffect(() => { - onChange({ - target: { - value: (val * unit).toString(), - }, - }); - if (val * unit > max || val * unit < min) { - setError("不符合尺寸限制"); - } else { - setError(""); - } - }, [val, unit, max, min]); - - useEffect(() => { - const res = unitTransform(value); - setUnit(res[1]); - setVal(res[0]); - }, []); - - return ( - - {label} - setVal(e.target.value)} - required={required} - endAdornment={ - - - - } - /> - {err !== "" && {err}} - - ); -} diff --git a/src/component/Admin/Common/TablePagination.tsx b/src/component/Admin/Common/TablePagination.tsx new file mode 100644 index 0000000..28096ff --- /dev/null +++ b/src/component/Admin/Common/TablePagination.tsx @@ -0,0 +1,86 @@ +import { + Box, + ListItemText, + Pagination, + PaginationProps, + SelectChangeEvent, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; + +export interface TablePaginationProps extends PaginationProps { + rowsPerPageOptions?: number[]; + rowsPerPage?: number; + onRowsPerPageChange?: (pageSize: number) => void; + page: number; + totalItems: number; + onChange: (event: React.ChangeEvent, value: number) => void; +} + +export const TablePagination = ({ + rowsPerPageOptions = [5, 10, 25, 50], + rowsPerPage = 5, + onRowsPerPageChange, + page, + onChange, + totalItems, + ...props +}: TablePaginationProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + const onDenseSelectChange = (e: SelectChangeEvent) => { + onRowsPerPageChange?.(e.target.value as number); + }; + + useEffect(() => { + if ((page - 1) * rowsPerPage >= totalItems) { + onChange({} as React.ChangeEvent, Math.ceil(totalItems / rowsPerPage)); + } + }, [rowsPerPage, totalItems]); + + return ( + + + ( + + )} + > + {rowsPerPageOptions.map((option) => ( + + + + ))} + + + ); +}; + +export default TablePagination; diff --git a/src/component/Admin/Dashboard.js b/src/component/Admin/Dashboard.js deleted file mode 100644 index 40c0559..0000000 --- a/src/component/Admin/Dashboard.js +++ /dev/null @@ -1,458 +0,0 @@ -import { withStyles } from "@material-ui/core"; -import AppBar from "@material-ui/core/AppBar"; -import Divider from "@material-ui/core/Divider"; -import Drawer from "@material-ui/core/Drawer"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import IconButton from "@material-ui/core/IconButton"; -import List from "@material-ui/core/List"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import ListItemText from "@material-ui/core/ListItemText"; -import { lighten, makeStyles, useTheme } from "@material-ui/core/styles"; -import Toolbar from "@material-ui/core/Toolbar"; -import Typography from "@material-ui/core/Typography"; -import { - Assignment, - Category, - CloudDownload, - Contacts, - Group, - Home, - Image, - InsertDriveFile, - Language, - ListAlt, - Mail, - Palette, - Person, - Settings, - SettingsEthernet, - Share, - Storage, - Contactless, -} from "@material-ui/icons"; -import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; -import ChevronRightIcon from "@material-ui/icons/ChevronRight"; -import MenuIcon from "@material-ui/icons/Menu"; -import clsx from "clsx"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory, useLocation } from "react-router"; -import { useRouteMatch } from "react-router-dom"; -import { changeSubTitle } from "../../redux/viewUpdate/action"; -import pathHelper from "../../utils/page"; -import UserAvatar from "../Navbar/UserAvatar"; -import { useTranslation } from "react-i18next"; - -const ExpansionPanel = withStyles({ - root: { - maxWidth: "100%", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": { margin: 0 }, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles({ - root: { - minHeight: 0, - padding: 0, - - "&$expanded": { - minHeight: 0, - }, - }, - content: { - maxWidth: "100%", - margin: 0, - display: "block", - "&$expanded": { - margin: "0", - }, - }, - expanded: {}, -})(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - display: "block", - padding: theme.spacing(0), - }, -}))(MuiExpansionPanelDetails); - -const drawerWidth = 240; - -const useStyles = makeStyles((theme) => ({ - root: { - display: "flex", - width: "100%", - }, - appBar: { - zIndex: theme.zIndex.drawer + 1, - transition: theme.transitions.create(["width", "margin"], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - appBarShift: { - marginLeft: drawerWidth, - width: `calc(100% - ${drawerWidth}px)`, - transition: theme.transitions.create(["width", "margin"], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - menuButton: { - marginRight: 36, - }, - hide: { - display: "none", - }, - drawer: { - width: drawerWidth, - flexShrink: 0, - whiteSpace: "nowrap", - }, - drawerOpen: { - width: drawerWidth, - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - drawerClose: { - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: "hidden", - width: theme.spacing(7) + 1, - [theme.breakpoints.up("sm")]: { - width: theme.spacing(9) + 1, - }, - }, - title: { - flexGrow: 1, - }, - toolbar: { - display: "flex", - alignItems: "center", - justifyContent: "flex-end", - padding: theme.spacing(0, 1), - ...theme.mixins.toolbar, - }, - content: { - flexGrow: 1, - padding: theme.spacing(3), - }, - sub: { - paddingLeft: 36, - color: theme.palette.text.secondary, - }, - subMenu: { - backgroundColor: theme.palette.background.default, - paddingTop: 0, - paddingBottom: 0, - }, - active: { - backgroundColor: lighten(theme.palette.primary.main, 0.8), - color: theme.palette.primary.main, - "&:hover": { - backgroundColor: lighten(theme.palette.primary.main, 0.7), - }, - }, - activeText: { - fontWeight: 500, - }, - activeIcon: { - color: theme.palette.primary.main, - }, -})); - -const items = [ - { - title: "nav.summary", - icon: , - path: "home", - }, - { - title: "nav.settings", - icon: , - sub: [ - { - title: "nav.basicSetting", - path: "basic", - icon: , - }, - { - title: "nav.publicAccess", - path: "access", - icon: , - }, - { - title: "nav.email", - path: "mail", - icon: , - }, - { - title: "nav.transportation", - path: "upload", - icon: , - }, - { - title: "nav.appearance", - path: "theme", - icon: , - }, - { - title: "nav.image", - path: "image", - icon: , - }, - { - title: "nav.captcha", - path: "captcha", - icon: , - }, - ], - }, - { - title: "nav.storagePolicy", - icon: , - path: "policy", - }, - { - title: "nav.nodes", - icon: , - path: "node", - }, - { - title: "nav.groups", - icon: , - path: "group", - }, - { - title: "nav.users", - icon: , - path: "user", - }, - { - title: "nav.files", - icon: , - path: "file", - }, - { - title: "nav.shares", - icon: , - path: "share", - }, - { - title: "nav.tasks", - icon: , - sub: [ - { - title: "nav.remoteDownload", - path: "download", - icon: , - }, - { - title: "nav.generalTasks", - path: "task", - icon: , - }, - ], - }, -]; - -export default function Dashboard({ content }) { - const { t } = useTranslation("dashboard"); - const classes = useStyles(); - const theme = useTheme(); - const [open, setOpen] = useState(!pathHelper.isMobile()); - const [menuOpen, setMenuOpen] = useState(null); - const history = useHistory(); - const location = useLocation(); - - const handleDrawerOpen = () => { - setOpen(true); - }; - - const handleDrawerClose = () => { - setOpen(false); - }; - - const dispatch = useDispatch(); - const SetSubTitle = useCallback( - (title) => dispatch(changeSubTitle(title)), - [dispatch] - ); - - useEffect(() => { - SetSubTitle(t("nav.title")); - }, []); - - useEffect(() => { - return () => { - SetSubTitle(); - }; - }, []); - - const { path } = useRouteMatch(); - - return ( -
- - - - - - - {t("nav.dashboard")} - - - - - -
- - {theme.direction === "rtl" ? ( - - ) : ( - - )} - -
- - - {items.map((item) => { - if (item.path !== undefined) { - return ( - - history.push("/admin/" + item.path) - } - button - className={clsx({ - [classes.active]: location.pathname.startsWith( - "/admin/" + item.path - ), - })} - key={item.title} - > - - {item.icon} - - - - ); - } - return ( - { - setMenuOpen(isExpanded ? item.title : null); - }} - > - - - {item.icon} - - - - - - {item.sub.map((sub) => ( - - history.push( - "/admin/" + sub.path - ) - } - className={clsx({ - [classes.sub]: open, - [classes.active]: location.pathname.startsWith( - "/admin/" + sub.path - ), - })} - button - key={sub.title} - > - - {sub.icon} - - - - ))} - - - - ); - })} - -
-
-
- {content(path)} -
-
- ); -} diff --git a/src/component/Admin/Dialogs/AddGroupk.js b/src/component/Admin/Dialogs/AddGroupk.js deleted file mode 100644 index 5b42e99..0000000 --- a/src/component/Admin/Dialogs/AddGroupk.js +++ /dev/null @@ -1,246 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Switch from "@material-ui/core/Switch"; -import React, { useEffect, useState } from "react"; -import API from "../../../middleware/Api"; - -const useStyles = makeStyles(() => ({ - formContainer: { - margin: "8px 0 8px 0", - }, -})); - -export default function AddGroup({ open, onClose, onSubmit }) { - const classes = useStyles(); - const [groups, setGroups] = useState([]); - const [group, setGroup] = useState({ - name: "", - group_id: 2, - time: "", - price: "", - score: "", - des: "", - highlight: false, - }); - - useEffect(() => { - if (open && groups.length === 0) { - API.get("/admin/groups") - .then((response) => { - setGroups(response.data); - }) - // eslint-disable-next-line @typescript-eslint/no-empty-function - .catch(() => {}); - } - // eslint-disable-next-line - }, [open]); - - const handleChange = (name) => (event) => { - setGroup({ - ...group, - [name]: event.target.value, - }); - }; - - const handleCheckChange = (name) => (event) => { - setGroup({ - ...group, - [name]: event.target.checked, - }); - }; - - const submit = (e) => { - e.preventDefault(); - const groupCopy = { ...group }; - groupCopy.time = parseInt(groupCopy.time) * 86400; - groupCopy.price = parseInt(groupCopy.price) * 100; - groupCopy.score = parseInt(groupCopy.score); - groupCopy.id = new Date().valueOf(); - groupCopy.des = groupCopy.des.split("\n"); - onSubmit(groupCopy); - }; - - return ( - -
- - 添加可购用户组 - - - -
- - - 名称 - - - - 商品展示名称 - - -
- -
- - - 用户组 - - - - 购买后升级的用户组 - - -
- -
- - - 有效期 (天) - - - - 单位购买时间的有效期 - - -
- -
- - - 单价 (元) - - - - 用户组的单价 - - -
- -
- - - 单价 (积分) - - - - 使用积分购买时的价格,填写为 0 - 表示不能使用积分购买 - - -
- -
- - - 商品描述 (一行一个) - - - - 购买页面展示的商品描述 - - -
- -
- - - } - label="突出展示" - /> - - 开启后,在商品选择页面会被突出展示 - - -
-
-
- - - - -
-
- ); -} diff --git a/src/component/Admin/Dialogs/AddPack.js b/src/component/Admin/Dialogs/AddPack.js deleted file mode 100644 index 2e60603..0000000 --- a/src/component/Admin/Dialogs/AddPack.js +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useState } from "react"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogActions from "@material-ui/core/DialogActions"; -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import SizeInput from "../Common/SizeInput"; -import { makeStyles } from "@material-ui/core/styles"; - -const useStyles = makeStyles(() => ({ - formContainer: { - margin: "8px 0 8px 0", - }, -})); - -export default function AddPack({ open, onClose, onSubmit }) { - const classes = useStyles(); - const [pack, setPack] = useState({ - name: "", - size: "1073741824", - time: "", - price: "", - score: "", - }); - - const handleChange = (name) => (event) => { - setPack({ - ...pack, - [name]: event.target.value, - }); - }; - - const submit = (e) => { - e.preventDefault(); - const packCopy = { ...pack }; - packCopy.size = parseInt(packCopy.size); - packCopy.time = parseInt(packCopy.time) * 86400; - packCopy.price = parseInt(packCopy.price) * 100; - packCopy.score = parseInt(packCopy.score); - packCopy.id = new Date().valueOf(); - onSubmit(packCopy); - }; - - return ( - -
- 添加容量包 - - -
- - - 名称 - - - - 商品展示名称 - - -
- -
- - - - 容量包的大小 - - -
- -
- - - 有效期 (天) - - - - 每个容量包的有效期 - - -
- -
- - - 单价 (元) - - - - 容量包的单价 - - -
- -
- - - 单价 (积分) - - - - 使用积分购买时的价格,填写为 0 - 表示不能使用积分购买 - - -
-
-
- - - - -
-
- ); -} diff --git a/src/component/Admin/Dialogs/AddPolicy.js b/src/component/Admin/Dialogs/AddPolicy.js deleted file mode 100644 index 5d7fe05..0000000 --- a/src/component/Admin/Dialogs/AddPolicy.js +++ /dev/null @@ -1,145 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import CardMedia from "@material-ui/core/CardMedia"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Grid from "@material-ui/core/Grid"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React from "react"; -import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - cardContainer: { - display: "flex", - }, - cover: { - width: 100, - height: 60, - }, - card: {}, - content: { - flex: "1 0 auto", - }, - bg: { - backgroundColor: theme.palette.background.default, - padding: "24px 24px", - }, - dialogFooter: { - justifyContent: "space-between", - }, -})); - -const policies = [ - { - name: "local", - img: "local.png", - path: "/admin/policy/add/local", - }, - { - name: "remote", - img: "remote.png", - path: "/admin/policy/add/remote", - }, - { - name: "qiniu", - img: "qiniu.png", - path: "/admin/policy/add/qiniu", - }, - { - name: "oss", - img: "oss.png", - path: "/admin/policy/add/oss", - }, - { - name: "upyun", - img: "upyun.png", - path: "/admin/policy/add/upyun", - }, - { - name: "cos", - img: "cos.png", - path: "/admin/policy/add/cos", - }, - { - name: "onedrive", - img: "onedrive.png", - path: "/admin/policy/add/onedrive", - }, - { - name: "s3", - img: "s3.png", - path: "/admin/policy/add/s3", - }, -]; - -export default function AddPolicy({ open, onClose }) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const { t: tCommon } = useTranslation("common"); - const classes = useStyles(); - - const location = useHistory(); - - return ( - - - {t("selectAStorageProvider")} - - - - {policies.map((v) => ( - - - { - location.push(v.path); - onClose(); - }} - className={classes.cardContainer} - > - - - - {t(v.name)} - - - - - - ))} - - - - - - - - ); -} diff --git a/src/component/Admin/Dialogs/AddRedeem.js b/src/component/Admin/Dialogs/AddRedeem.js deleted file mode 100644 index e7dee8a..0000000 --- a/src/component/Admin/Dialogs/AddRedeem.js +++ /dev/null @@ -1,175 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; - -const useStyles = makeStyles(() => ({ - formContainer: { - margin: "8px 0 8px 0", - }, -})); - -export default function AddRedeem({ open, onClose, products, onSuccess }) { - const classes = useStyles(); - const [input, setInput] = useState({ - num: 1, - id: 0, - time: 1, - }); - const [loading, setLoading] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const handleChange = (name) => (event) => { - setInput({ - ...input, - [name]: event.target.value, - }); - }; - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - input.num = parseInt(input.num); - input.id = parseInt(input.id); - input.time = parseInt(input.time); - input.type = 2; - for (let i = 0; i < products.length; i++) { - if (products[i].id === input.id) { - if (products[i].group_id !== undefined) { - input.type = 1; - } else { - input.type = 0; - } - break; - } - } - - API.post("/admin/redeem", input) - .then((response) => { - onSuccess(response.data); - onClose(); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( - -
- 生成兑换码 - - -
- - - 生成数量 - - - - 激活码批量生成数量 - - -
- -
- - - 对应商品 - - - -
- -
- - - 商品数量 - - - - 对于积分类商品,此处为积分数量,其他商品为时长倍数 - - -
-
-
- - - - -
-
- ); -} diff --git a/src/component/Admin/Dialogs/Alert.js b/src/component/Admin/Dialogs/Alert.js deleted file mode 100644 index afdfaea..0000000 --- a/src/component/Admin/Dialogs/Alert.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import Typography from "@material-ui/core/Typography"; -import DialogActions from "@material-ui/core/DialogActions"; -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import { useTranslation } from "react-i18next"; - -export default function AlertDialog({ title, msg, open, onClose }) { - const { t } = useTranslation("common"); - return ( - - {title} - - - {msg} - - - - - - - ); -} diff --git a/src/component/Admin/Dialogs/CreateTheme.js b/src/component/Admin/Dialogs/CreateTheme.js deleted file mode 100644 index 24eed79..0000000 --- a/src/component/Admin/Dialogs/CreateTheme.js +++ /dev/null @@ -1,355 +0,0 @@ -import AppBar from "@material-ui/core/AppBar"; -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import Fab from "@material-ui/core/Fab"; -import Grid from "@material-ui/core/Grid"; -import IconButton from "@material-ui/core/IconButton"; -import { createMuiTheme, makeStyles } from "@material-ui/core/styles"; -import TextField from "@material-ui/core/TextField"; -import Toolbar from "@material-ui/core/Toolbar"; -import Typography from "@material-ui/core/Typography"; -import { Add, Menu } from "@material-ui/icons"; -import { ThemeProvider } from "@material-ui/styles"; -import React, { useCallback, useState } from "react"; -import { CompactPicker } from "react-color"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - picker: { - "& div": { - boxShadow: "none !important", - }, - marginTop: theme.spacing(1), - }, - "@global": { - ".compact-picker:parent ": { - boxShadow: "none !important", - }, - }, - statusBar: { - height: 24, - width: "100%", - }, - fab: { - textAlign: "right", - }, -})); - -export default function CreateTheme({ open, onClose, onSubmit }) { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const { t: tGlobal } = useTranslation("common"); - const classes = useStyles(); - const [theme, setTheme] = useState({ - palette: { - primary: { - main: "#3f51b5", - contrastText: "#fff", - }, - secondary: { - main: "#d81b60", - contrastText: "#fff", - }, - }, - }); - - const subTheme = useCallback(() => { - try { - return createMuiTheme(theme); - } catch (e) { - return createMuiTheme({}); - } - }, [theme]); - - return ( - - - - - - - {t("primaryColor")} - - { - setTheme({ - ...theme, - palette: { - ...theme.palette, - primary: { - ...theme.palette.primary, - main: e.target.value, - }, - }, - }); - }} - fullWidth - /> -
- { - setTheme({ - ...theme, - palette: { - ...theme.palette, - primary: { - ...theme.palette.primary, - main: c.hex, - }, - }, - }); - }} - /> -
-
- - - {t("secondaryColor")} - - { - setTheme({ - ...theme, - palette: { - ...theme.palette, - secondary: { - ...theme.palette.secondary, - main: e.target.value, - }, - }, - }); - }} - fullWidth - /> -
- { - setTheme({ - ...theme, - palette: { - ...theme.palette, - secondary: { - ...theme.palette.secondary, - main: c.hex, - }, - }, - }); - }} - /> -
-
- - - {t("primaryColorText")} - - { - setTheme({ - ...theme, - palette: { - ...theme.palette, - primary: { - ...theme.palette.primary, - contrastText: e.target.value, - }, - }, - }); - }} - fullWidth - /> -
- { - setTheme({ - ...theme, - palette: { - ...theme.palette, - primary: { - ...theme.palette.primary, - contrastText: c.hex, - }, - }, - }); - }} - /> -
-
- - - {t("secondaryColorText")} - - { - setTheme({ - ...theme, - palette: { - ...theme.palette, - secondary: { - ...theme.palette.secondary, - contrastText: e.target.value, - }, - }, - }); - }} - fullWidth - /> -
- { - setTheme({ - ...theme, - palette: { - ...theme.palette, - secondary: { - ...theme.palette.secondary, - contrastText: c.hex, - }, - }, - }); - }} - /> -
-
-
- - -
- - - - - - - Color - - - -
- -
- - - -
-
- - - - - - - - -
- ); -} diff --git a/src/component/Admin/Dialogs/FileFilter.js b/src/component/Admin/Dialogs/FileFilter.js deleted file mode 100644 index a15ade7..0000000 --- a/src/component/Admin/Dialogs/FileFilter.js +++ /dev/null @@ -1,134 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import TextField from "@material-ui/core/TextField"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { useTranslation } from "react-i18next"; - -export default function FileFilter({ setFilter, setSearch, open, onClose }) { - const { t } = useTranslation("dashboard", { keyPrefix: "file" }); - const { t: tDashboard } = useTranslation("dashboard"); - const { t: tCommon } = useTranslation("common"); - const [input, setInput] = useState({ - policy_id: "all", - user_id: "", - }); - const [policies, setPolicies] = useState([]); - const [keywords, setKeywords] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const handleChange = (name) => (event) => { - setInput({ ...input, [name]: event.target.value }); - }; - - useEffect(() => { - API.post("/admin/policy/list", { - page: 1, - page_size: 10000, - order_by: "id asc", - conditions: {}, - }) - .then((response) => { - setPolicies(response.data.items); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - const submit = () => { - const res = {}; - Object.keys(input).forEach((v) => { - if (input[v] !== "all" && input[v] !== "") { - res[v] = input[v]; - } - }); - setFilter(res); - if (keywords !== "") { - setSearch({ - name: keywords, - }); - } else { - setSearch({}); - } - onClose(); - }; - - return ( - - - {tDashboard("user.filterCondition")} - - - - - {t("storagePolicy")} - - - - - - - - setKeywords(e.target.value)} - id="standard-basic" - label={t("searchFileName")} - /> - - - - - - - - ); -} diff --git a/src/component/Admin/Dialogs/MagicVar.js b/src/component/Admin/Dialogs/MagicVar.js deleted file mode 100644 index 6e90034..0000000 --- a/src/component/Admin/Dialogs/MagicVar.js +++ /dev/null @@ -1,180 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import React from "react"; -import { useTranslation } from "react-i18next"; - -const magicVars = [ - { - value: "{randomkey16}", - des: "16digitsRandomString", - example: "N6IimT5XZP324ACK", - fileOnly: false, - }, - { - value: "{randomkey8}", - des: "8digitsRandomString", - example: "gWz78q30", - fileOnly: false, - }, - { - value: "{timestamp}", - des: "secondTimestamp", - example: "1582692933", - fileOnly: false, - }, - { - value: "{timestamp_nano}", - des: "nanoTimestamp", - example: "1582692933231834600", - fileOnly: false, - }, - { - value: "{uid}", - des: "uid", - example: "1", - fileOnly: false, - }, - { - value: "{originname}", - des: "originalFileName", - example: "MyPico.mp4", - fileOnly: true, - }, - { - value: "{originname_without_ext}", - des: "originFileNameNoext", - example: "MyPico", - fileOnly: true, - }, - { - value: "{ext}", - des: "extension", - example: ".jpg", - fileOnly: true, - }, - { - value: "{uuid}", - des: "uuidV4", - example: "31f0a770-659d-45bf-a5a9-166c06f33281", - fileOnly: true, - }, - { - value: "{date}", - des: "date", - example: "20060102", - fileOnly: false, - }, - { - value: "{datetime}", - des: "dateAndTime", - example: "20060102150405", - fileOnly: false, - }, - { - value: "{year}", - des: "year", - example: "2006", - fileOnly: false, - }, - { - value: "{month}", - des: "month", - example: "01", - fileOnly: false, - }, - { - value: "{day}", - des: "day", - example: "02", - fileOnly: false, - }, - { - value: "{hour}", - des: "hour", - example: "15", - fileOnly: false, - }, - { - value: "{minute}", - des: "minute", - example: "04", - fileOnly: false, - }, - { - value: "{second}", - des: "second", - example: "05", - fileOnly: false, - }, -]; - -export default function MagicVar({ isFile, open, onClose, isSlave }) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy.magicVar" }); - const { t: tCommon } = useTranslation("common"); - return ( - - - {isFile ? t("fileNameMagicVar") : t("pathMagicVar")} - - - - - - - {t("variable")} - {t("description")} - {t("example")} - - - - {magicVars.map((m) => { - if (!m.fileOnly || isFile) { - return ( - - - {m.value} - - {t(m.des)} - {m.example} - - ); - } - })} - {!isFile && ( - - - {"{path}"} - - {t("userUploadPath")} - /MyFile/Documents/ - - )} - -
-
-
- - - -
- ); -} diff --git a/src/component/Admin/Dialogs/ShareFilter.js b/src/component/Admin/Dialogs/ShareFilter.js deleted file mode 100644 index 401fd07..0000000 --- a/src/component/Admin/Dialogs/ShareFilter.js +++ /dev/null @@ -1,103 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import TextField from "@material-ui/core/TextField"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; - -export default function ShareFilter({ setFilter, setSearch, open, onClose }) { - const { t } = useTranslation("dashboard", { keyPrefix: "share" }); - const { t: tDashboard } = useTranslation("dashboard"); - const { t: tCommon } = useTranslation("common"); - const [input, setInput] = useState({ - is_dir: "all", - user_id: "", - }); - const [keywords, setKeywords] = useState(""); - - const handleChange = (name) => (event) => { - setInput({ ...input, [name]: event.target.value }); - }; - - const submit = () => { - const res = {}; - Object.keys(input).forEach((v) => { - if (input[v] !== "all" && input[v] !== "") { - res[v] = input[v]; - } - }); - setFilter(res); - if (keywords !== "") { - setSearch({ - source_name: keywords, - }); - } else { - setSearch({}); - } - onClose(); - }; - - return ( - - - {tDashboard("user.filterCondition")} - - - - - {t("srcType")} - - - - - - - - setKeywords(e.target.value)} - id="standard-basic" - label={tDashboard("file.searchFileName")} - /> - - - - - - - - ); -} diff --git a/src/component/Admin/Dialogs/UserFilter.js b/src/component/Admin/Dialogs/UserFilter.js deleted file mode 100644 index 68b46cf..0000000 --- a/src/component/Admin/Dialogs/UserFilter.js +++ /dev/null @@ -1,139 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import TextField from "@material-ui/core/TextField"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { useTranslation } from "react-i18next"; - -export default function UserFilter({ setFilter, setSearch, open, onClose }) { - const { t } = useTranslation("dashboard", { keyPrefix: "user" }); - const { t: tCommon } = useTranslation("common"); - const [input, setInput] = useState({ - group_id: "all", - status: "all", - }); - const [groups, setGroups] = useState([]); - const [keywords, setKeywords] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const handleChange = (name) => (event) => { - setInput({ ...input, [name]: event.target.value }); - }; - - useEffect(() => { - API.get("/admin/groups") - .then((response) => { - setGroups(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - const submit = () => { - const res = {}; - Object.keys(input).forEach((v) => { - if (input[v] !== "all") { - res[v] = input[v]; - } - }); - setFilter(res); - if (keywords !== "") { - setSearch({ - nick: keywords, - email: keywords, - }); - } else { - setSearch({}); - } - onClose(); - }; - - return ( - - - {t("filterCondition")} - - - - - {t("group")} - - - - - - {t("userStatus")} - - - - - setKeywords(e.target.value)} - id="standard-basic" - label={t("searchNickUserName")} - /> - - - - - - - - ); -} diff --git a/src/component/Admin/Entity/EntityDeleteDialog.tsx b/src/component/Admin/Entity/EntityDeleteDialog.tsx new file mode 100644 index 0000000..2e2d56f --- /dev/null +++ b/src/component/Admin/Entity/EntityDeleteDialog.tsx @@ -0,0 +1,70 @@ +import { Checkbox, DialogContent, FormGroup, Stack, Tooltip } from "@mui/material"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { batchDeleteEntities } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { SmallFormControlLabel } from "../../Common/StyledComponents.tsx"; +import DialogAccordion from "../../Dialogs/DialogAccordion.tsx"; +import DraggableDialog, { StyledDialogContentText } from "../../Dialogs/DraggableDialog.tsx"; + +export interface EntityDeleteDialogProps { + entityID?: number[]; + open: boolean; + onClose?: () => void; + onDelete?: () => void; +} + +const EntityDeleteDialog = ({ entityID, open, onDelete, onClose }: EntityDeleteDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + + const [force, setForce] = useState(false); + const [deleting, setDeleting] = useState(false); + + const onAccept = useCallback(() => { + if (entityID) { + setDeleting?.(true); + dispatch(batchDeleteEntities({ ids: entityID, force })) + .then(() => { + onDelete?.(); + onClose?.(); + }) + .finally(() => { + setDeleting?.(false); + }); + } + }, [entityID, force, setDeleting]); + + return ( + + + + {t("entity.confirmBatchDelete", { num: entityID?.length })} + + + + setForce(e.target.checked)} checked={force} />} + label={t("entity.forceDelete")} + /> + + + + + + + ); +}; +export default EntityDeleteDialog; diff --git a/src/component/Admin/Entity/EntityDialog/EntityDialog.tsx b/src/component/Admin/Entity/EntityDialog/EntityDialog.tsx new file mode 100644 index 0000000..0ce2457 --- /dev/null +++ b/src/component/Admin/Entity/EntityDialog/EntityDialog.tsx @@ -0,0 +1,84 @@ +import { Box, DialogContent } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getEntityDetail } from "../../../../api/api.ts"; +import { Entity } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import EntityForm from "./EntityForm.tsx"; + +export interface EntityDialogProps { + open: boolean; + onClose: () => void; + entityID?: number; +} + +const EntityDialog = ({ open, onClose, entityID }: EntityDialogProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ edges: {}, id: 0 }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!entityID || !open) { + return; + } + setLoading(true); + dispatch(getEntityDetail(entityID)) + .then((res) => { + setValues(res); + }) + .catch(() => { + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [open]); + + return ( + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && } + + + + + + + ); +}; + +export default EntityDialog; diff --git a/src/component/Admin/Entity/EntityDialog/EntityFileList.tsx b/src/component/Admin/Entity/EntityDialog/EntityFileList.tsx new file mode 100644 index 0000000..6d1f7eb --- /dev/null +++ b/src/component/Admin/Entity/EntityDialog/EntityFileList.tsx @@ -0,0 +1,119 @@ +import { Box, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { File } from "../../../../api/dashboard"; +import { FileType } from "../../../../api/explorer"; +import { sizeToString } from "../../../../util"; +import { + NoWrapCell, + NoWrapTableCell, + NoWrapTypography, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents"; +import TimeBadge from "../../../Common/TimeBadge"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import FileTypeIcon from "../../../FileManager/Explorer/FileTypeIcon"; +import FileDialog from "../../File/FileDialog/FileDialog"; +import UserDialog from "../../User/UserDialog/UserDialog"; + +const EntityFileList = ({ files, userHashIDMap }: { files: File[]; userHashIDMap: Record }) => { + const { t } = useTranslation("dashboard"); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(0); + const [fileDialogOpen, setFileDialogOpen] = useState(false); + const [fileDialogID, setFileDialogID] = useState(0); + + const userClicked = (uid: number) => (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setUserDialogOpen(true); + setUserDialogID(uid); + }; + + const fileClicked = (fid: number) => (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setFileDialogOpen(true); + setFileDialogID(fid); + }; + return ( + <> + setUserDialogOpen(false)} userID={userDialogID} /> + setFileDialogOpen(false)} fileID={fileDialogID} /> + + + + + {t("group.#")} + {t("file.name")} + {t("file.size")} + {t("file.uploader")} + {t("file.createdAt")} + + + + {files?.map((option, index) => { + return ( + + + {option.id} + + + + + {option.name} + + + + {sizeToString(option.size ?? 0)} + + + + + + + {option.edges?.owner?.nick} + + + + + + + + + + + ); + })} + {!files?.length && ( + + + {t("file.noRecords")} + + + )} + +
+
+ + ); +}; + +export default EntityFileList; diff --git a/src/component/Admin/Entity/EntityDialog/EntityForm.tsx b/src/component/Admin/Entity/EntityDialog/EntityForm.tsx new file mode 100644 index 0000000..39b2202 --- /dev/null +++ b/src/component/Admin/Entity/EntityDialog/EntityForm.tsx @@ -0,0 +1,114 @@ +import { Box, Grid2 as Grid, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { Entity } from "../../../../api/dashboard"; +import { EntityType } from "../../../../api/explorer"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { sizeToString } from "../../../../util"; +import { NoWrapTypography } from "../../../Common/StyledComponents"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import { EntityTypeText } from "../../../FileManager/Sidebar/Data"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import UserDialog from "../../User/UserDialog/UserDialog"; +import EntityFileList from "./EntityFileList"; +const EntityForm = ({ values }: { values: Entity }) => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation("dashboard"); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(0); + + const userClicked = (e: React.MouseEvent) => { + e.preventDefault(); + setUserDialogOpen(true); + setUserDialogID(values?.edges?.user?.id ?? 0); + }; + + return ( + <> + setUserDialogOpen(false)} userID={userDialogID} /> + + + + + {values.id} + + + + + {sizeToString(values.size ?? 0)} + + + + + {t(EntityTypeText[values.type ?? EntityType.version])} + + + + + {values.reference_count ?? 0} + + + + + + + + + {values?.edges?.user?.nick} + + + + + + + + {values.upload_session_id ?? "-"} + + + + + {values.source ?? "-"} + + + + + + {values.edges?.storage_policy?.name} + + + + + + + + + + ); +}; + +export default EntityForm; diff --git a/src/component/Admin/Entity/EntityFilterPopover.tsx b/src/component/Admin/Entity/EntityFilterPopover.tsx new file mode 100644 index 0000000..cfadd43 --- /dev/null +++ b/src/component/Admin/Entity/EntityFilterPopover.tsx @@ -0,0 +1,153 @@ +import { Box, Button, ListItemText, Popover, PopoverProps, Stack } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { EntityType } from "../../../api/explorer"; +import { DenseFilledTextField, DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import { EntityTypeText } from "../../FileManager/Sidebar/Data"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import SinglePolicySelectionInput from "../Common/SinglePolicySelectionInput"; +export interface EntityFilterPopoverProps extends PopoverProps { + storagePolicy: string; + setStoragePolicy: (storagePolicy: string) => void; + owner: string; + setOwner: (owner: string) => void; + type?: EntityType; + setType: (type?: EntityType) => void; + clearFilters: () => void; +} + +const EntityFilterPopover = ({ + storagePolicy, + setStoragePolicy, + owner, + setOwner, + type, + setType, + clearFilters, + onClose, + open, + ...rest +}: EntityFilterPopoverProps) => { + const { t } = useTranslation("dashboard"); + + // Create local state to track changes before applying + const [localStoragePolicy, setLocalStoragePolicy] = useState(storagePolicy); + const [localOwner, setLocalOwner] = useState(owner); + const [localType, setLocalType] = useState(type); + + // Initialize local state when popup opens + useEffect(() => { + if (open) { + setLocalStoragePolicy(storagePolicy); + setLocalOwner(owner); + setLocalType(type); + } + }, [open]); + + // Apply filters and close popover + const handleApplyFilters = () => { + setStoragePolicy(localStoragePolicy); + setOwner(localOwner); + setType(localType); + onClose?.({}, "backdropClick"); + }; + + // Reset filters and close popover + const handleResetFilters = () => { + setLocalStoragePolicy(""); + setLocalOwner(""); + setLocalType(undefined); + clearFilters(); + onClose?.({}, "backdropClick"); + }; + + return ( + + + + setLocalOwner(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalType(e.target.value === -1 ? undefined : (e.target.value as EntityType))} + > + {[EntityType.version, EntityType.thumbnail, EntityType.live_photo].map((type) => ( + + + + ))} + + {t("user.all")}} + slotProps={{ + primary: { + variant: "body2", + }, + }} + /> + + + + + + setLocalStoragePolicy(value.toString())} + emptyValue={-1} + emptyText={t("user.all")} + /> + + + + + + + + + ); +}; + +export default EntityFilterPopover; diff --git a/src/component/Admin/Entity/EntityRow.tsx b/src/component/Admin/Entity/EntityRow.tsx new file mode 100644 index 0000000..94867c1 --- /dev/null +++ b/src/component/Admin/Entity/EntityRow.tsx @@ -0,0 +1,211 @@ +import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow, Tooltip } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { getEntityUrl } from "../../../api/api"; +import { Entity } from "../../../api/dashboard"; +import { EntityType } from "../../../api/explorer"; +import { useAppDispatch } from "../../../redux/hooks"; +import { sizeToString } from "../../../util"; +import { NoWrapTableCell, NoWrapTypography, SquareChip } from "../../Common/StyledComponents"; +import TimeBadge from "../../Common/TimeBadge"; +import UserAvatar from "../../Common/User/UserAvatar"; +import { EntityTypeText } from "../../FileManager/Sidebar/Data"; +import Delete from "../../Icons/Delete"; +import Download from "../../Icons/Download"; + +export interface EntityRowProps { + entity?: Entity; + loading?: boolean; + selected?: boolean; + onDelete?: (id: number) => void; + onSelect?: (id: number) => void; + openEntityDialog?: (id: number) => void; + openUserDialog?: (id: number) => void; +} + +const EntityRow = ({ + entity, + loading, + selected, + onDelete, + onSelect, + openUserDialog, + openEntityDialog, +}: EntityRowProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [deleteLoading, setDeleteLoading] = useState(false); + const [openLoading, setOpenLoading] = useState(false); + + const onSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onSelect?.(entity?.id ?? 0); + }; + + const onOpenClick = (e: React.MouseEvent) => { + e.stopPropagation(); + var entityLink = window.open("", "_blank"); + entityLink?.document.write("Loading entity URL..."); + setOpenLoading(true); + dispatch(getEntityUrl(entity?.id ?? 0)) + .then((url) => { + entityLink ? (entityLink.location.href = url) : window.open(url, "_blank"); + }) + .finally(() => { + setOpenLoading(false); + }) + .catch(() => { + entityLink && entityLink.close(); + }); + }; + + const userClicked = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + openUserDialog?.(entity?.edges?.user?.id ?? 0); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onDelete?.(entity?.id ?? 0); + }; + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + return ( + openEntityDialog?.(entity?.id ?? 0)} + selected={selected} + > + + + + + {entity?.id} + + + {t(EntityTypeText[entity?.type ?? EntityType.version])} + + + + + {entity?.source || "-"} + + + {!entity?.reference_count && } + + + + + {sizeToString(entity?.size ?? 0)} + + + + + {entity?.edges?.storage_policy?.name || "-"} + + + + + {entity?.reference_count ?? 0} + + + + + + + + + + + + {entity?.edges?.user?.nick || "-"} + + + + + + + + + + + + + + + + ); +}; + +export default EntityRow; diff --git a/src/component/Admin/Entity/EntitySetting.tsx b/src/component/Admin/Entity/EntitySetting.tsx new file mode 100644 index 0000000..9a28ea9 --- /dev/null +++ b/src/component/Admin/Entity/EntitySetting.tsx @@ -0,0 +1,328 @@ +import { Delete } from "@mui/icons-material"; +import { + Badge, + Box, + Button, + Checkbox, + Container, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getEntityList } from "../../../api/api"; +import { AdminListService, Entity } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import ArrowSync from "../../Icons/ArrowSync"; +import Filter from "../../Icons/Filter"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import UserDialog from "../User/UserDialog/UserDialog"; +import EntityDeleteDialog from "./EntityDeleteDialog"; +import EntityDialog from "./EntityDialog/EntityDialog"; +import EntityFilterPopover from "./EntityFilterPopover"; +import EntityRow from "./EntityRow"; +export const StoragePolicyQuery = "storage_policy"; +export const UserQuery = "user"; +export const TypeQuery = "type"; + +const EntitySetting = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [entities, setEntities] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [storagePolicy, setStoragePolicy] = useQueryState(StoragePolicyQuery, { defaultValue: "" }); + const [user, setUser] = useQueryState(UserQuery, { defaultValue: "" }); + const [type, setType] = useQueryState(TypeQuery, { defaultValue: "" }); + const [count, setCount] = useState(0); + const [selected, setSelected] = useState([]); + const filterPopupState = usePopupState({ + variant: "popover", + popupId: "entityFilterPopover", + }); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(undefined); + const [entityDialogOpen, setEntityDialogOpen] = useState(false); + const [entityDialogID, setEntityDialogID] = useState(undefined); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteDialogID, setDeleteDialogID] = useState(undefined); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 10; + + const clearFilters = useCallback(() => { + setStoragePolicy(""); + setUser(""); + setType(""); + }, [setStoragePolicy, setUser, setType]); + + useEffect(() => { + fetchEntities(); + }, [page, pageSize, orderBy, orderDirection, storagePolicy, user, type]); + + const fetchEntities = () => { + setLoading(true); + setSelected([]); + + const params: AdminListService = { + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: {}, + }; + + if (storagePolicy) { + params.conditions!.entity_policy = storagePolicy; + } + + if (user) { + params.conditions!.entity_user = user; + } + + if (type) { + params.conditions!.entity_type = type; + } + + dispatch(getEntityList(params)) + .then((res) => { + setEntities(res.entities); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleDelete = () => { + setDeleteDialogOpen(true); + setDeleteDialogID(Array.from(selected)); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = entities.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleSelect = useCallback( + (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); + } + setSelected(newSelected); + }, + [selected], + ); + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + const hasActiveFilters = useMemo(() => { + return !!(storagePolicy || user || type); + }, [storagePolicy, user, type]); + + const handleUserDialogOpen = (id: number) => { + setUserDialogID(id); + setUserDialogOpen(true); + }; + + const handleEntityDialogOpen = (id: number) => { + setEntityDialogID(id); + setEntityDialogOpen(true); + }; + + const handleSingleDelete = (id: number) => { + setDeleteDialogOpen(true); + setDeleteDialogID([id]); + }; + + return ( + + setEntityDialogOpen(false)} entityID={entityDialogID} /> + setUserDialogOpen(false)} userID={userDialogID} /> + setDeleteDialogOpen(false)} + entityID={deleteDialogID} + onDelete={fetchEntities} + /> + + + + setType(type !== undefined ? type.toString() : "")} + clearFilters={clearFilters} + /> + + }> + {t("node.refresh")} + + + + } variant="contained" {...bindTrigger(filterPopupState)}> + {t("user.filter")} + + + + {selected.length > 0 && !isMobile && ( + <> + + + + )} + + {isMobile && selected.length > 0 && ( + + + + )} + + + + + + 0 && selected.length < entities.length} + checked={entities.length > 0 && selected.length === entities.length} + onChange={handleSelectAllClick} + /> + + + + {t("group.#")} + + + {t("file.blobType")} + {t("file.source")} + + + {t("file.size")} + + + {t("file.storagePolicy")} + + + {t("entity.refenenceCount")} + + + + + {t("file.createdAt")} + + + {t("file.creator")} + + + + + {!loading && + entities.map((entity) => ( + + ))} + {loading && + entities.length > 0 && + entities.slice(0, 10).map((entity) => )} + {loading && + entities.length === 0 && + Array.from(Array(10)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default EntitySetting; diff --git a/src/component/Admin/File/File.js b/src/component/Admin/File/File.js deleted file mode 100644 index 52f1ca3..0000000 --- a/src/component/Admin/File/File.js +++ /dev/null @@ -1,501 +0,0 @@ -import { lighten } from "@material-ui/core"; -import Badge from "@material-ui/core/Badge"; -import Button from "@material-ui/core/Button"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import Link from "@material-ui/core/Link"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Toolbar from "@material-ui/core/Toolbar"; -import Tooltip from "@material-ui/core/Tooltip"; -import Typography from "@material-ui/core/Typography"; -import { Delete, DeleteForever, FilterList,LinkOff } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { sizeToString } from "../../../utils"; -import FileFilter from "../Dialogs/FileFilter"; -import { formatLocalTime } from "../../../utils/datetime"; -import Chip from "@material-ui/core/Chip"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - alignItems: "flex-start", - }, - headerRight: {}, - highlight: - theme.palette.type === "light" - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, - disabledBadge: { - marginLeft: theme.spacing(1), - height: 18, - }, -})); - -export default function File() { - const { t } = useTranslation("dashboard", { keyPrefix: "file" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [files, setFiles] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [filter, setFilter] = useState({}); - const [users, setUsers] = useState({}); - const [search, setSearch] = useState({}); - const [orderBy, setOrderBy] = useState(["id", "desc"]); - const [filterDialog, setFilterDialog] = useState(false); - const [selected, setSelected] = useState([]); - const [loading, setLoading] = useState(false); - - const history = useHistory(); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/file/list", { - page: page, - page_size: pageSize, - order_by: orderBy.join(" "), - conditions: filter, - searches: search, - }) - .then((response) => { - setFiles(response.data.items); - setTotal(response.data.total); - setSelected([]); - setUsers(response.data.users); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, orderBy, filter, search]); - - const deleteFile = (id, unlink = false) => { - setLoading(true); - API.post("/admin/file/delete", { id: [id], unlink }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleteAsync"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteBatch = - (force, unlink = false) => - () => { - setLoading(true); - API.post("/admin/file/delete", { id: selected, force, unlink }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleteAsync"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = files.map((n) => n.ID); - setSelected(newSelecteds); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - selected.slice(0, selectedIndex), - selected.slice(selectedIndex + 1) - ); - } - - setSelected(newSelected); - }; - - const isSelected = (id) => selected.indexOf(id) !== -1; - - return ( -
- setFilterDialog(false)} - setSearch={setSearch} - setFilter={setFilter} - /> -
- -
- - setFilterDialog(true)} - > - - - - - - -
-
- - - {selected.length > 0 && ( - - - {tDashboard("user.selectedObjects", { - num: selected.length, - })} - - - - - - - - - - - - - - - - - - )} - - - - - - 0 && - selected.length < files.length - } - checked={ - files.length > 0 && - selected.length === files.length - } - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all desserts", - }} - /> - - - - setOrderBy([ - "id", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - # - {orderBy[0] === "id" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "name", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("name")} - {orderBy[0] === "name" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "size", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("size")} - {orderBy[0] === "size" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {t("uploader")} - - - {t("createdAt")} - - - {tDashboard("policy.actions")} - - - - - {files.map((row) => ( - - - - handleClick(event, row.ID) - } - checked={isSelected(row.ID)} - /> - - {row.ID} - - - {row.Name} - {row.UploadSessionID && ( - - )} - - - - {sizeToString(row.Size)} - - - - {users[row.UserID] - ? users[row.UserID].Nick - : t("unknownUploader")} - - - - {formatLocalTime( - row.CreatedAt, - "YYYY-MM-DD H:mm:ss" - )} - - - - - deleteFile(row.ID) - } - size={"small"} - > - - - - - - deleteFile(row.ID, true) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/File/FileDialog/FileDialog.tsx b/src/component/Admin/File/FileDialog/FileDialog.tsx new file mode 100644 index 0000000..6931ed4 --- /dev/null +++ b/src/component/Admin/File/FileDialog/FileDialog.tsx @@ -0,0 +1,162 @@ +import { Box, Button, Collapse, DialogActions, DialogContent } from "@mui/material"; +import * as React from "react"; +import { createContext, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getFileDetail, upsertFile } from "../../../../api/api.ts"; +import { File, UpsertFileService } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import FileForm from "./FileForm.tsx"; + +export interface FileDialogProps { + open: boolean; + onClose: () => void; + fileID?: number; + onUpdated?: (file: File) => void; +} + +export interface FileDialogContextProps { + values: File; + setFile: (f: (p: File) => File) => void; + formRef?: React.RefObject; +} + +const defaultFile: File = { + id: 0, + name: "", + size: 0, + edges: {}, +}; + +export const FileDialogContext = createContext({ + values: { ...defaultFile }, + setFile: () => {}, +}); + +const FileDialog = ({ open, onClose, fileID, onUpdated }: FileDialogProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ + ...defaultFile, + }); + const [modifiedValues, setModifiedValues] = useState({ + ...defaultFile, + }); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + useEffect(() => { + if (!fileID || !open) { + return; + } + setLoading(true); + dispatch(getFileDetail(fileID)) + .then((res) => { + setValues(res); + setModifiedValues(res); + }) + .catch(() => { + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [open]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + const args: UpsertFileService = { + file: { ...modifiedValues }, + }; + + setSubmitting(true); + dispatch(upsertFile(args)) + .then((res) => { + setValues(res); + setModifiedValues(res); + onUpdated?.(res); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && } + + + + + + + + + + + + + + ); +}; + +export default FileDialog; diff --git a/src/component/Admin/File/FileDialog/FileDirectLinks.tsx b/src/component/Admin/File/FileDialog/FileDirectLinks.tsx new file mode 100644 index 0000000..e68eb3a --- /dev/null +++ b/src/component/Admin/File/FileDialog/FileDirectLinks.tsx @@ -0,0 +1,109 @@ +import { IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip } from "@mui/material"; +import { useCallback, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { sizeToString } from "../../../../util"; +import { + NoWrapCell, + NoWrapTableCell, + NoWrapTypography, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents"; +import TimeBadge from "../../../Common/TimeBadge"; +import Delete from "../../../Icons/Delete"; +import Open from "../../../Icons/Open"; +import { FileDialogContext } from "./FileDialog"; + +const FileDirectLinks = () => { + const { t } = useTranslation("dashboard"); + const { setFile, values } = useContext(FileDialogContext); + + const handleDelete = (id: number) => { + setFile((prev) => ({ + ...prev, + edges: { + ...prev.edges, + direct_links: prev.edges?.direct_links?.filter((link) => link.id !== id), + }, + })); + }; + + const handleOpen = (id: number) => { + window.open(values?.direct_link_map?.[id] ?? "", "_blank"); + }; + + const linkId = useCallback( + (id: number) => { + const url = new URL(values?.direct_link_map?.[id] ?? ""); + return url.pathname; + }, + [values?.direct_link_map], + ); + + return ( + + + + + {t("group.#")} + {t("file.name")} + {t("file.downloads")} + {t("file.speed")} + {t("file.directLinkId")} + {t("file.createdAt")} + + + + + {values?.edges?.direct_links?.map((option, index) => { + const lid = linkId(option.id); + return ( + + + {option.id} + + + {option.name ?? ""} + + + {option.downloads ?? 0} + + + + {option.speed ? `${sizeToString(option.speed)}/s` : "-"} + + + + + {lid} + + + + + + + + + handleOpen(option.id)}> + + + handleDelete(option.id)}> + + + + + ); + })} + {!values?.edges?.direct_links?.length && ( + + + {t("file.noRecords")} + + + )} + +
+
+ ); +}; + +export default FileDirectLinks; diff --git a/src/component/Admin/File/FileDialog/FileEntity.tsx b/src/component/Admin/File/FileDialog/FileEntity.tsx new file mode 100644 index 0000000..677ac40 --- /dev/null +++ b/src/component/Admin/File/FileDialog/FileEntity.tsx @@ -0,0 +1,130 @@ +import { Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip } from "@mui/material"; +import { useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { EntityType } from "../../../../api/explorer"; +import { sizeToString } from "../../../../util"; +import { + NoWrapCell, + NoWrapTableCell, + NoWrapTypography, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents"; +import TimeBadge from "../../../Common/TimeBadge"; +import { EntityTypeText } from "../../../FileManager/Sidebar/Data"; +import EntityDialog from "../../Entity/EntityDialog/EntityDialog"; +import UserDialog from "../../User/UserDialog/UserDialog"; +import { FileDialogContext } from "./FileDialog"; + +const FileEntity = () => { + const { t } = useTranslation("dashboard"); + const { setFile, values } = useContext(FileDialogContext); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogId, setUserDialogId] = useState(null); + const [entityDialogOpen, setEntityDialogOpen] = useState(false); + const [entityDialogId, setEntityDialogId] = useState(null); + + const handleUserDialogOpen = (id: number) => (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setUserDialogId(id); + setUserDialogOpen(true); + }; + + const handleEntityDialogOpen = (id: number) => (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setEntityDialogId(id); + setEntityDialogOpen(true); + }; + return ( + + setUserDialogOpen(false)} userID={userDialogId ?? undefined} /> + setEntityDialogOpen(false)} + entityID={entityDialogId ?? undefined} + /> + + + + {t("group.#")} + {t("file.blobType")} + {t("file.size")} + {t("file.storagePolicy")} + {t("file.source")} + {t("file.createdAt")} + {t("file.creator")} + + + + {values?.edges?.entities?.map((option, index) => ( + + + {option.id} + + + + {t(EntityTypeText[option.type ?? EntityType.version])} + + + + {sizeToString(option.size ?? 0)} + + + + + {option.edges?.storage_policy?.name ?? ""} + + + + + + {option.source ?? ""} + + + + + + + + + + + {option.edges?.user?.nick ?? ""} + + + + + ))} + {!values?.edges?.entities?.length && ( + + + {t("file.noEntities")} + + + )} + +
+
+ ); +}; + +export default FileEntity; diff --git a/src/component/Admin/File/FileDialog/FileForm.tsx b/src/component/Admin/File/FileDialog/FileForm.tsx new file mode 100644 index 0000000..e36cd23 --- /dev/null +++ b/src/component/Admin/File/FileDialog/FileForm.tsx @@ -0,0 +1,125 @@ +import { Box, Grid2 as Grid, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { sizeToString } from "../../../../util"; +import { DenseFilledTextField, NoWrapTypography } from "../../../Common/StyledComponents"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import SinglePolicySelectionInput from "../../Common/SinglePolicySelectionInput"; +import UserDialog from "../../User/UserDialog/UserDialog"; +import { FileDialogContext } from "./FileDialog"; +import FileDirectLinks from "./FileDirectLinks"; +import FileEntity from "./FileEntity"; +import FileMetadata from "./FileMetadata"; + +const FileForm = () => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation("dashboard"); + const { formRef, values, setFile } = useContext(FileDialogContext); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(0); + + const onNameChange = useCallback( + (e: React.ChangeEvent) => { + setFile((prev) => ({ ...prev, name: e.target.value })); + }, + [setFile], + ); + + const userClicked = (e: React.MouseEvent) => { + e.preventDefault(); + setUserDialogOpen(true); + setUserDialogID(values?.edges?.owner?.id ?? 0); + }; + + const sizeUsed = useMemo(() => { + return sizeToString(values?.edges?.entities?.reduce((acc, entity) => acc + (entity.size ?? 0), 0) ?? 0); + }, [values?.edges?.entities]); + + return ( + <> + setUserDialogOpen(false)} userID={userDialogID} /> + e.preventDefault()}> + + + + {values.id} + + + + + {sizeToString(values.size ?? 0)} + + + + + {sizeUsed} + + + + + , + ]} + ns="dashboard" + values={{ num: values.edges?.shares?.length ?? 0 }} + /> + + + + + + + + + {values?.edges?.owner?.nick} + + + + + + + + + + {}} /> + + + + + + + + + + + + + + ); +}; + +export default FileForm; diff --git a/src/component/Admin/File/FileDialog/FileMetadata.tsx b/src/component/Admin/File/FileDialog/FileMetadata.tsx new file mode 100644 index 0000000..b51f625 --- /dev/null +++ b/src/component/Admin/File/FileDialog/FileMetadata.tsx @@ -0,0 +1,114 @@ +import { Delete } from "@mui/icons-material"; +import { Checkbox, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { + DenseFilledTextField, + NoWrapCell, + NoWrapTableCell, + NoWrapTypography, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents"; +import { FileDialogContext } from "./FileDialog"; + +const FileMetadata = () => { + const { t } = useTranslation("dashboard"); + const { setFile, values } = useContext(FileDialogContext); + const onKeyChange = (index: number, value: string) => { + setFile((prev) => ({ + ...prev, + edges: { + ...prev.edges, + metadata: prev.edges?.metadata?.map((item, i) => (i === index ? { ...item, name: value } : item)), + }, + })); + }; + const onValueChange = (index: number, value: string) => { + setFile((prev) => ({ + ...prev, + edges: { + ...prev.edges, + metadata: prev.edges?.metadata?.map((item, i) => (i === index ? { ...item, value: value } : item)), + }, + })); + }; + const onIsPublicChange = (index: number, value: boolean) => { + setFile((prev) => ({ + ...prev, + edges: { + ...prev.edges, + metadata: prev.edges?.metadata?.map((item, i) => + i === index ? { ...item, is_public: value ? true : undefined } : item, + ), + }, + })); + }; + const handleDelete = (id: number) => { + setFile((prev) => ({ + ...prev, + edges: { ...prev.edges, metadata: prev.edges?.metadata?.filter((item) => item.id !== id) }, + })); + }; + return ( + + + + + {t("file.name")} + {t("file.value")} + {t("file.isPublic")} + {t("group.#")} + + + + + {values?.edges?.metadata?.map((option, index) => ( + + + onKeyChange(index, e.target.value)} + /> + + + onValueChange(index, e.target.value)} + /> + + + onIsPublicChange(index, e.target.checked)} + /> + + + {option.id} + + + handleDelete(option.id)}> + + + + + ))} + {!values?.edges?.metadata?.length && ( + + + {t("file.noMetadata")} + + + )} + +
+
+ ); +}; + +export default FileMetadata; diff --git a/src/component/Admin/File/FileFilterPopover.tsx b/src/component/Admin/File/FileFilterPopover.tsx new file mode 100644 index 0000000..e33bec6 --- /dev/null +++ b/src/component/Admin/File/FileFilterPopover.tsx @@ -0,0 +1,129 @@ +import { Box, Button, Popover, PopoverProps, Stack } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseFilledTextField } from "../../Common/StyledComponents"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import SinglePolicySelectionInput from "../Common/SinglePolicySelectionInput"; + +export interface FileFilterPopoverProps extends PopoverProps { + storagePolicy: string; + setStoragePolicy: (storagePolicy: string) => void; + owner: string; + setOwner: (owner: string) => void; + name: string; + setName: (name: string) => void; + clearFilters: () => void; +} + +const FileFilterPopover = ({ + storagePolicy, + setStoragePolicy, + owner, + setOwner, + name, + setName, + clearFilters, + onClose, + open, + ...rest +}: FileFilterPopoverProps) => { + const { t } = useTranslation("dashboard"); + + // Create local state to track changes before applying + const [localStoragePolicy, setLocalStoragePolicy] = useState(storagePolicy); + const [localOwner, setLocalOwner] = useState(owner); + const [localName, setLocalName] = useState(name); + + // Initialize local state when popup opens + useEffect(() => { + if (open) { + setLocalStoragePolicy(storagePolicy); + setLocalOwner(owner); + setLocalName(name); + } + }, [open]); + + // Apply filters and close popover + const handleApplyFilters = () => { + setStoragePolicy(localStoragePolicy); + setOwner(localOwner); + setName(localName); + onClose?.({}, "backdropClick"); + }; + + // Reset filters and close popover + const handleResetFilters = () => { + setLocalStoragePolicy(""); + setLocalOwner(""); + setLocalName(""); + clearFilters(); + onClose?.({}, "backdropClick"); + }; + + return ( + + + + setLocalOwner(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalName(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalStoragePolicy(value.toString())} + emptyValue={-1} + emptyText={t("user.all")} + /> + + + + + + + + + ); +}; + +export default FileFilterPopover; diff --git a/src/component/Admin/File/FileRow.tsx b/src/component/Admin/File/FileRow.tsx new file mode 100644 index 0000000..3c1f56c --- /dev/null +++ b/src/component/Admin/File/FileRow.tsx @@ -0,0 +1,235 @@ +import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow, Tooltip } from "@mui/material"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { batchDeleteFiles, getFileUrl } from "../../../api/api"; +import { File } from "../../../api/dashboard"; +import { FileType, Metadata } from "../../../api/explorer"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { sizeToString } from "../../../util"; +import { NoWrapTableCell, NoWrapTypography } from "../../Common/StyledComponents"; +import TimeBadge from "../../Common/TimeBadge"; +import UserAvatar from "../../Common/User/UserAvatar"; +import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon"; +import UploadingTag from "../../FileManager/Explorer/UploadingTag"; +import Delete from "../../Icons/Delete"; +import LinkIcon from "../../Icons/LinkOutlined"; +import Open from "../../Icons/Open"; +import Share from "../../Icons/Share"; + +export interface FileRowProps { + file?: File; + loading?: boolean; + deleting?: boolean; + selected?: boolean; + onDelete?: () => void; + onDetails?: (id: number) => void; + onSelect?: (id: number) => void; + openUserDialog?: (id: number) => void; +} + +const FileRow = ({ + file, + loading, + deleting, + selected, + onDelete, + onDetails, + onSelect, + openUserDialog, +}: FileRowProps) => { + const navigate = useNavigate(); + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [deleteLoading, setDeleteLoading] = useState(false); + const [openLoading, setOpenLoading] = useState(false); + const onRowClick = () => { + onDetails?.(file?.id ?? 0); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(confirmOperation(t("file.confirmDelete", { file: file?.name }))).then(() => { + if (file?.id) { + setDeleteLoading(true); + dispatch(batchDeleteFiles({ ids: [file.id] })) + .then(() => { + onDelete?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + } + }); + }; + + const onOpenClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setOpenLoading(true); + var fileLink = window.open("", "_blank"); + fileLink?.document.write("Loading file URL..."); + dispatch(getFileUrl(file?.id ?? 0)) + .then((url) => { + fileLink ? (fileLink.location.href = url) : window.open(url, "_blank"); + }) + .finally(() => { + setOpenLoading(false); + }) + .catch(() => { + fileLink && fileLink.close(); + }); + }; + + const onSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onSelect?.(file?.id ?? 0); + }; + + const uploading = useMemo(() => { + return ( + file?.edges?.metadata && file?.edges?.metadata.some((metadata) => metadata.name === Metadata.upload_session_id) + ); + }, [file?.edges?.metadata]); + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const userClicked = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + openUserDialog?.(file?.owner_id ?? 0); + }; + + const sizeUsed = useMemo(() => { + return sizeToString(file?.edges?.entities?.reduce((acc, entity) => acc + (entity.size ?? 0), 0) ?? 0); + }, [file?.edges?.entities]); + + return ( + + + + + + {file?.id} + + + + + {file?.name} + + {uploading && } + {file?.edges?.direct_links?.length && ( + + + + + + )} + {file?.edges?.shares?.length && ( + + + + + + )} + + + + + {sizeToString(file?.size ?? 0)} + + + {sizeUsed} + + + + + + + {file?.edges?.owner?.nick} + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default FileRow; diff --git a/src/component/Admin/File/FileSetting.tsx b/src/component/Admin/File/FileSetting.tsx new file mode 100644 index 0000000..dae72b9 --- /dev/null +++ b/src/component/Admin/File/FileSetting.tsx @@ -0,0 +1,326 @@ +import { Delete } from "@mui/icons-material"; +import { + Badge, + Box, + Button, + Checkbox, + Container, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { batchDeleteFiles, getFlattenFileList } from "../../../api/api"; +import { File } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import ArrowSync from "../../Icons/ArrowSync"; +import Filter from "../../Icons/Filter"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import UserDialog from "../User/UserDialog/UserDialog"; +import FileDialog from "./FileDialog/FileDialog"; +import FileFilterPopover from "./FileFilterPopover"; +import FileRow from "./FileRow"; + +export const StoragePolicyQuery = "storage_policy"; +export const OwnerQuery = "owner"; +export const NameQuery = "name"; + +const FileSetting = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [files, setFiles] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [storagePolicy, setStoragePolicy] = useQueryState(StoragePolicyQuery, { defaultValue: "" }); + const [owner, setOwner] = useQueryState(OwnerQuery, { defaultValue: "" }); + const [name, setName] = useQueryState(NameQuery, { defaultValue: "" }); + const [count, setCount] = useState(0); + const [selected, setSelected] = useState([]); + const [createNewOpen, setCreateNewOpen] = useState(false); + const filterPopupState = usePopupState({ + variant: "popover", + popupId: "userFilterPopover", + }); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(undefined); + const [fileDialogOpen, setFileDialogOpen] = useState(false); + const [fileDialogID, setFileDialogID] = useState(undefined); + const [deleteLoading, setDeleteLoading] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 11; + + const clearFilters = useCallback(() => { + setStoragePolicy(""); + setOwner(""); + setName(""); + }, [setStoragePolicy, setOwner, setName]); + + useEffect(() => { + fetchFiles(); + }, [page, pageSize, orderBy, orderDirection, storagePolicy, owner, name]); + + const fetchFiles = () => { + setLoading(true); + setSelected([]); + dispatch( + getFlattenFileList({ + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: { + file_policy: storagePolicy, + file_user: owner, + file_name: name, + }, + }), + ) + .then((res) => { + setFiles(res.files); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleDelete = () => { + setDeleteLoading(true); + dispatch(confirmOperation(t("file.confirmBatchDelete", { num: selected.length }))) + .then(() => { + dispatch(batchDeleteFiles({ ids: Array.from(selected) })) + .then(() => { + fetchFiles(); + }) + .finally(() => { + setDeleteLoading(false); + }); + }) + .finally(() => { + setDeleteLoading(false); + }); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = files.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleSelect = useCallback( + (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); + } + setSelected(newSelected); + }, + [selected], + ); + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + const hasActiveFilters = useMemo(() => { + return !!(storagePolicy || owner || name); + }, [storagePolicy, owner, name]); + + const handleFileDialogOpen = (id: number) => { + setFileDialogID(id); + setFileDialogOpen(true); + }; + + const handleUserDialogOpen = (id: number) => { + setUserDialogID(id); + setUserDialogOpen(true); + }; + + return ( + + {/* setCreateNewOpen(false)} + onCreated={(user) => { + setUserDialogID(user.id); + setUserDialogOpen(true); + }} + /> */} + setFileDialogOpen(false)} + fileID={fileDialogID} + onUpdated={(file) => { + setFileDialogID(file.id); + setFileDialogOpen(true); + }} + /> + setUserDialogOpen(false)} userID={userDialogID} /> + + + + + + }> + {t("node.refresh")} + + + + } variant="contained" {...bindTrigger(filterPopupState)}> + {t("user.filter")} + + + + {selected.length > 0 && !isMobile && ( + <> + + + + )} + + {isMobile && selected.length > 0 && ( + + + + )} + + + + + + 0 && selected.length < files.length} + checked={files.length > 0 && selected.length === files.length} + onChange={handleSelectAllClick} + /> + + + + {t("group.#")} + + + + + {t("file.name")} + + + + + {t("file.size")} + + + {t("file.sizeUsed")} + {t("file.uploader")} + + + {t("file.createdAt")} + + + + + + + {!loading && + files.map((file) => ( + + ))} + {loading && + files.length > 0 && + files.slice(0, 10).map((file) => )} + {loading && + files.length === 0 && + Array.from(Array(10)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default FileSetting; diff --git a/src/component/Admin/File/Import.js b/src/component/Admin/File/Import.js deleted file mode 100644 index 8d4f07e..0000000 --- a/src/component/Admin/File/Import.js +++ /dev/null @@ -1,481 +0,0 @@ -import { Dialog } from "@material-ui/core"; -import Button from "@material-ui/core/Button"; -import Chip from "@material-ui/core/Chip"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Fade from "@material-ui/core/Fade"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Paper from "@material-ui/core/Paper"; -import Popper from "@material-ui/core/Popper"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Switch from "@material-ui/core/Switch"; -import Typography from "@material-ui/core/Typography"; -import Alert from "@material-ui/lab/Alert"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import PathSelector from "../../FileManager/PathSelector"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - userSelect: { - width: 400, - borderRadius: 0, - }, -})); - -function useDebounce(value, delay) { - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - return () => { - clearTimeout(handler); - }; - }, [value]); - - return debouncedValue; -} - -export default function Import() { - const { t } = useTranslation("dashboard", { keyPrefix: "file" }); - const { t: tCommon } = useTranslation("common"); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - policy: 1, - userInput: "", - src: "", - dst: "", - recursive: true, - }); - const [anchorEl, setAnchorEl] = useState(null); - const [policies, setPolicies] = useState({}); - const [users, setUsers] = useState([]); - const [user, setUser] = useState(null); - const [selectRemote, setSelectRemote] = useState(false); - const [selectLocal, setSelectLocal] = useState(false); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const handleCheckChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.checked, - }); - }; - - const history = useHistory(); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submit = (e) => { - e.preventDefault(); - if (user === null) { - ToggleSnackbar("top", "right", t("selectTargetUser"), "warning"); - return; - } - setLoading(true); - API.post("/admin/task/import", { - uid: user.ID, - policy_id: parseInt(options.policy), - src: options.src, - dst: options.dst, - recursive: options.recursive, - }) - .then(() => { - setLoading(false); - history.push("/admin/file"); - ToggleSnackbar( - "top", - "right", - t("importTaskCreated"), - "success" - ); - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const debouncedSearchTerm = useDebounce(options.userInput, 500); - - useEffect(() => { - if (debouncedSearchTerm !== "") { - API.post("/admin/user/list", { - page: 1, - page_size: 10000, - order_by: "id asc", - searches: { - nick: debouncedSearchTerm, - email: debouncedSearchTerm, - }, - }) - .then((response) => { - setUsers(response.data.items); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - } - }, [debouncedSearchTerm]); - - useEffect(() => { - API.post("/admin/policy/list", { - page: 1, - page_size: 10000, - order_by: "id asc", - conditions: {}, - }) - .then((response) => { - const res = {}; - response.data.items.forEach((v) => { - res[v.ID] = v; - }); - setPolicies(res); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const selectUser = (u) => { - setOptions({ - ...options, - userInput: "", - }); - setUser(u); - }; - - const setMoveTarget = (setter) => (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setter(path === "//" ? "/" : path); - }; - - const openPathSelector = (isSrcSelect) => { - if (isSrcSelect) { - if ( - !policies[options.policy] || - policies[options.policy].Type === "local" || - policies[options.policy].Type === "remote" - ) { - ToggleSnackbar( - "top", - "right", - t("manuallyPathOnly"), - "warning" - ); - return; - } - setSelectRemote(true); - } else { - if (user === null) { - ToggleSnackbar( - "top", - "right", - t("selectTargetUser"), - "warning" - ); - return; - } - setSelectLocal(true); - } - }; - - return ( -
- setSelectRemote(false)} - aria-labelledby="form-dialog-title" - > - - {t("selectFolder")} - - - setOptions({ - ...options, - src: p, - }) - )} - /> - - - - - - setSelectLocal(false)} - aria-labelledby="form-dialog-title" - > - - {t("selectFolder")} - - - setOptions({ - ...options, - dst: p, - }) - )} - /> - - - - - -
-
- - {t("importExternalFolder")} - -
-
- - {t("importExternalFolderDes")} - -
-
- - - {t("storagePolicy")} - - - - {t("storagePolicyDes")} - - -
-
- - - {t("targetUser")} - - { - handleChange("userInput")(e); - setAnchorEl(e.currentTarget); - }} - startAdornment={ - user !== null && ( - - { - setUser(null); - }} - label={user.Nick} - /> - - ) - } - disabled={user !== null} - /> - 0 - } - anchorEl={anchorEl} - placement={"bottom"} - transition - > - {({ TransitionProps }) => ( - - - {users.map((u) => ( - - selectUser(u) - } - > - {u.Nick}{" "} - {"<" + u.Email + ">"} - - ))} - - - )} - - - {t("targetUserDes")} - - -
- -
- - - {t("srcFolderPath")} - - - { - handleChange("src")(e); - setAnchorEl(e.currentTarget); - }} - required - endAdornment={ - - } - /> - - - {t("selectSrcDes")} - - -
- -
- - - {t("dstFolderPath")} - - - { - handleChange("dst")(e); - setAnchorEl(e.currentTarget); - }} - required - endAdornment={ - - } - /> - - - {t("dstFolderPathDes")} - - -
- -
- - - } - label={t("recursivelyImport")} - /> - - {t("recursivelyImportDes")} - - -
-
-
- -
- -
-
-
- ); -} diff --git a/src/component/Admin/Group/EditGroup.js b/src/component/Admin/Group/EditGroup.js deleted file mode 100644 index 8fc557b..0000000 --- a/src/component/Admin/Group/EditGroup.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import API from "../../../middleware/Api"; -import { useDispatch } from "react-redux"; -import GroupForm from "./GroupForm"; -import { toggleSnackbar } from "../../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -export default function EditGroupPreload() { - const { t } = useTranslation("dashboard", { keyPrefix: "group" }); - const [group, setGroup] = useState({}); - - const { id } = useParams(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - setGroup({}); - API.get("/admin/group/" + id) - .then((response) => { - // 布尔值转换 - ["ShareEnabled", "WebDAVEnabled"].forEach((v) => { - response.data[v] = response.data[v] ? "true" : "false"; - }); - [ - "archive_download", - "archive_task", - "one_time_download", - "share_download", - "webdav_proxy", - "aria2", - "redirected_source", - "advance_delete" - ].forEach((v) => { - if (response.data.OptionsSerialized[v] !== undefined) { - response.data.OptionsSerialized[v] = response.data - .OptionsSerialized[v] - ? "true" - : "false"; - } - }); - - // 整型转换 - ["MaxStorage", "SpeedLimit"].forEach((v) => { - response.data[v] = response.data[v].toString(); - }); - [ - "compress_size", - "decompress_size", - "source_batch", - "aria2_batch", - ].forEach((v) => { - if (response.data.OptionsSerialized[v] !== undefined) { - response.data.OptionsSerialized[ - v - ] = response.data.OptionsSerialized[v].toString(); - } - }); - response.data.PolicyList = response.data.PolicyList[0]; - - // JSON转换 - if ( - response.data.OptionsSerialized.aria2_options === undefined - ) { - response.data.OptionsSerialized.aria2_options = "{}"; - } else { - try { - response.data.OptionsSerialized.aria2_options = JSON.stringify( - response.data.OptionsSerialized.aria2_options - ); - } catch (e) { - ToggleSnackbar( - "top", - "right", - t("aria2FormatError"), - "warning" - ); - return; - } - } - setGroup(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, [id]); - - return
{group.ID !== undefined && }
; -} diff --git a/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx b/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx new file mode 100644 index 0000000..e760653 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/BasicInfoSection.tsx @@ -0,0 +1,116 @@ +import { Alert, FormControl, FormControlLabel, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { GroupEnt, StoragePolicy } from "../../../../api/dashboard"; +import { GroupPermission } from "../../../../api/user"; +import Boolset from "../../../../util/boolset"; +import SizeInput from "../../../Common/SizeInput"; +import { DenseFilledTextField } from "../../../Common/StyledComponents"; +import InPrivate from "../../../Icons/InPrivate"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { AnonymousGroupID } from "../GroupRow"; +import { GroupSettingContext } from "./GroupSettingWrapper"; +import PolicySelectionInput from "./PolicySelectionInput"; +const BasicInfoSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setGroup } = useContext(GroupSettingContext); + + const permission = useMemo(() => { + return new Boolset(values.permissions ?? ""); + }, [values.permissions]); + + const onNameChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ ...p, name: e.target.value })); + }, + [setGroup], + ); + + const onPolicyChange = useCallback( + (value: number) => { + setGroup((p: GroupEnt) => ({ + ...p, + edges: { ...p.edges, storage_policies: { id: value } as StoragePolicy }, + })); + }, + [setGroup], + ); + + const onMaxStorageChange = useCallback( + (size: number) => { + setGroup((p: GroupEnt) => ({ + ...p, + max_storage: size ? size : undefined, + })); + }, + [setGroup], + ); + + const onIsAdminChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.is_admin, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + return ( + + + {t("policy.basicInfo")} + + + {values?.id == AnonymousGroupID && ( + + } severity="info"> + {t("group.anonymousHint")} + + + )} + + + + {t("group.nameOfGroupDes")} + + + {values?.id != AnonymousGroupID && ( + <> + + + {t("group.availablePoliciesDes")} + + {t("group.availablePolicyDesPro")} + + + + + + {t("group.initialStorageQuotaDes")} + + + + + } + label={t("group.isAdmin")} + /> + {t("group.isAdminDes")} + + + + )} + + + ); +}; + +export default BasicInfoSection; diff --git a/src/component/Admin/Group/EditGroup/EditGroup.tsx b/src/component/Admin/Group/EditGroup/EditGroup.tsx new file mode 100644 index 0000000..cf8307e --- /dev/null +++ b/src/component/Admin/Group/EditGroup/EditGroup.tsx @@ -0,0 +1,27 @@ +import { Container } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import PageContainer from "../../../Pages/PageContainer"; +import PageHeader from "../../../Pages/PageHeader"; +import GroupForm from "./GroupForm"; +import GroupSettingWrapper from "./GroupSettingWrapper"; + +const EditGroup = () => { + const { t } = useTranslation("dashboard"); + const { id } = useParams(); + const [name, setName] = useState(""); + + return ( + + + + setName(p.name)}> + + + + + ); +}; + +export default EditGroup; diff --git a/src/component/Admin/Group/EditGroup/FileManagementSection.tsx b/src/component/Admin/Group/EditGroup/FileManagementSection.tsx new file mode 100644 index 0000000..d0a5574 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/FileManagementSection.tsx @@ -0,0 +1,388 @@ +import { + Box, + CircularProgress, + Collapse, + FormControl, + FormControlLabel, + Link, + Stack, + Switch, + Typography, + useTheme, +} from "@mui/material"; +import { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { GroupEnt } from "../../../../api/dashboard"; +import { GroupPermission } from "../../../../api/user"; +import Boolset from "../../../../util/boolset"; +import SizeInput from "../../../Common/SizeInput"; +import { DenseFilledTextField } from "../../../Common/StyledComponents"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm"; +import ProDialog from "../../Common/ProDialog"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { AnonymousGroupID } from "../GroupRow"; +import { GroupSettingContext } from "./GroupSettingWrapper"; +import MultipleNodeSelectionInput from "./MultipleNodeSelectionInput"; + +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor")); + +const FileManagementSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setGroup } = useContext(GroupSettingContext); + const [proOpen, setProOpen] = useState(false); + const theme = useTheme(); + + const [editedConfig, setEditedConfig] = useState(""); + + const permission = useMemo(() => { + return new Boolset(values.permissions ?? ""); + }, [values.permissions]); + + useEffect(() => { + setEditedConfig( + values.settings?.remote_download_options ? JSON.stringify(values.settings?.remote_download_options, null, 2) : "", + ); + }, [values.settings?.remote_download_options]); + + const onAllowWabDAVChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.webdav, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onAllowWabDAVProxyChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.webdav_proxy, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onAllowCompressTaskChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.archive_task, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onCompressSizeChange = useCallback( + (e: number) => { + setGroup((p: GroupEnt) => ({ ...p, settings: { ...p.settings, compress_size: e ? e : undefined } })); + }, + [setGroup], + ); + + const onDecompressSizeChange = useCallback( + (e: number) => { + setGroup((p: GroupEnt) => ({ ...p, settings: { ...p.settings, decompress_size: e ? e : undefined } })); + }, + [setGroup], + ); + + const onAllowRemoteDownloadChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.remote_download, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onEditedConfigBlur = useCallback( + (value: string) => { + var res: Record | undefined = undefined; + if (value) { + try { + res = JSON.parse(value); + } catch (e) { + console.error(e); + } + } + setGroup((p: GroupEnt) => ({ ...p, settings: { ...p.settings, remote_download_options: res } })); + }, + [editedConfig, setGroup], + ); + + const onAria2BatchSizeChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, aria2_batch: parseInt(e.target.value) ? parseInt(e.target.value) : undefined }, + })); + }, + [setGroup], + ); + + const onAllowAdvanceDeleteChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.advance_delete, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onMaxWalkedFilesChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, max_walked_files: parseInt(e.target.value) ? parseInt(e.target.value) : undefined }, + })); + }, + [setGroup], + ); + + const onTrashBinDurationChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, trash_retention: parseInt(e.target.value) ? parseInt(e.target.value) : undefined }, + })); + }, + [setGroup], + ); + + const onProClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setProOpen(true); + }, []); + + return ( + + setProOpen(false)} /> + + {t("group.fileManagement")} + + + {values?.id != AnonymousGroupID && ( + <> + + + + } + label={t("group.allowWabDAV")} + /> + {t("group.allowWabDAVDes")} + + + + + + + } + label={t("group.allowWabDAVProxy")} + /> + {t("group.allowWabDAVProxyDes")} + + + + + + } + label={ + + {t("group.migratePolicy")} + + + } + /> + {t("group.migratePolicyDes")} + + + + + + } + label={t("group.compressTask")} + /> + {t("group.compressTaskDes")} + + + + + + + + {t("group.compressSizeDes")} + + + + + + {t("group.decompressSizeDes")} + + + + + + + + } + label={t("group.allowRemoteDownload")} + /> + + ]} + /> + + + + + + + + }> + setEditedConfig(value || "")} + onBlur={onEditedConfigBlur} + height="200px" + minHeight="200px" + options={{ + wordWrap: "on", + minimap: { enabled: false }, + scrollBeyondLastLine: false, + }} + /> + + {t("group.aria2OptionsDes")} + + + + + + {t("group.aria2BatchSizeDes")} + + + + + + + + } + label={t("group.advanceDelete")} + /> + {t("group.advanceDeleteDes")} + + + + + + {t("group.allowedNodesDes")} + + + + + } + label={ + + {t("group.allowSelectNode")} + + + } + /> + {t("group.allowSelectNodeDes")} + + + + )} + + + + + {t("group.maxWalkedFilesDes")} + + + {values?.id != AnonymousGroupID && ( + + + + {t("group.trashBinDurationDes")} + + + )} + + + ); +}; + +export default FileManagementSection; diff --git a/src/component/Admin/Group/EditGroup/GroupForm.tsx b/src/component/Admin/Group/EditGroup/GroupForm.tsx new file mode 100644 index 0000000..5fbfce3 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/GroupForm.tsx @@ -0,0 +1,25 @@ +import { Box, Stack } from "@mui/material"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import BasicInfoSection from "./BasicInfoSection"; +import FileManagementSection from "./FileManagementSection"; +import { GroupSettingContext } from "./GroupSettingWrapper"; +import ShareSection from "./ShareSection"; +import UploadDownloadSection from "./UploadDownloadSection"; + +const GroupForm = () => { + const { t } = useTranslation("dashboard"); + const { formRef, values } = useContext(GroupSettingContext); + return ( + e.preventDefault()}> + + + + + + + + ); +}; + +export default GroupForm; diff --git a/src/component/Admin/Group/EditGroup/GroupSettingWrapper.tsx b/src/component/Admin/Group/EditGroup/GroupSettingWrapper.tsx new file mode 100644 index 0000000..1658f81 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/GroupSettingWrapper.tsx @@ -0,0 +1,145 @@ +import { Box } from "@mui/material"; +import * as React from "react"; +import { createContext, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getGroupDetail, upsertGroup } from "../../../../api/api.ts"; +import { GroupEnt, StoragePolicy } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import { SavingFloat } from "../../Settings/SettingWrapper.tsx"; + +export interface GroupSettingWrapperProps { + groupID: number; + children: React.ReactNode; + onGroupChange: (group: GroupEnt) => void; +} + +export interface GroupSettingContextProps { + values: GroupEnt; + setGroup: (f: (p: GroupEnt) => GroupEnt) => void; + formRef?: React.RefObject; +} + +const defaultGroup: GroupEnt = { + id: 0, + name: "", + edges: {}, +}; + +export const GroupSettingContext = createContext({ + values: { ...defaultGroup }, + setGroup: () => {}, +}); + +const groupValueFilter = (group: GroupEnt): GroupEnt => { + return { + ...group, + edges: { + storage_policies: { + id: group.edges.storage_policies?.id ?? 0, + } as StoragePolicy, + }, + }; +}; + +const GroupSettingWrapper = ({ groupID, children, onGroupChange }: GroupSettingWrapperProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ + ...defaultGroup, + }); + const [modifiedValues, setModifiedValues] = useState({ + ...defaultGroup, + }); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + useEffect(() => { + setLoading(true); + dispatch(getGroupDetail(groupID)) + .then((res) => { + setValues(groupValueFilter(res)); + setModifiedValues(groupValueFilter(res)); + onGroupChange(groupValueFilter(res)); + }) + .finally(() => { + setLoading(false); + }); + }, [groupID]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + setSubmitting(true); + dispatch( + upsertGroup({ + group: { ...modifiedValues }, + }), + ) + .then((res) => { + setValues(groupValueFilter(res)); + setModifiedValues(groupValueFilter(res)); + onGroupChange(groupValueFilter(res)); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + {children} + + + )} + + + + + ); +}; + +export default GroupSettingWrapper; diff --git a/src/component/Admin/Group/EditGroup/MultipleNodeSelectionInput.tsx b/src/component/Admin/Group/EditGroup/MultipleNodeSelectionInput.tsx new file mode 100644 index 0000000..2be4da1 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/MultipleNodeSelectionInput.tsx @@ -0,0 +1,41 @@ +import { ListItemText } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { DenseSelect } from "../../../Common/StyledComponents"; + +const MultipleNodeSelectionInput = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + + return ( + { + return ( + {t("group.allNodes")}} + slotProps={{ + primary: { color: "textSecondary", variant: "body2" }, + }} + /> + ); + }} + > + ); +}; + +export default MultipleNodeSelectionInput; diff --git a/src/component/Admin/Group/EditGroup/PolicySelectionInput.tsx b/src/component/Admin/Group/EditGroup/PolicySelectionInput.tsx new file mode 100644 index 0000000..ad85832 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/PolicySelectionInput.tsx @@ -0,0 +1,102 @@ +import { Box, FormControl, SelectChangeEvent, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getStoragePolicyList } from "../../../../api/api"; +import { StoragePolicy } from "../../../../api/dashboard"; +import { useAppDispatch } from "../../../../redux/hooks"; +import FacebookCircularProgress from "../../../Common/CircularProgress"; +import { DenseSelect, SquareChip } from "../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu"; +export interface PolicySelectionInputProps { + value: number; + onChange: (value: number) => void; +} + +const PolicySelectionInput = ({ value, onChange }: PolicySelectionInputProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(false); + const [policyMap, setPolicyMap] = useState>({}); + + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event; + onChange(value as number); + }; + + useEffect(() => { + setLoading(true); + dispatch(getStoragePolicyList({ page: 1, page_size: 1000, order_by: "id", order_direction: "asc" })) + .then((res) => { + setPolicies(res.policies); + setPolicyMap( + res.policies.reduce( + (acc, policy) => { + acc[policy.id] = policy; + return acc; + }, + {} as Record, + ), + ); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( + + ( + + {!loading ? ( + + ) : ( + + )} + + )} + > + {policies.length > 0 && + policies.map((policy) => ( + + + + {policy.name} + + + {t(`policy.${policy.type}`)} + + + + ))} + + + ); +}; + +export default PolicySelectionInput; diff --git a/src/component/Admin/Group/EditGroup/ShareSection.tsx b/src/component/Admin/Group/EditGroup/ShareSection.tsx new file mode 100644 index 0000000..840cd5f --- /dev/null +++ b/src/component/Admin/Group/EditGroup/ShareSection.tsx @@ -0,0 +1,113 @@ +import { Box, FormControl, FormControlLabel, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { GroupEnt } from "../../../../api/dashboard"; +import { GroupPermission } from "../../../../api/user"; +import Boolset from "../../../../util/boolset"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm"; +import ProDialog from "../../Common/ProDialog"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { AnonymousGroupID } from "../GroupRow"; +import { GroupSettingContext } from "./GroupSettingWrapper"; + +const ShareSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setGroup } = useContext(GroupSettingContext); + const [proOpen, setProOpen] = useState(false); + + const permission = useMemo(() => { + return new Boolset(values.permissions ?? ""); + }, [values.permissions]); + + const onAllowCreateShareLinkChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.share, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onShareDownloadChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.share_download, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onProClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setProOpen(true); + }; + + return ( + + setProOpen(false)} /> + + {t("group.share")} + + + {values?.id != AnonymousGroupID && ( + + + + } + label={t("group.allowCreateShareLink")} + /> + {t("group.allowCreateShareLinkDes")} + + + )} + + + } + label={ + + {t("group.shareFree")} + + + } + /> + {t("group.shareFreeDes")} + + + + + + } + label={t("group.allowDownloadShare")} + /> + {t("group.allowDownloadShareDes")} + + + {values?.id != AnonymousGroupID && ( + + + } + label={ + + {t("group.esclateAnonymity")} + + + } + /> + {t("group.esclateAnonymityDes")} + + + )} + + + ); +}; + +export default ShareSection; diff --git a/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx b/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx new file mode 100644 index 0000000..9a58974 --- /dev/null +++ b/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx @@ -0,0 +1,151 @@ +import { Collapse, FormControl, FormControlLabel, Stack, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { GroupEnt } from "../../../../api/dashboard"; +import { GroupPermission } from "../../../../api/user"; +import Boolset from "../../../../util/boolset"; +import SizeInput from "../../../Common/SizeInput"; +import { DenseFilledTextField } from "../../../Common/StyledComponents"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { GroupSettingContext } from "./GroupSettingWrapper"; + +const UploadDownloadSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setGroup } = useContext(GroupSettingContext); + + const permission = useMemo(() => { + return new Boolset(values.permissions ?? ""); + }, [values.permissions]); + + const onAllowArchiveDownloadChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.archive_download, e.target.checked).toString(), + })); + }, + [setGroup], + ); + + const onAllowDirectLinkChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { + ...p.settings, + source_batch: e.target.checked ? 1 : 0, + }, + })); + }, + [setGroup], + ); + + const onSourceBatchChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, source_batch: parseInt(e.target.value) ? parseInt(e.target.value) : undefined }, + })); + }, + [setGroup], + ); + + const onRedirectedSourceChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, redirected_source: e.target.checked ? true : undefined }, + })); + }, + [setGroup], + ); + + const onDownloadSpeedLimitChange = useCallback( + (e: number) => { + setGroup((p: GroupEnt) => ({ ...p, speed_limit: e ? e : undefined })); + }, + [setGroup], + ); + + return ( + + + {t("group.uploadDownload")} + + + + + + } + label={t("group.serverSideBatchDownload")} + /> + {t("group.serverSideBatchDownloadDes")} + + + + + 0} onChange={onAllowDirectLinkChange} /> + } + label={t("group.getDirectLink")} + /> + {t("group.getDirectLinkDes")} + + + 0} unmountOnExit> + + + + + {t("group.bathSourceLinkLimitDes")} + + + + + + } + label={t("group.redirectedSource")} + /> + {t("group.redirectedSourceDes")} + + + + + + + + {t("group.downloadSpeedLimitDes")} + + + + + ); +}; + +export default UploadDownloadSection; diff --git a/src/component/Admin/Group/Group.js b/src/component/Admin/Group/Group.js deleted file mode 100644 index 333bcdf..0000000 --- a/src/component/Admin/Group/Group.js +++ /dev/null @@ -1,252 +0,0 @@ -import Button from "@material-ui/core/Button"; -import IconButton from "@material-ui/core/IconButton"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import Tooltip from "@material-ui/core/Tooltip"; -import { Delete, Edit } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory, useLocation } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { sizeToString } from "../../../utils"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, -})); - -const columns = [ - { id: "#", minWidth: 50 }, - { id: "name", minWidth: 170 }, - { id: "type", label: "存储策略", minWidth: 170 }, - { - id: "count", - minWidth: 50, - align: "right", - }, - { - id: "size", - minWidth: 100, - align: "right", - }, - { - id: "action", - minWidth: 170, - align: "right", - }, -]; - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -export default function Group() { - const { t } = useTranslation("dashboard", { keyPrefix: "group" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [groups, setGroups] = useState([]); - const [statics, setStatics] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [policies, setPolicies] = React.useState({}); - - const location = useLocation(); - const history = useHistory(); - const query = useQuery(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/group/list", { - page: page, - page_size: pageSize, - order_by: "id desc", - }) - .then((response) => { - setGroups(response.data.items); - setStatics(response.data.statics); - setTotal(response.data.total); - setPolicies(response.data.policies); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - if (query.get("code") === "0") { - ToggleSnackbar("top", "right", "授权成功", "success"); - } else if (query.get("msg") && query.get("msg") !== "") { - ToggleSnackbar( - "top", - "right", - query.get("msg") + ", " + query.get("err"), - "warning" - ); - } - }, [location]); - - useEffect(() => { - loadList(); - }, [page, pageSize]); - - const deletePolicy = (id) => { - API.delete("/admin/group/" + id) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - return ( -
-
- -
- -
-
- - - - - - - {columns.map((column) => ( - - {t(column.id)} - - ))} - - - - {groups.map((row) => ( - - {row.ID} - {row.Name} - - {row.PolicyList !== null && - row.PolicyList.map((pid, key) => { - let res = ""; - if (policies[pid]) { - res += policies[pid].Name; - } - if ( - key !== - row.PolicyList.length - 1 - ) { - res += " / "; - } - return res; - })} - - - {statics[row.ID] !== undefined && - statics[row.ID].toLocaleString()} - - - {statics[row.ID] !== undefined && - sizeToString(row.MaxStorage)} - - - - - history.push( - "/admin/group/edit/" + - row.ID - ) - } - size={"small"} - > - - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/Group/GroupForm.js b/src/component/Admin/Group/GroupForm.js deleted file mode 100644 index 34d8f4e..0000000 --- a/src/component/Admin/Group/GroupForm.js +++ /dev/null @@ -1,694 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Switch from "@material-ui/core/Switch"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import SizeInput from "../Common/SizeInput"; -import { Trans, useTranslation } from "react-i18next"; -import { Link } from "@material-ui/core"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -// function getStyles(name, personName, theme) { -// return { -// fontWeight: -// personName.indexOf(name) === -1 -// ? theme.typography.fontWeightRegular -// : theme.typography.fontWeightMedium -// }; -// } - -export default function GroupForm(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "group" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [group, setGroup] = useState( - props.group - ? props.group - : { - ID: 0, - Name: "", - MaxStorage: "1073741824", // 转换类型 - ShareEnabled: "true", // 转换类型 - WebDAVEnabled: "true", // 转换类型 - SpeedLimit: "0", // 转换类型 - PolicyList: 1, // 转换类型,至少选择一个 - OptionsSerialized: { - // 批量转换类型 - share_download: "true", - aria2_options: "{}", // json decode - compress_size: "0", - decompress_size: "0", - source_batch: "0", - aria2_batch: "1", - }, - } - ); - const [policies, setPolicies] = useState({}); - - const history = useHistory(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/policy/list", { - page: 1, - page_size: 10000, - order_by: "id asc", - conditions: {}, - }) - .then((response) => { - const res = {}; - response.data.items.forEach((v) => { - res[v.ID] = v.Name; - }); - setPolicies(res); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - const handleChange = (name) => (event) => { - setGroup({ - ...group, - [name]: event.target.value, - }); - }; - - const handleCheckChange = (name) => (event) => { - const value = event.target.checked ? "true" : "false"; - setGroup({ - ...group, - [name]: value, - }); - }; - - const handleOptionCheckChange = (name) => (event) => { - const value = event.target.checked ? "true" : "false"; - setGroup({ - ...group, - OptionsSerialized: { - ...group.OptionsSerialized, - [name]: value, - }, - }); - }; - - const handleOptionChange = (name) => (event) => { - setGroup({ - ...group, - OptionsSerialized: { - ...group.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const submit = (e) => { - e.preventDefault(); - const groupCopy = { - ...group, - OptionsSerialized: { ...group.OptionsSerialized }, - }; - - // 布尔值转换 - ["ShareEnabled", "WebDAVEnabled"].forEach((v) => { - groupCopy[v] = groupCopy[v] === "true"; - }); - [ - "archive_download", - "archive_task", - "one_time_download", - "share_download", - "webdav_proxy", - "aria2", - "redirected_source", - "advance_delete" - ].forEach((v) => { - if (groupCopy.OptionsSerialized[v] !== undefined) { - groupCopy.OptionsSerialized[v] = - groupCopy.OptionsSerialized[v] === "true"; - } - }); - - // 整型转换 - ["MaxStorage", "SpeedLimit"].forEach((v) => { - groupCopy[v] = parseInt(groupCopy[v]); - }); - [ - "compress_size", - "decompress_size", - "source_batch", - "aria2_batch", - ].forEach((v) => { - if (groupCopy.OptionsSerialized[v] !== undefined) { - groupCopy.OptionsSerialized[v] = parseInt( - groupCopy.OptionsSerialized[v] - ); - } - }); - groupCopy.PolicyList = [parseInt(groupCopy.PolicyList)]; - // JSON转换 - try { - groupCopy.OptionsSerialized.aria2_options = JSON.parse( - groupCopy.OptionsSerialized.aria2_options - ); - } catch (e) { - ToggleSnackbar("top", "right", t("aria2FormatError"), "warning"); - return; - } - - setLoading(true); - API.post("/admin/group", { - group: groupCopy, - }) - .then(() => { - history.push("/admin/group"); - ToggleSnackbar( - "top", - "right", - props.group ? t("saved") : t("added"), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
-
-
- - {group.ID === 0 && t("new")} - {group.ID !== 0 && t("editGroup", { group: group.Name })} - - -
- {group.ID !== 3 && ( - <> -
- - - {t("nameOfGroup")} - - - - {t("nameOfGroupDes")} - - -
- -
- - - {t("storagePolicy")} - - - - {t("storageDes")} - - -
- -
- - - - - {t("initialStorageQuotaDes")} - -
- - )} - -
- - - - - {t("downloadSpeedLimitDes")} - -
- - {group.ID !== 3 && ( -
- - - {t("bathSourceLinkLimit")} - - - - {t("bathSourceLinkLimitDes")} - - -
- )} - - {group.ID !== 3 && ( -
- - - } - label={t("allowCreateShareLink")} - /> - - {t("allowCreateShareLinkDes")} - - -
- )} - -
- - - } - label={t("allowDownloadShare")} - /> - - {t("allowDownloadShareDes")} - - -
- - {group.ID !== 3 && ( -
- - - } - label={t("allowWabDAV")} - /> - - {t("allowWabDAVDes")} - - -
- )} - - {group.ID !== 3 && group.WebDAVEnabled === "true" && ( -
- - - } - label={t("allowWabDAVProxy")} - /> - - {t("allowWabDAVProxyDes")} - - -
- )} - -
- - - } - label={t("disableMultipleDownload")} - /> - - {t("disableMultipleDownloadDes")} - - -
- - {group.ID !== 3 && ( -
- - - } - label={t("allowRemoteDownload")} - /> - - {t("allowRemoteDownloadDes")} - - -
- )} - - -
- - - {t("aria2Options")} - - - - {t("aria2OptionsDes")} - - -
-
- - - {t("aria2BatchSize")} - - - - {t("aria2BatchSizeDes")} - - -
-
- -
- - - } - label={t("serverSideBatchDownload")} - /> - - {t("serverSideBatchDownloadDes")} - - -
- - {group.ID !== 3 && ( -
- - - } - label={t("compressTask")} - /> - - {t("compressTaskDes")} - - -
- )} - - -
- - - - - {t("compressSizeDes")} - -
- -
- - - - - {t("decompressSizeDes")} - -
-
- - {group.ID !== 3 && ( -
- - - } - label={t("redirectedSource")} - /> - - , - ]} - /> - - -
- )} - - {group.ID !== 3 && ( -
- - - } - label={t("advanceDelete")} - /> - - {t("advanceDeleteDes")} - - -
- )} -
-
-
- -
-
-
- ); -} diff --git a/src/component/Admin/Group/GroupRow.tsx b/src/component/Admin/Group/GroupRow.tsx new file mode 100644 index 0000000..be51218 --- /dev/null +++ b/src/component/Admin/Group/GroupRow.tsx @@ -0,0 +1,179 @@ +import { Box, IconButton, Link, Skeleton, TableRow, Tooltip } from "@mui/material"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { deleteGroup, getGroupDetail } from "../../../api/api"; +import { GroupEnt } from "../../../api/dashboard"; +import { GroupPermission } from "../../../api/user"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { sizeToString } from "../../../util"; +import Boolset from "../../../util/boolset"; +import { NoWrapBox, NoWrapTableCell, NoWrapTypography, SquareChip } from "../../Common/StyledComponents"; +import Delete from "../../Icons/Delete"; +import InPrivate from "../../Icons/InPrivate"; +import PersonPasskey from "../../Icons/PersonPasskey"; +import Shield from "../../Icons/Shield"; + +export interface GroupRowProps { + group?: GroupEnt; + loading?: boolean; + onDelete?: () => void; +} + +export const AnonymousGroupID = 3; + +const GroupRow = ({ group, loading, onDelete }: GroupRowProps) => { + const navigate = useNavigate(); + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [userCount, setUserCount] = useState(undefined); + const [countLoading, setCountLoading] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + + const onPolicyClick = + (policyId: number): ((e: React.MouseEvent) => void) => + (e) => { + e.stopPropagation(); + navigate(`/admin/policy/${policyId}`); + }; + + const onRowClick = () => { + navigate(`/admin/group/${group?.id}`); + }; + + const groupBs = useMemo(() => { + return new Boolset(group?.permissions); + }, [group?.permissions]); + + const onCountClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setCountLoading(true); + dispatch(getGroupDetail(group?.id ?? 0, true)) + .then((res) => { + setUserCount(res.total_users); + setCountLoading(false); + }) + .finally(() => { + setCountLoading(false); + }); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(confirmOperation(t("group.confirmDelete", { group: group?.name }))).then(() => { + if (group?.id) { + setDeleteLoading(true); + dispatch(deleteGroup(group.id)) + .then(() => { + onDelete?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + } + }); + }; + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } + return ( + + {group?.id} + + + {group?.name} + + {(group?.id ?? 0) <= 3 && ( + + + + + + )} + {(group?.id ?? 0) == AnonymousGroupID && ( + + + + + + )} + {groupBs.enabled(GroupPermission.is_admin) && ( + + + + + + )} + + + + + + {group?.edges.storage_policies && ( + + )} + + + + {countLoading ? ( + + ) : userCount != undefined ? ( + + {userCount} + + ) : ( + + {t("group.countUser")} + + )} + + {sizeToString(group?.max_storage ?? 0)} + + + + + + + ); +}; + +export default GroupRow; diff --git a/src/component/Admin/Group/GroupSetting.tsx b/src/component/Admin/Group/GroupSetting.tsx new file mode 100644 index 0000000..ed42724 --- /dev/null +++ b/src/component/Admin/Group/GroupSetting.tsx @@ -0,0 +1,152 @@ +import { useTheme } from "@emotion/react"; +import { + Box, + Button, + Container, + Stack, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + TableSortLabel, +} from "@mui/material"; +import { useQueryState } from "nuqs"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getGroupList } from "../../../api/api"; +import { GroupEnt } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import Add from "../../Icons/Add"; +import ArrowSync from "../../Icons/ArrowSync"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import GroupRow from "./GroupRow"; +import NewGroupDialog from "./NewGroupDIalog"; + +const GroupSetting = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [groups, setGroups] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [count, setCount] = useState(0); + const [createNewOpen, setCreateNewOpen] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 11; + + useEffect(() => { + fetchGroups(); + }, [page, pageSize, orderBy, orderDirection]); + + const fetchGroups = () => { + setLoading(true); + dispatch( + getGroupList({ + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + }), + ) + .then((res) => { + setGroups(res.groups); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + return ( + + setCreateNewOpen(false)} /> + + + + + }> + {t("node.refresh")} + + + + + + + + + {t("group.#")} + + + + + {t("group.name")} + + + {t("group.type")} + {t("group.count")} + + + {t("group.size")} + + + + + + + {!loading && groups.map((group) => )} + {loading && + groups.length > 0 && + groups.map((group) => )} + {loading && + groups.length === 0 && + Array.from(Array(5)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default GroupSetting; diff --git a/src/component/Admin/Group/NewGroupDIalog.tsx b/src/component/Admin/Group/NewGroupDIalog.tsx new file mode 100644 index 0000000..ea9431e --- /dev/null +++ b/src/component/Admin/Group/NewGroupDIalog.tsx @@ -0,0 +1,132 @@ +import { DialogContent, FormControl, Stack } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { upsertGroup } from "../../../api/api"; +import { GroupEnt } from "../../../api/dashboard"; +import { GroupPermission } from "../../../api/user"; +import { useAppDispatch } from "../../../redux/hooks"; +import Boolset from "../../../util/boolset"; +import { DenseFilledTextField } from "../../Common/StyledComponents"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import GroupSelectionInput from "../Common/GroupSelectionInput"; +import { NoMarginHelperText } from "../Settings/Settings"; + +export interface NewGroupDialogProps { + open: boolean; + onClose: () => void; +} + +const defaultGroupBs = new Boolset(""); +defaultGroupBs.sets({ + [GroupPermission.share]: true, + [GroupPermission.share_download]: true, + [GroupPermission.set_anonymous_permission]: true, +}); +const defaultGroup: GroupEnt = { + name: "", + permissions: defaultGroupBs.toString(), + max_storage: 1024 << 20, // 1GB + settings: { + compress_size: 1024 << 20, // 1MB + decompress_size: 1024 << 20, // 1MB + max_walked_files: 100000, + trash_retention: 7 * 24 * 3600, + source_batch: 10, + aria2_batch: 1, + redirected_source: true, + }, + edges: { + storage_policies: [], + }, + id: 0, +}; + +const NewGroupDialog = ({ open, onClose }: NewGroupDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [copyFrom, setCopyFrom] = useState("0"); + const [loading, setLoading] = useState(false); + const [group, setGroup] = useState({ ...defaultGroup }); + const formRef = useRef(null); + const copyFromSrc = useRef(undefined); + + useEffect(() => { + if (open) { + setGroup({ ...defaultGroup }); + setCopyFrom("0"); + copyFromSrc.current = undefined; + } + }, [open]); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + + let newGroup = { ...group }; + if (copyFrom != "0" && copyFromSrc.current) { + newGroup = { ...copyFromSrc.current, id: 0, name: group.name }; + } + + setLoading(true); + dispatch(upsertGroup({ group: newGroup })) + .then((r) => { + navigate(`/admin/group/${r.id}`); + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + +
+ + + setGroup({ ...group, name: e.target.value })} + /> + {t("policy.policyName")} + + + + { + copyFromSrc.current = g; + }} + emptyValue={"0"} + emptyText={"group.notCopy"} + /> + + + +
+
+
+ ); +}; + +export default NewGroupDialog; diff --git a/src/component/Admin/Home/Home.tsx b/src/component/Admin/Home/Home.tsx new file mode 100644 index 0000000..fe45f11 --- /dev/null +++ b/src/component/Admin/Home/Home.tsx @@ -0,0 +1,393 @@ +import Giscus from "@giscus/react"; +import { GitHub } from "@mui/icons-material"; +import { + Avatar, + Box, + Container, + Divider, + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + Skeleton, + styled, + Typography, +} from "@mui/material"; +import { blue, green, red, yellow } from "@mui/material/colors"; +import Grid from "@mui/material/Grid"; +import { useTheme } from "@mui/material/styles"; +import dayjs from "dayjs"; +import i18next from "i18next"; +import { useCallback, useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +import { getDashboardSummary } from "../../../api/api.ts"; +import { HomepageSummary } from "../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import { SecondaryButton, SquareChip } from "../../Common/StyledComponents.tsx"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import Book from "../../Icons/Book.tsx"; +import BoxMultipleFilled from "../../Icons/BoxMultipleFilled.tsx"; +import Discord from "../../Icons/Discord.tsx"; +import DocumentCopyFilled from "../../Icons/DocumentCopyFilled.tsx"; +import HomeIcon from "../../Icons/Home.tsx"; +import OpenFilled from "../../Icons/OpenFilled.tsx"; +import PeopleFilled from "../../Icons/PeopleFilled.tsx"; +import ShareFilled from "../../Icons/ShareFilled.tsx"; +import SparkleFilled from "../../Icons/SparkleFilled.tsx"; +import PageContainer from "../../Pages/PageContainer.tsx"; +import PageHeader from "../../Pages/PageHeader.tsx"; +import ProDialog from "../Common/ProDialog.tsx"; +import SiteUrlWarning from "./SiteUrlWarning.tsx"; + +const StyledPaper = styled(Paper)(({ theme }) => ({ + padding: theme.spacing(3), + boxShadow: "initial", + border: "1px solid " + theme.palette.divider, +})); + +const StyledListItemIcon = styled(ListItemIcon)(() => ({ + minWidth: 0, +})); + +const Home = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const [summary, setSummary] = useState(); + const [chartLoading, setChartLoading] = useState(false); + const [siteUrlWarning, setSiteUrlWarning] = useState(false); + const [proDialogOpen, setProDialogOpen] = useState(false); + useEffect(() => { + loadSummary(false); + }, []); + + const loadSummary = useCallback((loadChart?: boolean) => { + if (loadChart) { + setChartLoading(true); + } + dispatch(getDashboardSummary(loadChart)) + .then((r) => { + setSummary(r); + if (!loadChart) { + const target = r.site_urls.find((site) => site == window.location.origin); + if (!target) { + setSiteUrlWarning(true); + } + } + }) + .finally(() => { + setChartLoading(false); + }); + }, []); + + return ( + + setProDialogOpen(false)} /> + setSiteUrlWarning(false)} + existingUrls={summary?.site_urls ?? []} + /> + + + + + + + {t("summary.trend")} + + {summary?.metrics_summary?.generated_at && ( + ]} + /> + )} + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${!!summary?.metrics_summary}-${!!chartLoading}`} + > + + {summary?.metrics_summary && ( + + ({ + name: dayjs(i).format("MM-DD"), + file: summary?.metrics_summary?.files[d] ?? 0, + user: summary?.metrics_summary?.users[d] ?? 0, + share: summary?.metrics_summary?.shares[d] ?? 0, + }))} + > + + + + + + + + + + + )} + {chartLoading && ( + + + + )} + {!summary?.metrics_summary?.generated_at && !chartLoading && ( + + loadSummary(true)}> + {t("application:fileManager.calculate")} + + + )} + + + + + + + + + {t("summary.summary")} + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${!!summary?.metrics_summary}-${chartLoading}`} + > + + {summary?.metrics_summary && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + {chartLoading && ( + + + + )} + {!summary?.metrics_summary?.generated_at && !chartLoading && ( + + loadSummary(true)}> + {t("application:fileManager.calculate")} + + + )} + + + + + + + + + + + + Cloudreve + {summary && summary.version.pro && ( + + )} + + + {summary ? summary.version.version : } + {summary && ( + t.palette.action.disabled }}> + #{summary.version.commit} + + )} + + + + + + window.open("https://cloudreve.org")}> + + + + + + + + + window.open("https://github.com/cloudreve/cloudreve")}> + + + + + + + + + window.open("https://docs.cloudreve.org/")}> + + + + + + + + + window.open("https://discord.gg/WTpMFpZT76")}> + + + + + + + + + {summary && !summary.version.pro && ( + setProDialogOpen(true)}> + + + + + + )} + + + + + + + + 公告 + + + + + + + + + ); +}; + +export default Home; diff --git a/src/component/Admin/Home/SiteUrlWarning.tsx b/src/component/Admin/Home/SiteUrlWarning.tsx new file mode 100644 index 0000000..954bac0 --- /dev/null +++ b/src/component/Admin/Home/SiteUrlWarning.tsx @@ -0,0 +1,81 @@ +import { DialogContent, List, ListItemButton, Stack, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { sendSetSetting } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { StyledListItemText } from "../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; + +export interface SiteUrlWarningProps { + open: boolean; + onClose: () => void; + existingUrls: string[]; +} + +const SiteUrlWarning = ({ open, onClose, existingUrls }: SiteUrlWarningProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + + const setSiteUrl = (isPrimary: boolean) => () => { + const urls = [...existingUrls]; + if (isPrimary) { + urls.unshift(window.location.origin); + } else { + urls.push(window.location.origin); + } + onClose(); + dispatch( + sendSetSetting({ + settings: { + siteURL: urls.join(","), + }, + }), + ); + }; + + return ( + <> + + + + + {t("summary.siteURLNotMatch", { + current: window.location.origin, + })} + + + + + + + + + + + {t("summary.siteURLDescription")} + + + + + + ); +}; + +export default SiteUrlWarning; diff --git a/src/component/Admin/Index.js b/src/component/Admin/Index.js deleted file mode 100644 index bc12ae4..0000000 --- a/src/component/Admin/Index.js +++ /dev/null @@ -1,527 +0,0 @@ -import Avatar from "@material-ui/core/Avatar"; -import Button from "@material-ui/core/Button"; -import Chip from "@material-ui/core/Chip"; -import { blue, green, red, yellow } from "@material-ui/core/colors"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Divider from "@material-ui/core/Divider"; -import Grid from "@material-ui/core/Grid"; -import List from "@material-ui/core/List"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemAvatar from "@material-ui/core/ListItemAvatar"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import ListItemText from "@material-ui/core/ListItemText"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import { - Description, - Favorite, - FileCopy, - Forum, - GitHub, - Home, - Launch, - Lock, - People, - Public, - Telegram, -} from "@material-ui/icons"; -import axios from "axios"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { - CartesianGrid, - Legend, - Line, - LineChart, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { ResponsiveContainer } from "recharts/lib/component/ResponsiveContainer"; -import TimeAgo from "timeago-react"; -import { toggleSnackbar } from "../../redux/explorer"; -import API from "../../middleware/Api"; -import pathHelper from "../../utils/page"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - paper: { - padding: theme.spacing(3), - height: "100%", - }, - logo: { - width: 70, - }, - logoContainer: { - padding: theme.spacing(3), - display: "flex", - }, - title: { - marginLeft: 16, - }, - cloudreve: { - fontSize: 25, - color: theme.palette.text.secondary, - }, - version: { - color: theme.palette.text.hint, - }, - links: { - padding: theme.spacing(3), - }, - iconRight: { - minWidth: 0, - }, - userIcon: { - backgroundColor: blue[100], - color: blue[600], - }, - fileIcon: { - backgroundColor: yellow[100], - color: yellow[800], - }, - publicIcon: { - backgroundColor: green[100], - color: green[800], - }, - secretIcon: { - backgroundColor: red[100], - color: red[800], - }, -})); - -export default function Index() { - const { t } = useTranslation("dashboard"); - const classes = useStyles(); - const [lineData, setLineData] = useState([]); - const [news, setNews] = useState([]); - const [newsUsers, setNewsUsers] = useState({}); - const [open, setOpen] = React.useState(false); - const [siteURL, setSiteURL] = React.useState(""); - const [statistics, setStatistics] = useState({ - fileTotal: 0, - userTotal: 0, - publicShareTotal: 0, - secretShareTotal: 0, - }); - const [version, setVersion] = useState({ - backend: "-", - }); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const ResetSiteURL = () => { - setOpen(false); - API.patch("/admin/setting", { - options: [ - { - key: "siteURL", - value: window.location.origin, - }, - ], - }) - .then(() => { - setSiteURL(window.location.origin); - ToggleSnackbar("top", "right", t("settings.saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - API.get("/admin/summary") - .then((response) => { - const data = []; - response.data.date.forEach((v, k) => { - data.push({ - name: v, - file: response.data.files[k], - user: response.data.users[k], - share: response.data.shares[k], - }); - }); - setLineData(data); - setStatistics({ - fileTotal: response.data.fileTotal, - userTotal: response.data.userTotal, - publicShareTotal: response.data.publicShareTotal, - secretShareTotal: response.data.secretShareTotal, - }); - setVersion(response.data.version); - setSiteURL(response.data.siteURL); - if ( - response.data.siteURL === "" || - response.data.siteURL !== window.location.origin - ) { - setOpen(true); - } - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - - axios - .get("/api/v3/admin/news?tag=" + t("summary.newsTag")) - .then((response) => { - setNews(response.data.data); - const res = {}; - response.data.included.forEach((v) => { - if (v.type === "users") { - res[v.id] = v.attributes; - } - }); - setNewsUsers(res); - }) - .catch((error) => { - ToggleSnackbar( - "top", - "right", - t("summary.newsletterError"), - "warning" - ); - }); - }, []); - - return ( - - setOpen(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - - {t("summary.confirmSiteURLTitle")} - - - - - {siteURL === "" && - t("summary.siteURLNotSet", { - current: window.location.origin, - })} - {siteURL !== "" && - t("summary.siteURLNotMatch", { - current: window.location.origin, - })} - - - {t("summary.siteURLDescription")} - - - - - - - - - - - - {t("summary.trend")} - - - - - - - - - - - - - - - - - - - {t("summary.summary")} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- cloudreve -
- - Cloudreve - - - {version.backend}{" "} - {version.is_pro === "true" && ( - - )} - -
-
- -
- - - window.open("https://cloudreve.org") - } - > - - - - - - - - - - window.open( - "https://github.com/cloudreve/cloudreve" - ) - } - > - - - - - - - - - - window.open("https://docs.cloudreve.org/") - } - > - - - - - - - - - - window.open(t("summary.forumLink")) - } - > - - - - - - - - - - window.open(t("summary.telegramGroupLink")) - } - > - - - - - - - - - - window.open("https://cloudreve.org/pro") - } - > - - - - - - - - - -
-
-
- - - - {news && - news.map((v) => ( - <> - - window.open( - "https://forum.cloudreve.org/d/" + - v.id - ) - } - > - - - - - - {newsUsers[ - v.relationships - .startUser.data - .id - ] && - newsUsers[ - v.relationships - .startUser - .data.id - ].username}{" "} - - , - ]} - /> - - } - /> - - - - ))} - - - -
- ); -} diff --git a/src/component/Admin/Node/AddNode.js b/src/component/Admin/Node/AddNode.js deleted file mode 100644 index 57479cb..0000000 --- a/src/component/Admin/Node/AddNode.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Paper from "@material-ui/core/Paper"; -import NodeGuide from "./Guide/NodeGuide"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, -})); - -export default function AddNode() { - const classes = useStyles(); - return ( -
- - - -
- ); -} diff --git a/src/component/Admin/Node/EditNode.js b/src/component/Admin/Node/EditNode.js deleted file mode 100644 index 7ccc1a8..0000000 --- a/src/component/Admin/Node/EditNode.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Paper from "@material-ui/core/Paper"; -import NodeGuide from "./Guide/NodeGuide"; -import { useParams } from "react-router"; -import { useDispatch } from "react-redux"; -import API from "../../../middleware/Api"; -import { toggleSnackbar } from "../../../redux/explorer"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, -})); - -export default function EditNode() { - const classes = useStyles(); - const { id } = useParams(); - const [node, setNode] = useState(null); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.get("/admin/node/" + id) - .then((response) => { - response.data.Rank = response.data.Rank.toString(); - response.data.Aria2OptionsSerialized.interval = response.data.Aria2OptionsSerialized.interval.toString(); - response.data.Aria2OptionsSerialized.timeout = response.data.Aria2OptionsSerialized.timeout.toString(); - response.data.Aria2Enabled = response.data.Aria2Enabled - ? "true" - : "false"; - setNode(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, [id]); - - return ( -
- - {node && } - -
- ); -} diff --git a/src/component/Admin/Node/EditNode/BasicInfoSection.tsx b/src/component/Admin/Node/EditNode/BasicInfoSection.tsx new file mode 100644 index 0000000..9a543ff --- /dev/null +++ b/src/component/Admin/Node/EditNode/BasicInfoSection.tsx @@ -0,0 +1,167 @@ +import { Alert, FormControl, FormControlLabel, Switch, Typography } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { testNode } from "../../../../api/api"; +import { Node, NodeStatus, NodeType } from "../../../../api/dashboard"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { Code } from "../../Common/Code"; +import { EndpointInput } from "../../Common/EndpointInput"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { NodeSettingContext } from "./NodeSettingWrapper"; +const BasicInfoSection = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const { values, setNode } = useContext(NodeSettingContext); + const [testNodeLoading, setTestNodeLoading] = useState(false); + + const onNameChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ ...p, name: e.target.value })); + }, + [setNode], + ); + + const onServerChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ ...p, server: e.target.value })); + }, + [setNode], + ); + + const onSlaveKeyChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ ...p, slave_key: e.target.value })); + }, + [setNode], + ); + + const onWeightChange = useCallback( + (e: React.ChangeEvent) => { + const weight = parseInt(e.target.value); + setNode((p: Node) => ({ ...p, weight: isNaN(weight) ? 1 : weight })); + }, + [setNode], + ); + + const onStatusChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + status: e.target.checked ? NodeStatus.active : NodeStatus.suspended, + })); + }, + [setNode], + ); + + const isActive = useMemo(() => { + return values.status === NodeStatus.active; + }, [values.status]); + + const nodeTypeText = useMemo(() => { + return values.type === NodeType.master ? t("node.master") : t("node.slave"); + }, [values.type, t]); + + const onTestNode = useCallback(() => { + setTestNodeLoading(true); + dispatch(testNode({ node: values })) + .then((res) => { + enqueueSnackbar(t("node.testNodeSuccess"), { variant: "success", action: DefaultCloseAction }); + }) + .finally(() => { + setTestNodeLoading(false); + }); + }, [dispatch, values]); + + return ( + + + {t("policy.basicInfo")} + + + {values.type === NodeType.master && ( + + {t("node.thisIsMasterNodes")} + + )} + + + } + label={t("node.enableNode")} + /> + {t("node.enableNodeDes")} + + + + + + {t("node.nameNode")} + + + + + + + + {values.type === NodeType.slave && ( + <> + + + + {t("node.serverDes")} + + + + + + + , ]} /> + + + + + )} + + + + {t("node.loadBalancerRankDes")} + + + {values.type === NodeType.slave && ( + + + {t("node.testNode")} + + + )} + + + ); +}; + +export default BasicInfoSection; diff --git a/src/component/Admin/Node/EditNode/CapabilitiesSection.tsx b/src/component/Admin/Node/EditNode/CapabilitiesSection.tsx new file mode 100644 index 0000000..3c6af73 --- /dev/null +++ b/src/component/Admin/Node/EditNode/CapabilitiesSection.tsx @@ -0,0 +1,551 @@ +import { + CircularProgress, + Collapse, + FormControl, + FormControlLabel, + Link, + ListItemText, + SelectChangeEvent, + Switch, + Typography, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { testNodeDownloader } from "../../../../api/api"; +import { DownloaderProvider, Node, NodeType } from "../../../../api/dashboard"; +import { NodeCapability } from "../../../../api/workflow"; +import { useAppDispatch } from "../../../../redux/hooks"; +import Boolset from "../../../../util/boolset"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { Code } from "../../Common/Code"; +import { EndpointInput } from "../../Common/EndpointInput"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings"; +import { NodeSettingContext } from "./NodeSettingWrapper"; +import StoreFilesHintDialog from "./StoreFilesHintDialog"; +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor")); + +const CapabilitiesSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setNode } = useContext(NodeSettingContext); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const [editedConfigAria2, setEditedConfigAria2] = useState(""); + const [editedConfigQbittorrent, setEditedConfigQbittorrent] = useState(""); + const [testDownloaderLoading, setTestDownloaderLoading] = useState(false); + const [storeFilesHintDialogOpen, setStoreFilesHintDialogOpen] = useState(false); + + const capabilities = useMemo(() => { + return new Boolset(values.capabilities ?? ""); + }, [values.capabilities]); + + const hasRemoteDownload = useMemo(() => { + return capabilities.enabled(NodeCapability.remote_download); + }, [capabilities]); + + useEffect(() => { + setEditedConfigAria2( + values.settings?.aria2?.options ? JSON.stringify(values.settings?.aria2?.options, null, 2) : "", + ); + }, [values.settings?.aria2?.options]); + + useEffect(() => { + setEditedConfigQbittorrent( + values.settings?.qbittorrent?.options ? JSON.stringify(values.settings?.qbittorrent?.options, null, 2) : "", + ); + }, [values.settings?.qbittorrent?.options]); + + const onCapabilityChange = useCallback( + (capability: number) => (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + capabilities: new Boolset(p.capabilities).set(capability, e.target.checked).toString(), + })); + }, + [setNode], + ); + + const onProviderChange = useCallback( + (e: SelectChangeEvent) => { + const provider = e.target.value as DownloaderProvider; + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + provider, + }, + })); + }, + [setNode], + ); + + const onAria2ServerChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + aria2: { + ...p.settings?.aria2, + server: e.target.value, + }, + }, + })); + }, + [setNode], + ); + + const onAria2TokenChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + aria2: { + ...p.settings?.aria2, + token: e.target.value, + }, + }, + })); + }, + [setNode], + ); + + const onAria2TempPathChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + aria2: { + ...p.settings?.aria2, + temp_path: e.target.value ? e.target.value : undefined, + }, + }, + })); + }, + [setNode], + ); + + const onQBittorrentServerChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + qbittorrent: { + ...p.settings?.qbittorrent, + server: e.target.value, + }, + }, + })); + }, + [setNode], + ); + + const onQBittorrentUserChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + qbittorrent: { + ...p.settings?.qbittorrent, + user: e.target.value ? e.target.value : undefined, + }, + }, + })); + }, + [setNode], + ); + + const onQBittorrentPasswordChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + qbittorrent: { + ...p.settings?.qbittorrent, + password: e.target.value ? e.target.value : undefined, + }, + }, + })); + }, + [setNode], + ); + + const onQBittorrentTempPathChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + qbittorrent: { + ...p.settings?.qbittorrent, + temp_path: e.target.value ? e.target.value : undefined, + }, + }, + })); + }, + [setNode], + ); + + const onIntervalChange = useCallback( + (e: React.ChangeEvent) => { + const interval = parseInt(e.target.value); + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + interval: isNaN(interval) ? undefined : interval, + }, + })); + }, + [setNode], + ); + + const onWaitForSeedingChange = useCallback( + (e: React.ChangeEvent) => { + setNode((p: Node) => ({ + ...p, + settings: { + ...p.settings, + wait_for_seeding: e.target.checked ? true : undefined, + }, + })); + }, + [setNode], + ); + + const onEditedConfigAria2Blur = useCallback( + (value: string) => { + var res: Record | undefined = undefined; + if (value) { + try { + res = JSON.parse(value); + } catch (e) { + console.error(e); + } + } + setNode((p: Node) => ({ ...p, settings: { ...p.settings, aria2: { ...p.settings?.aria2, options: res } } })); + }, + [editedConfigAria2, setNode], + ); + + const onEditedConfigQbittorrentBlur = useCallback( + (value: string) => { + var res: Record | undefined = undefined; + if (value) { + try { + res = JSON.parse(value); + } catch (e) { + console.error(e); + } + } + setNode((p: Node) => ({ + ...p, + settings: { ...p.settings, qbittorrent: { ...p.settings?.qbittorrent, options: res } }, + })); + }, + [editedConfigQbittorrent, setNode], + ); + + const onTestDownloaderClick = useCallback(() => { + setTestDownloaderLoading(true); + dispatch(testNodeDownloader({ node: values })) + .then((res) => { + enqueueSnackbar({ + variant: "success", + message: t("node.downloaderTestPass", { version: res }), + action: DefaultCloseAction, + }); + }) + .finally(() => { + setTestDownloaderLoading(false); + }); + }, [values]); + + const onStoreFilesClick = useCallback(() => { + setStoreFilesHintDialogOpen(true); + }, []); + + return ( + <> + setStoreFilesHintDialogOpen(false)} /> + + + {t("node.features")} + + + + + + } + label={t("application:fileManager.createArchive")} + /> + {t("node.createArchiveDes")} + + + + + + } + label={t("application:fileManager.extractArchive")} + /> + {t("node.extractArchiveDes")} + + + + + + } + label={t("application:navbar.remoteDownload")} + /> + {t("node.remoteDownloadDes")} + + + {values.type === NodeType.slave && ( + + + 0} + checked={(values.edges?.storage_policy?.length ?? 0) > 0} + /> + } + label={t("node.storeFiles")} + /> + {t("node.storeFilesDes")} + + + )} + + + + + + + {t("node.remoteDownload")} + + + + + + + + + + + + + + {values.settings?.provider === DownloaderProvider.qbittorrent + ? t("node.qbittorrentDes") + : t("node.aria2Des")} + + + + + {values.settings?.provider === DownloaderProvider.aria2 && ( + <> + + + + + ]} /> + + + + + + + + ]} /> + + + + + + }> + setEditedConfigAria2(value || "")} + onBlur={onEditedConfigAria2Blur} + height="200px" + minHeight="200px" + options={{ + wordWrap: "on", + minimap: { enabled: false }, + scrollBeyondLastLine: false, + }} + /> + + + , + ]} + /> + + + + + + + {t("node.tempPathDes")} + + + + )} + + {values.settings?.provider === DownloaderProvider.qbittorrent && ( + <> + + + + + ]} /> + + + + + + + + {t("node.webUICredDes")} + + + + + }> + setEditedConfigQbittorrent(value || "")} + onBlur={onEditedConfigQbittorrentBlur} + height="200px" + minHeight="200px" + options={{ + wordWrap: "on", + minimap: { enabled: false }, + scrollBeyondLastLine: false, + }} + /> + + + , + ]} + /> + + + + + + + {t("node.tempPathDes")} + + + + )} + + + + + {t("node.refreshIntervalDes")} + + + + + + + } + label={t("node.waitForSeeding")} + /> + {t("node.waitForSeedingDes")} + + + + + {t("node.testDownloader")} + + + + + + + ); +}; + +export default CapabilitiesSection; diff --git a/src/component/Admin/Node/EditNode/EditNode.tsx b/src/component/Admin/Node/EditNode/EditNode.tsx new file mode 100644 index 0000000..46d764d --- /dev/null +++ b/src/component/Admin/Node/EditNode/EditNode.tsx @@ -0,0 +1,34 @@ +import { Container } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { Node } from "../../../../api/dashboard"; +import PageContainer from "../../../Pages/PageContainer"; +import PageHeader from "../../../Pages/PageHeader"; +import BasicInfoSection from "./BasicInfoSection"; +import CapabilitiesSection from "./CapabilitiesSection"; +import NodeForm from "./NodeForm"; +import NodeSettingWrapper from "./NodeSettingWrapper"; + +const EditNode = () => { + const { t } = useTranslation("dashboard"); + const { id } = useParams<{ id: string }>(); + const [node, setNode] = useState(null); + const nodeID = parseInt(id ?? "0"); + + return ( + + + + + + + + + + + + ); +}; + +export default EditNode; diff --git a/src/component/Admin/Node/EditNode/NodeForm.tsx b/src/component/Admin/Node/EditNode/NodeForm.tsx new file mode 100644 index 0000000..4da51c9 --- /dev/null +++ b/src/component/Admin/Node/EditNode/NodeForm.tsx @@ -0,0 +1,19 @@ +import { Box, Stack } from "@mui/material"; +import { useContext } from "react"; +import { NodeSettingContext } from "./NodeSettingWrapper"; + +export interface NodeFormProps { + children: React.ReactNode; +} + +const NodeForm = ({ children }: NodeFormProps) => { + const { formRef } = useContext(NodeSettingContext); + + return ( + + {children} + + ); +}; + +export default NodeForm; diff --git a/src/component/Admin/Node/EditNode/NodeSettingWrapper.tsx b/src/component/Admin/Node/EditNode/NodeSettingWrapper.tsx new file mode 100644 index 0000000..098511d --- /dev/null +++ b/src/component/Admin/Node/EditNode/NodeSettingWrapper.tsx @@ -0,0 +1,157 @@ +import { Box } from "@mui/material"; +import * as React from "react"; +import { createContext, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getNodeDetail, upsertNode } from "../../../../api/api.ts"; +import { Node, StoragePolicy } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import { SavingFloat } from "../../Settings/SettingWrapper.tsx"; + +export interface NodeSettingWrapperProps { + nodeID: number; + children: React.ReactNode; + onNodeChange: (node: Node) => void; +} + +export interface NodeSettingContextProps { + values: Node; + setNode: (f: (p: Node) => Node) => void; + formRef?: React.RefObject; +} + +const defaultNode: Node = { + id: 0, + name: "", + status: undefined, + type: undefined, + server: "", + slave_key: "", + capabilities: "", + weight: 1, + settings: {}, + edges: { + storage_policy: [], + }, +}; + +export const NodeSettingContext = createContext({ + values: { ...defaultNode }, + setNode: () => {}, +}); + +const nodeValueFilter = (node: Node): Node => { + return { + ...node, + edges: { + storage_policy: node.edges.storage_policy?.map( + (p): StoragePolicy => + ({ + id: p.id, + }) as StoragePolicy, + ), + }, + }; +}; + +const NodeSettingWrapper = ({ nodeID, children, onNodeChange }: NodeSettingWrapperProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ + ...defaultNode, + }); + const [modifiedValues, setModifiedValues] = useState({ + ...defaultNode, + }); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + useEffect(() => { + setLoading(true); + dispatch(getNodeDetail(nodeID)) + .then((res) => { + setValues(nodeValueFilter(res)); + setModifiedValues(nodeValueFilter(res)); + onNodeChange(nodeValueFilter(res)); + }) + .finally(() => { + setLoading(false); + }); + }, [nodeID]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + setSubmitting(true); + dispatch( + upsertNode({ + node: { ...modifiedValues }, + }), + ) + .then((res) => { + setValues(nodeValueFilter(res)); + setModifiedValues(nodeValueFilter(res)); + onNodeChange(nodeValueFilter(res)); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + {children} + + + )} + + + + + ); +}; + +export default NodeSettingWrapper; diff --git a/src/component/Admin/Node/EditNode/StoreFilesHintDialog.tsx b/src/component/Admin/Node/EditNode/StoreFilesHintDialog.tsx new file mode 100644 index 0000000..6628dee --- /dev/null +++ b/src/component/Admin/Node/EditNode/StoreFilesHintDialog.tsx @@ -0,0 +1,35 @@ +import { DialogContent, Link, Typography } from "@mui/material"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import DraggableDialog from "../../../Dialogs/DraggableDialog"; +export interface StoreFilesHintDialogProps { + open: boolean; + onClose: () => void; +} + +const StoreFilesHintDialog = ({ open, onClose }: StoreFilesHintDialogProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + ]} + /> + + + + ); +}; + +export default StoreFilesHintDialog; diff --git a/src/component/Admin/Node/EditNode/index.tsx b/src/component/Admin/Node/EditNode/index.tsx new file mode 100644 index 0000000..fef0766 --- /dev/null +++ b/src/component/Admin/Node/EditNode/index.tsx @@ -0,0 +1,3 @@ +import EditNode from "./EditNode"; + +export default EditNode; diff --git a/src/component/Admin/Node/Guide/Aria2RPC.js b/src/component/Admin/Node/Guide/Aria2RPC.js deleted file mode 100644 index d065903..0000000 --- a/src/component/Admin/Node/Guide/Aria2RPC.js +++ /dev/null @@ -1,454 +0,0 @@ -import { lighten, makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useState } from "react"; -import Typography from "@material-ui/core/Typography"; -import { useDispatch } from "react-redux"; -import Link from "@material-ui/core/Link"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Radio from "@material-ui/core/Radio"; -import Collapse from "@material-ui/core/Collapse"; -import Button from "@material-ui/core/Button"; -import Alert from "@material-ui/lab/Alert"; -import Box from "@material-ui/core/Box"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import API from "../../../../middleware/Api"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - }, - }, -})); - -export default function Aria2RPC(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const dispatch = useDispatch(); - const [loading, setLoading] = useState(false); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const testAria2 = () => { - setLoading(true); - API.post("/admin/node/aria2/test", { - type: props.node.Type, - server: props.node.Server, - secret: props.node.SlaveKey, - rpc: props.node.Aria2OptionsSerialized.server, - token: props.node.Aria2OptionsSerialized.token, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - t("ariaSuccess", { version: response.data }), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const mode = props.node.Type === 0 ? t("slave") : t("master"); - - return ( -
{ - e.preventDefault(); - props.onSubmit(e); - }} - > - - - , - , - , - ]} - /> - - - -
-
-
1
-
-
- - {props.node.Type === 0 - ? t("slaveTakeOverRemoteDownload") - : t("masterTakeOverRemoteDownload")} -
- {props.node.Type === 0 - ? t("routeTaskSlave") - : t("routeTaskMaster")} -
- -
- - - } - label={t("enable")} - /> - } - label={t("disable")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("aria2ConfigDes", { - target: - props.node.Type === 0 - ? t("slaveNodeTarget") - : t("masterNodeTarget"), - })} - -
-                            # {t("enableRPCComment")}
-                            
- enable-rpc=true -
# {t("rpcPortComment")} -
- rpc-listen-port=6800 -
# {t("rpcSecretComment")} -
- rpc-secret= - {props.node.Aria2OptionsSerialized.token} -
-
- - - {t("rpcConfigDes")} - - -
-
- -
-
-
3
-
-
- - , - , - , - ]} - /> - -
- - - {t("rpcServer")} - - - - {t("rpcServerHelpDes")} - - -
-
-
- -
-
-
4
-
-
- - ]} - /> - -
- -
-
-
- -
-
-
5
-
-
- - ]} - /> - -
- -
-
-
- -
-
-
5
-
-
- - {t("aria2SettingDes")} - -
- - - {t("refreshInterval")} - - - - {t("refreshIntervalDes")} - - -
-
- - - {t("rpcTimeout")} - - - - {t("rpcTimeoutDes")} - - -
-
- - - {t("globalOptions")} - - - - {t("globalOptionsDes")} - - -
-
-
- -
-
-
6
-
-
- - {t("testAria2Des", { mode })} - {props.node.Type === 0 && - t("testAria2DesSlaveAddition")} - -
- -
-
-
-
- -
- {props.activeStep !== 0 && ( - - )} - -
- - ); -} diff --git a/src/component/Admin/Node/Guide/Communication.js b/src/component/Admin/Node/Guide/Communication.js deleted file mode 100644 index 6a99e49..0000000 --- a/src/component/Admin/Node/Guide/Communication.js +++ /dev/null @@ -1,305 +0,0 @@ -import { lighten, makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useState } from "react"; -import Typography from "@material-ui/core/Typography"; -import { useDispatch } from "react-redux"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import Button from "@material-ui/core/Button"; -import API from "../../../../middleware/Api"; -import Alert from "@material-ui/lab/Alert"; -import Box from "@material-ui/core/Box"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - }, - }, -})); - -export default function Communication(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const dispatch = useDispatch(); - const [loading, setLoading] = useState(false); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const testSlave = () => { - setLoading(true); - - // 测试路径是否可用 - API.post("/admin/policy/test/slave", { - server: props.node.Server, - secret: props.node.SlaveKey, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - tDashboard("policy.communicationOK"), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
{ - e.preventDefault(); - props.onSubmit(e); - }} - > - - ]} - /> - - -
-
-
1
-
-
- - {tDashboard("policy.remoteCopyBinaryDescription")} - -
-
- -
-
-
2
-
-
- - {tDashboard("policy.remoteSecretDescription")} - -
- - - {tDashboard("policy.remoteSecret")} - - - -
-
-
- -
-
-
3
-
-
- - {tDashboard("policy.modifyRemoteConfig")} -
- ]} - /> -
-
-                        [System]
-                        
- Mode = slave -
- Listen = :5212 -
-
- [Slave] -
- Secret = {props.node.SlaveKey} -
-
- ,
]} - /> -
- [OptionOverwrite] -
; {t("workerNumDes")} -
- max_worker_num = 50 -
; {t("parallelTransferDes")} -
- max_parallel_transfer = 10 -
; {t("chunkRetriesDes")} -
- chunk_retries = 10 -
- - {tDashboard("policy.remoteConfigDifference")} -
    -
  • - , - , - , - ]} - /> -
  • -
  • - , - , - ]} - /> -
  • -
- {t("multipleMasterDes")} -
-
-
- -
-
-
4
-
-
- - {tDashboard("policy.inputRemoteAddress")} -
- {tDashboard("policy.inputRemoteAddressDes")} -
-
- - - {tDashboard("policy.remoteAddress")} - - - -
-
-
- -
-
-
5
-
-
- - {tDashboard("policy.testCommunicationDes")} - -
- -
-
-
- -
- -
- - ); -} diff --git a/src/component/Admin/Node/Guide/Completed.js b/src/component/Admin/Node/Guide/Completed.js deleted file mode 100644 index 1aa835b..0000000 --- a/src/component/Admin/Node/Guide/Completed.js +++ /dev/null @@ -1,98 +0,0 @@ -import { lighten, makeStyles } from "@material-ui/core/styles"; -import React from "react"; -import Typography from "@material-ui/core/Typography"; -import Button from "@material-ui/core/Button"; -import { useHistory } from "react-router"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - }, - }, -})); - -export default function Completed(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const classes = useStyles(); - const history = useHistory(); - - return ( -
- {t("nodeSaved")} - - {t("nodeSavedFutureAction")} - - -
- -
-
- ); -} diff --git a/src/component/Admin/Node/Guide/Metainfo.js b/src/component/Admin/Node/Guide/Metainfo.js deleted file mode 100644 index a2b6ede..0000000 --- a/src/component/Admin/Node/Guide/Metainfo.js +++ /dev/null @@ -1,157 +0,0 @@ -import { lighten, makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useState } from "react"; -import Typography from "@material-ui/core/Typography"; -import { useDispatch } from "react-redux"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import Button from "@material-ui/core/Button"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - }, - }, -})); - -export default function Metainfo(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const dispatch = useDispatch(); - const [loading, setLoading] = useState(false); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - return ( -
{ - e.preventDefault(); - props.onSubmit(e); - }} - > -
-
-
1
-
-
- {t("nameNode")} -
- - - -
-
-
- -
-
-
2
-
-
- - {t("loadBalancerRankDes")} - -
- - - {t("loadBalancerRank")} - - - -
-
-
- -
- -
-
- ); -} diff --git a/src/component/Admin/Node/Guide/NodeGuide.js b/src/component/Admin/Node/Guide/NodeGuide.js deleted file mode 100644 index 1d3e62f..0000000 --- a/src/component/Admin/Node/Guide/NodeGuide.js +++ /dev/null @@ -1,191 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import Stepper from "@material-ui/core/Stepper"; -import StepLabel from "@material-ui/core/StepLabel"; -import Step from "@material-ui/core/Step"; -import Typography from "@material-ui/core/Typography"; -import { useDispatch } from "react-redux"; -import { randomStr } from "../../../../utils"; -import Communication from "./Communication"; -import Aria2RPC from "./Aria2RPC"; -import API from "../../../../middleware/Api"; -import Metainfo from "./Metainfo"; -import Completed from "./Completed"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const steps = [ - { - slaveOnly: true, - title: "communication", - optional: false, - component: function show(p) { - return ; - }, - }, - { - slaveOnly: false, - title: "remoteDownload", - optional: false, - component: function show(p) { - return ; - }, - }, - { - slaveOnly: false, - title: "otherSettings", - optional: false, - component: function show(p) { - return ; - }, - }, - { - slaveOnly: false, - title: "finish", - optional: false, - component: function show(p) { - return ; - }, - }, -]; - -export default function NodeGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const { t: tDashboard } = useTranslation("dashboard"); - const [activeStep, setActiveStep] = useState(0); - const [skipped, setSkipped] = React.useState(new Set()); - const [loading, setLoading] = useState(false); - const [node, setNode] = useState( - props.node - ? props.node - : { - Status: 1, - Type: 0, - Aria2Enabled: "false", - Server: "https://example.com:5212", - SlaveKey: randomStr(64), - MasterKey: randomStr(64), - Rank: "0", - Aria2OptionsSerialized: { - token: randomStr(32), - options: "{}", - interval: "10", - timeout: "10", - }, - } - ); - - const usedSteps = useMemo(() => { - return steps.filter((step) => !(step.slaveOnly && node.Type === 1)); - }, [node.Type]); - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const handleTextChange = (name) => (event) => { - setNode({ - ...node, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setNode({ - ...node, - Aria2OptionsSerialized: { - ...node.Aria2OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const nextStep = () => { - if (props.node || activeStep + 1 === steps.length - 1) { - setLoading(true); - - const nodeCopy = { ...node }; - nodeCopy.Aria2OptionsSerialized = { - ...node.Aria2OptionsSerialized, - }; - nodeCopy.Rank = parseInt(nodeCopy.Rank); - nodeCopy.Aria2OptionsSerialized.interval = parseInt( - nodeCopy.Aria2OptionsSerialized.interval - ); - nodeCopy.Aria2OptionsSerialized.timeout = parseInt( - nodeCopy.Aria2OptionsSerialized.timeout - ); - nodeCopy.Aria2Enabled = nodeCopy.Aria2Enabled === "true"; - API.post("/admin/node", { - node: nodeCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.node ? t("nodeSavedNow") : t("nodeAdded"), - "success" - ); - setActiveStep(activeStep + 1); - setLoading(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - } else { - setActiveStep(activeStep + 1); - } - }; - - return ( -
- - {props.node ? t("editNode") : t("addNode")} - - - {usedSteps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {tDashboard("policy.optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - if (!(label.slaveOnly && node.Type === 1)) { - return ( - - - {t(label.title)} - - - ); - } - })} - - - {usedSteps[activeStep].component({ - onSubmit: (e) => nextStep(), - node: node, - loading: loading, - onBack: (e) => setActiveStep(activeStep - 1), - handleTextChange: handleTextChange, - activeStep: activeStep, - handleOptionChange: handleOptionChange, - })} -
- ); -} diff --git a/src/component/Admin/Node/NewNode/NewNodeDialog.tsx b/src/component/Admin/Node/NewNode/NewNodeDialog.tsx new file mode 100644 index 0000000..8e0de34 --- /dev/null +++ b/src/component/Admin/Node/NewNode/NewNodeDialog.tsx @@ -0,0 +1,234 @@ +import { Box, Stack, Typography, useTheme } from "@mui/material"; +import { grey } from "@mui/material/colors"; +import { useSnackbar } from "notistack"; +import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { testNode, upsertNode } from "../../../../api/api"; +import { DownloaderProvider, Node, NodeStatus, NodeType } from "../../../../api/dashboard"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { randomString } from "../../../../util"; +import FacebookCircularProgress from "../../../Common/CircularProgress"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents"; +import DraggableDialog from "../../../Dialogs/DraggableDialog"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { Code } from "../../Common/Code"; +import { EndpointInput } from "../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../Settings/Settings"; +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor")); + +export interface NewNodeDialogProps { + open: boolean; + onClose: () => void; +} + +const defaultNode: Node = { + id: 0, + name: "", + type: NodeType.slave, + status: NodeStatus.active, + server: "", + slave_key: "", + capabilities: "", + weight: 1, + settings: { + provider: DownloaderProvider.aria2, + qbittorrent: {}, + aria2: {}, + interval: 5, + }, + edges: { + storage_policy: [], + }, +}; + +export const Step = ({ step, children }: { step: number; children: React.ReactNode }) => { + return ( + + theme.transitions.create("background-color", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + "&:focus-within": { + backgroundColor: (theme) => (theme.palette.mode == "dark" ? grey[900] : grey[100]), + }, + }} + > + + t.typography.body2.fontSize, + height: "20px", + backgroundColor: (theme) => theme.palette.primary.light, + color: (theme) => theme.palette.primary.contrastText, + textAlign: "center", + borderRadius: " 50%", + }} + > + {step} + + + {children} + + ); +}; + +export const NewNodeDialog = ({ open, onClose }: NewNodeDialogProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + const [loading, setLoading] = useState(false); + const [node, setNode] = useState({ ...defaultNode }); + const formRef = useRef(null); + + useEffect(() => { + if (open) { + setNode({ ...defaultNode, slave_key: randomString(64) }); + } + }, [open]); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + + setLoading(true); + dispatch(upsertNode({ node })) + .then((r) => { + navigate(`/admin/node/${r.id}`); + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }; + + const config = useMemo(() => { + return `[System] +Mode = slave +Listen = :5212 + +[Slave] +Secret = ${node.slave_key} + +; ${t("node.keepIfUpload")} +[CORS] +AllowOrigins = * +AllowMethods = OPTIONS,GET,POST +AllowHeaders = * +`; + }, [t, node.slave_key]); + + const handleTest = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + + setLoading(true); + dispatch(testNode({ node })) + .then(() => { + setLoading(false); + enqueueSnackbar(t("node.testNodeSuccess"), { variant: "success", action: DefaultCloseAction }); + }) + .finally(() => setLoading(false)); + }; + + return ( + +
+ + + + + {t("node.nameTheNode")} + + setNode({ ...node, name: e.target.value })} + /> + {t("node.nameNode")} + + + + + + {t("node.runCrSlave")} + + }> + + + + ]} /> + + + + + + + {t("node.inputServer")} + + setNode({ ...node, server: e.target.value })} + /> + {t("node.serverDes")} + + + + + + {t("node.testButton")} + + + {t("node.testNode")} + + + ]} /> + + + + +
+
+ ); +}; diff --git a/src/component/Admin/Node/Node.js b/src/component/Admin/Node/Node.js deleted file mode 100644 index 87f076d..0000000 --- a/src/component/Admin/Node/Node.js +++ /dev/null @@ -1,333 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import API from "../../../middleware/Api"; -import { useDispatch } from "react-redux"; -import Paper from "@material-ui/core/Paper"; -import Button from "@material-ui/core/Button"; -import TableContainer from "@material-ui/core/TableContainer"; -import Table from "@material-ui/core/Table"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TableCell from "@material-ui/core/TableCell"; -import TableBody from "@material-ui/core/TableBody"; -import TablePagination from "@material-ui/core/TablePagination"; -import { useHistory } from "react-router"; -import IconButton from "@material-ui/core/IconButton"; -import { - Cancel, - CheckCircle, - Delete, - Edit, - Pause, - PlayArrow, -} from "@material-ui/icons"; -import Tooltip from "@material-ui/core/Tooltip"; -import Chip from "@material-ui/core/Chip"; -import classNames from "classnames"; -import Box from "@material-ui/core/Box"; -import { toggleSnackbar } from "../../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - disabledBadge: { - marginLeft: theme.spacing(1), - height: 18, - }, - disabledCell: { - color: theme.palette.text.disabled, - }, - verticalAlign: { - verticalAlign: "middle", - display: "inline-block", - }, -})); - -const columns = [ - { id: "#", minWidth: 50 }, - { id: "name", minWidth: 170 }, - { - id: "status", - minWidth: 50, - }, - { - id: "features", - minWidth: 170, - }, - { - id: "action", - minWidth: 170, - }, -]; - -const features = [ - { - field: "Aria2Enabled", - name: "remoteDownload", - }, -]; - -export default function Node() { - const { t } = useTranslation("dashboard", { keyPrefix: "node" }); - const classes = useStyles(); - const [nodes, setNodes] = useState([]); - const [isActive, setIsActive] = useState({}); - const [loading, setLoading] = useState(false); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - - const history = useHistory(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/node/list", { - page: page, - page_size: pageSize, - order_by: "id desc", - }) - .then((response) => { - setNodes(response.data.items); - setTotal(response.data.total); - setIsActive(response.data.active); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const toggleNode = (id, desired) => { - setLoading(true); - API.patch("/admin/node/enable/" + id + "/" + desired) - .then((response) => { - loadList(); - ToggleSnackbar( - "top", - "right", - desired === 1 ? t("nodeDisabled") : t("nodeEnabled"), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteNode = (id) => { - setLoading(true); - API.delete("/admin/node/" + id) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("nodeDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize]); - - const getStatusBadge = (status) => { - if (status === 1) { - return ( - - ); - } - }; - - const getFeatureBadge = (node) => - features.map((feature) => { - if (node[feature.field]) { - return ( - - ); - } - }); - - const getRealStatusBadge = (status) => - status ? ( - - {" "} - {t("online")} - - ) : ( - - {" "} - {t("offline")} - - ); - - return ( -
-
- -
- -
-
- - - - - - - {columns.map((column) => ( - - {t(column.id)} - - ))} - - - - {nodes.map((row) => ( - - {row.ID} - - {row.Name} - {getStatusBadge(row.Status)} - - - {getRealStatusBadge(isActive[row.ID])} - - - {getFeatureBadge(row)} - - - - - - toggleNode( - row.ID, - 1 - row.Status - ) - } - size={"small"} - > - {row.Status === 1 && ( - - )} - {row.Status !== 1 && } - - - - - history.push( - "/admin/node/edit/" + - row.ID - ) - } - size={"small"} - > - - - - - - deleteNode(row.ID) - } - disabled={loading} - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/Node/NodeCard.tsx b/src/component/Admin/Node/NodeCard.tsx new file mode 100644 index 0000000..9011254 --- /dev/null +++ b/src/component/Admin/Node/NodeCard.tsx @@ -0,0 +1,200 @@ +import { Box, Divider, IconButton, Skeleton, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { deleteNode } from "../../../api/api"; +import { Node, NodeStatus, NodeType } from "../../../api/dashboard"; +import { NodeCapability } from "../../../api/workflow"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import Boolset from "../../../util/boolset"; +import { NoWrapBox, SquareChip } from "../../Common/StyledComponents"; +import CheckCircleFilled from "../../Icons/CheckCircleFilled"; +import Delete from "../../Icons/Delete"; +import DismissCircleFilled from "../../Icons/DismissCircleFilled"; +import Info from "../../Icons/Info"; +import StarFilled from "../../Icons/StarFilled"; +import { BorderedCardClickable } from "../Common/AdminCard"; + +export interface NodeCardProps { + node?: Node; + onRefresh?: () => void; + loading?: boolean; +} + +const NodeCard = ({ node, onRefresh, loading }: NodeCardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [deleteLoading, setDeleteLoading] = useState(false); + + const handleDelete = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch(confirmOperation(t("node.deleteNodeConfirmation", { name: node?.name ?? "" }))).then(() => { + setDeleteLoading(true); + dispatch(deleteNode(node?.id ?? 0)) + .then(() => { + onRefresh?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + }); + }, + [node, dispatch, onRefresh], + ); + + const handleEdit = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + navigate(`/admin/node/${node?.id}`); + }, + [node, navigate], + ); + + // Decode node capabilities + const getCapabilities = useCallback(() => { + if (!node?.capabilities) return []; + + const boolset = new Boolset(node.capabilities); + const capabilities = []; + + if (boolset.enabled(NodeCapability.create_archive)) { + capabilities.push({ id: NodeCapability.create_archive, name: t("application:fileManager.createArchive") }); + } + if (boolset.enabled(NodeCapability.extract_archive)) { + capabilities.push({ id: NodeCapability.extract_archive, name: t("application:fileManager.extractArchive") }); + } + if (boolset.enabled(NodeCapability.remote_download)) { + capabilities.push({ id: NodeCapability.remote_download, name: t("application:navbar.remoteDownload") }); + } + + return capabilities; + }, [node, t]); + + // If loading is true, render a skeleton placeholder + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const capabilities = getCapabilities(); + + return ( + + + + + {node?.name} + + + {node?.type === NodeType.master && } + + {node?.type === NodeType.master ? t("node.master") : t("node.slave")} + + + + + {capabilities.length > 0 ? ( + capabilities.map((capability) => ( + + )) + ) : ( + + + {t("node.noCapabilities")} + + )} + + + + + {node?.status === NodeStatus.active ? ( + <> + + + {t("node.active")} + + + ) : ( + <> + + + {t("node.suspended")} + + + )} + + + + + + + + + + + ); +}; + +export default NodeCard; diff --git a/src/component/Admin/Node/NodeSetting.tsx b/src/component/Admin/Node/NodeSetting.tsx new file mode 100644 index 0000000..1313c93 --- /dev/null +++ b/src/component/Admin/Node/NodeSetting.tsx @@ -0,0 +1,123 @@ +import { Add } from "@mui/icons-material"; +import { Box, Container, Grid2 as Grid, Stack, Typography } from "@mui/material"; +import { useQueryState } from "nuqs"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getNodeList } from "../../../api/api"; +import { Node } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { SecondaryButton } from "../../Common/StyledComponents"; +import ArrowSync from "../../Icons/ArrowSync"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import { BorderedCardClickable } from "../Common/AdminCard"; +import TablePagination from "../Common/TablePagination"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import { NewNodeDialog } from "./NewNode/NewNodeDialog"; +import NodeCard from "./NodeCard"; + +const NodeSetting = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [nodes, setNodes] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "11", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [count, setCount] = useState(0); + const [selectProviderOpen, setSelectProviderOpen] = useState(false); + const [createNewOpen, setCreateNewOpen] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 11; + + useEffect(() => { + fetchNodes(); + }, [page, pageSize, orderBy, orderDirection]); + + const fetchNodes = () => { + setLoading(true); + dispatch( + getNodeList({ + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: {}, + }), + ) + .then((res) => { + setNodes(res.nodes); + setPage((res.pagination.page + 1).toString()); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + setCreateNewOpen(false)} /> + + + + }> + {t("node.refresh")} + + + + + setCreateNewOpen(true)} + sx={{ + height: "100%", + borderStyle: "dashed", + display: "flex", + alignItems: "center", + gap: 1, + justifyContent: "center", + color: (t) => t.palette.text.secondary, + }} + > + + {t("node.addNewNode")} + + + {!loading && nodes.map((n) => )} + {loading && nodes.length > 0 && nodes.map((n) => )} + {loading && + nodes.length === 0 && + Array.from(Array(5)).map((_, index) => )} + + {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} + + + ); +}; + +export default NodeSetting; diff --git a/src/component/Admin/Policy/AddPolicy.js b/src/component/Admin/Policy/AddPolicy.js deleted file mode 100644 index fdad810..0000000 --- a/src/component/Admin/Policy/AddPolicy.js +++ /dev/null @@ -1,45 +0,0 @@ -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import React from "react"; -import { useParams } from "react-router"; -import COSGuide from "./Guid/COSGuide"; -import LocalGuide from "./Guid/LocalGuide"; -import OneDriveGuide from "./Guid/OneDriveGuide"; -import OSSGuide from "./Guid/OSSGuide"; -import QiniuGuide from "./Guid/QiniuGuide"; -import RemoteGuide from "./Guid/RemoteGuide"; -import UpyunGuide from "./Guid/UpyunGuide"; -import S3Guide from "./Guid/S3Guide"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, -})); - -export default function AddPolicyParent() { - const classes = useStyles(); - - const { type } = useParams(); - - return ( -
- - {type === "local" && } - {type === "remote" && } - {type === "qiniu" && } - {type === "oss" && } - {type === "upyun" && } - {type === "cos" && } - {type === "onedrive" && } - {type === "s3" && } - -
- ); -} diff --git a/src/component/Admin/Policy/EditPolicy.js b/src/component/Admin/Policy/EditPolicy.js deleted file mode 100644 index 385e7e4..0000000 --- a/src/component/Admin/Policy/EditPolicy.js +++ /dev/null @@ -1,80 +0,0 @@ -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useParams } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import COSGuide from "./Guid/COSGuide"; -import EditPro from "./Guid/EditPro"; -import LocalGuide from "./Guid/LocalGuide"; -import OneDriveGuide from "./Guid/OneDriveGuide"; -import OSSGuide from "./Guid/OSSGuide"; -import QiniuGuide from "./Guid/QiniuGuide"; -import RemoteGuide from "./Guid/RemoteGuide"; -import UpyunGuide from "./Guid/UpyunGuide"; -import S3Guide from "./Guid/S3Guide"; -import { transformResponse } from "./utils"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, -})); - -export default function EditPolicyPreload() { - const classes = useStyles(); - const [type, setType] = useState(""); - const [policy, setPolicy] = useState({}); - - const { mode, id } = useParams(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - setType(""); - API.get("/admin/policy/" + id) - .then((response) => { - response = transformResponse(response); - setPolicy(response.data); - setType(response.data.Type); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, [id]); - - return ( -
- - {mode === "guide" && ( - <> - {type === "local" && } - {type === "remote" && } - {type === "qiniu" && } - {type === "oss" && } - {type === "upyun" && } - {type === "cos" && } - {type === "onedrive" && ( - - )} - {type === "s3" && } - - )} - - {mode === "pro" && type !== "" && } - -
- ); -} diff --git a/src/component/Admin/Policy/Guid/COSGuide.js b/src/component/Admin/Policy/Guid/COSGuide.js deleted file mode 100644 index f7bbf15..0000000 --- a/src/component/Admin/Policy/Guid/COSGuide.js +++ /dev/null @@ -1,1220 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import MenuItem from "@material-ui/core/MenuItem"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Select from "@material-ui/core/Select"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import { getNumber } from "../../../../utils"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - }, -})); - -const steps = [ - { - title: "storageBucket", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "corsSettingStep", - optional: true, - }, - { - title: "callbackFunctionStep", - optional: true, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function COSGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped, setSkipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState("false"); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "cos", - Name: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - Server: "", - IsPrivate: "true", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - placeholder_with_size: "false", - }, - } - ); - const [policyID, setPolicyID] = useState( - props.policy ? props.policy.ID : 0 - ); - const [region, setRegion] = useState("ap-chengdu"); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - if (useCDN === "false") { - policyCopy.BaseURL = policy.Server; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(4); - setPolicyID(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - const createCORS = () => { - setLoading(true); - API.post("/admin/policy/cors", { - id: policyID, - }) - .then(() => { - ToggleSnackbar("top", "right", t("corsPolicyAdded"), "success"); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const creatCallback = () => { - setLoading(true); - API.post("/admin/policy/scf", { - id: policyID, - region: region, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - t("callbackFunctionAdded"), - "success" - ); - setActiveStep(6); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
- - {props.policy - ? t("editCOSStoragePolicy") - : t("addCOSStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
0
-
-
- - ]} - /> - -
-
- -
-
-
1
-
-
- - , - ]} - /> - -
-
- -
-
-
2
-
-
- - ]} - /> - -
- - - {t("qiniuBucketName")} - - - -
-
-
- -
-
-
3
-
-
- - ]} - /> - -
- - - - } - label={t("cosPrivateRW")} - /> - - } - label={t("cosPublicRW")} - /> - - -
-
-
- -
-
-
4
-
-
- - , - , - ]} - /> - -
- -
-
-
- -
-
-
5
-
-
- - {t("cosCDNDes")} - -
- - { - setUseCDN(e.target.value); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
-
-
- - -
-
-
6
-
-
- - , - ]} - /> - -
- -
-
-
-
- -
-
-
- {getNumber(6, [useCDN === "true"])} -
-
-
- - , - ]} - /> - -
- - - {t("secretId")} - - - -
-
- - - {t("secretKey")} - - - -
-
-
- -
-
-
- {getNumber(7, [useCDN === "true"])} -
-
-
- - {t("nameThePolicyFirst")} - -
- - - {t("policyName")} - - - -
-
-
- -
- -
- - )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForPrivateBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
-
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("createPlaceholderDes")} - -
- - - - } - label={t("createPlaceholder")} - /> - - } - label={t("notCreatePlaceholder")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("ossCORSDes")} - -
- -
-
-
-
- {" "} -
- - )} - - {activeStep === 5 && ( -
-
-
-
- - , - ]} - /> -
-
-
- - {t("cosCallbackCreate")} - - -
- - - {t("cosBucketRegion")} - - - -
- -
- -
-
-
-
- {" "} -
- - )} - - {activeStep === 6 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/EditPro.js b/src/component/Admin/Policy/Guid/EditPro.js deleted file mode 100644 index f76e5b4..0000000 --- a/src/component/Admin/Policy/Guid/EditPro.js +++ /dev/null @@ -1,690 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import { useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -export default function EditPro(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const [, setLoading] = useState(false); - const [policy, setPolicy] = useState(props.policy); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- {t("editPolicy")} - -
- - - - {t("setting")} - {t("value")} - {t("description")} - - - - - - {t("id")} - - {policy.ID} - {t("policyID")} - - - - {t("type")} - - {policy.Type} - {t("policyType")} - - - - {t("name")} - - - - - - - {t("policyName")} - - - - {t("server")} - - - - - - - {t("policyEndpoint")} - - - - {t("bucketName")} - - - - - - - {t("bucketID")} - - - - {t("privateBucket")} - - - - - - } - label={t("yes")} - /> - - } - label={t("no")} - /> - - - - {t("privateBucketDes")} - - - - {t("resourceRootURL")} - - - - - - - {t("resourceRootURLDes")} - - - - {t("accessKey")} - - - - - - - {t("akDes")} - - - - {t("secretKey")} - - - - - - - {t("secretKey")} - - - - {t("maxSizeBytes")} - - - - - - - {t("maxSizeBytesDes")} - - - - {t("autoRename")} - - - - - - } - label={t("yes")} - /> - - } - label={t("no")} - /> - - - - {t("autoRenameDes")} - - - - {t("storagePath")} - - - - - - - {t("storagePathDes")} - - - - {t("fileName")} - - - - - - - {t("fileNameDes")} - - - - {t("allowGetSourceLink")} - - - - - - } - label={t("yes")} - /> - - } - label={t("no")} - /> - - - - - {t("allowGetSourceLinkDes")} - - - - - {t("upyunToken")} - - - - - - - {t("upyunOnly")} - - - - {t("allowedFileExtension")} - - - - - - - {t("emptyIsNoLimit")} - - - - {t("allowedMimetype")} - - - - - - - {t("qiniuOnly")} - - - - {t("odRedirectURL")} - - - - - - - - {t("noModificationNeeded")} - - - - - {t("odReverseProxy")} - - - - - - - {t("odOnly")} - - - - {t("odDriverID")} - - - - - - - {t("odDriverIDDes")} - - - - {t("s3Region")} - - - - - - - {t("s3Only")} - - - - {t("lanEndpoint")} - - - - - - - {t("ossOnly")} - - - - {t("chunkSizeBytes")} - - - - - - - {t("chunkSizeBytesDes")} - - - - {t("placeHolderWithSize")} - - - - - - } - label={t("yes")} - /> - - } - label={t("no")} - /> - - - - - {t("placeHolderWithSizeDes")} - - - - - {t("tps")} - - - - - - - {t("odOnly")} - - - - {t("tpsBurst")} - - - - - - - {t("odOnly")} - - - - {t("usePathEndpoint")} - - - - - - } - label={t("yes")} - /> - - } - label={t("no")} - /> - - - - {t("s3Only")} - - - - {t("thumbExt")} - - - - - - - {t("thumbExtDes")} - - -
- -
-
-
- ); -} diff --git a/src/component/Admin/Policy/Guid/LocalGuide.js b/src/component/Admin/Policy/Guid/LocalGuide.js deleted file mode 100644 index 452e071..0000000 --- a/src/component/Admin/Policy/Guid/LocalGuide.js +++ /dev/null @@ -1,797 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { getNumber } from "../../../../utils"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, -})); - -const steps = [ - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function LocalGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState("false"); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "local", - Name: "", - DirNameRule: "uploads/{uid}/{path}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - BaseURL: "", - IsPrivate: "true", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - chunk_size: 25 << 20, - }, - } - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const checkPathSetting = (e) => { - e.preventDefault(); - setLoading(true); - - // 测试路径是否可用 - API.post("/admin/policy/test/path", { - path: policy.DirNameRule, - }) - .then(() => { - setActiveStep(1); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // 处理存储策略 - if (useCDN === "false" || policy.IsOriginLinkEnable === "false") { - policyCopy.BaseURL = ""; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(4); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- - {props.policy - ? t("editLocalStoragePolicy") - : t("addLocalStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - {activeStep === 0 && ( -
-
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- -
-
- )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("useCDN")} -
- {t("useCDNDes")} -
- -
- - { - if ( - e.target.value === "false" - ) { - setPolicy({ - ...policy, - BaseURL: "", - }); - } - setUseCDN(e.target.value); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
-
-
- - -
-
-
3
-
-
- - {t("cdnDomain")} - - -
- -
-
-
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("chunkSizeLabel")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
-
-
-
- - {t("nameThePolicy")} - -
- - - {t("policyName")} - - - -
-
-
-
- {" "} - -
- - )} - - {activeStep === 4 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/OSSGuide.js b/src/component/Admin/Policy/Guid/OSSGuide.js deleted file mode 100644 index f67eae3..0000000 --- a/src/component/Admin/Policy/Guid/OSSGuide.js +++ /dev/null @@ -1,1204 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import { getNumber } from "../../../../utils"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - }, -})); - -const steps = [ - { - title: "storageBucket", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "corsSettingStep", - optional: true, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function OSSGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped, setSkipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState("false"); - const [useLanEndpoint, setUseLanEndpoint] = useState( - props.policy && props.policy.OptionsSerialized.server_side_endpoint - ? props.policy.OptionsSerialized.server_side_endpoint !== "" - : false - ); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "oss", - Name: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - Server: "", - IsPrivate: "true", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - server_side_endpoint: "", - chunk_size: 25 << 20, - placeholder_with_size: "false", - }, - } - ); - const [policyID, setPolicyID] = useState( - props.policy ? props.policy.ID : 0 - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - if (useCDN === "false") { - policyCopy.BaseURL = ""; - } - - if (!useLanEndpoint) { - policyCopy.OptionsSerialized.server_side_endpoint = ""; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(4); - setPolicyID(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - const createCORS = () => { - setLoading(true); - API.post("/admin/policy/cors", { - id: policyID, - }) - .then(() => { - ToggleSnackbar("top", "right", t("corsPolicyAdded"), "success"); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
- - {props.policy - ? t("editOSSStoragePolicy") - : t("addOSSStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
0
-
-
- - ]} - /> - -
-
- -
-
-
1
-
-
- - , - , - , - , - ]} - /> - -
-
- -
-
-
2
-
-
- - ]} - /> - -
- - - {t("bucketName")} - - - -
-
-
- -
-
-
3
-
-
- - {t("bucketTypeDes")} - -
- - - - } - label={t("privateBucket")} - /> - - } - label={t("publicReadBucket")} - /> - - -
-
-
- -
-
-
4
-
-
- - , - , - , - ]} - /> - -
- - - {t("endpoint")} - - [\\w\\-]*)(?:\\.))?(?[\\w\\-]*))\\.(?[\\w\\-]*)", - title: t("endpointDomainOnly"), - }} - /> - -
-
-
- -
-
-
5
-
-
- - {t("ossLANEndpointDes")} - -
- - { - setUseLanEndpoint( - e.target.value === "true" - ); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
- -
- - - {t("intranetEndPoint")} - - [\\w\\-]*)(?:\\.))?(?[\\w\\-]*))\\.(?[\\w\\-]*)", - title: t("endpointDomainOnly"), - }} - /> - -
-
-
-
- -
-
-
6
-
-
- - {t("ossCDNDes")} - -
- - { - setUseCDN(e.target.value); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
-
-
- - -
-
-
7
-
-
- - , - ]} - /> - -
- -
-
-
-
- -
-
-
- {getNumber(7, [useCDN === "true"])} -
-
-
- - , - ]} - /> - -
- - - AccessKey ID - - - -
-
- - - Access Key Secret - - - -
-
-
- -
-
-
- {getNumber(8, [useCDN === "true"])} -
-
-
- - {t("nameThePolicyFirst")} - -
- - - {t("policyName")} - - - -
-
-
- -
- -
- - )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForPrivateBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
-
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("chunkSizeLabelOSS")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
-
-
- {getNumber(4, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("createPlaceholderDes")} - -
- - - - } - label={t("createPlaceholder")} - /> - - } - label={t("notCreatePlaceholder")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("ossCORSDes")} - -
- -
-
-
-
- {" "} -
- - )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/OneDriveGuide.js b/src/component/Admin/Policy/Guid/OneDriveGuide.js deleted file mode 100644 index 93d5225..0000000 --- a/src/component/Admin/Policy/Guid/OneDriveGuide.js +++ /dev/null @@ -1,1323 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import SizeInput from "../../Common/SizeInput"; -import AlertDialog from "../../Dialogs/Alert"; -import MagicVar from "../../Dialogs/MagicVar"; -import DomainInput from "../../Common/DomainInput"; -import { getNumber } from "../../../../utils"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - }, -})); - -const steps = [ - { - title: "applicationRegistration", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "grantAccess", - optional: false, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function OneDriveGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState( - props.policy && props.policy.OptionsSerialized.od_proxy - ? props.policy.OptionsSerialized.od_proxy !== "" - : false - ); - const [useSharePoint, setUseSharePoint] = useState( - props.policy && props.policy.OptionsSerialized.od_driver - ? props.policy.OptionsSerialized.od_driver !== "" - : false - ); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "onedrive", - Name: "", - BucketName: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - Server: "https://graph.microsoft.com/v1.0", - IsPrivate: "true", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - od_redirect: "", - od_proxy: "", - od_driver: "", - chunk_size: 50 << 20, - placeholder_with_size: "false", - tps_limit: "0", - tps_limit_burst: "0", - }, - } - ); - const [policyID, setPolicyID] = useState( - props.policy ? props.policy.ID : 0 - ); - const [httpsAlert, setHttpsAlert] = useState(false); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: ["siteURL"], - }) - .then((response) => { - if (!response.data.siteURL.startsWith("https://")) { - setHttpsAlert(true); - } - if (policy.OptionsSerialized.od_redirect === "") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - od_redirect: new URL( - "/api/v3/callback/onedrive/auth", - response.data.siteURL - ).toString(), - }, - }); - } - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - const statOAuth = () => { - setLoading(true); - API.get("/admin/policy/" + policyID + "/oauth/onedrive") - .then((response) => { - window.location.href = response.data; - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - setLoading(false); - }); - }; - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // baseURL处理 - if (policyCopy.Server === "https://graph.microsoft.com/v1.0") { - policyCopy.BaseURL = - "https://login.microsoftonline.com/common/oauth2/v2.0"; - } else { - policyCopy.BaseURL = "https://login.chinacloudapi.cn/common/oauth2"; - } - - if (!useCDN) { - policyCopy.OptionsSerialized.od_proxy = ""; - } - - if (!useSharePoint) { - policyCopy.OptionsSerialized.od_driver = ""; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(4); - setPolicyID(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- setHttpsAlert(false)} - title={t("warning")} - msg={t("odHttpsWarning")} - /> - - {props.policy - ? t("editOdStoragePolicy") - : t("addOdStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
1
-
-
- - , - , - , - ]} - /> - -
-
- -
-
-
2
-
-
- - , - , - ]} - /> - -
-
- -
-
-
3
-
-
- - , - , - , - , - , - ]} - /> - -
-
- -
-
-
4
-
-
- - , - , - ]} - /> - -
- - - {t("aadAppID")} - - - -
-
-
- -
-
-
5
-
-
- - , - , - , - , - ]} - /> - -
- - - {t("aadAppSecret")} - - - -
-
-
- -
-
-
6
-
-
- - {t("aadAccountCloudDes")} - -
- - - - } - label={t("multiTenant")} - /> - - } - label={t("gallatin")} - /> - - -
-
-
- -
-
-
7
-
-
- - {t("sharePointDes")} - -
- - { - setUseSharePoint( - e.target.value === "true" - ); - }} - row - > - - } - label={t("saveToSharePoint")} - /> - - } - label={t("saveToOneDrive")} - /> - - -
- -
- - - {t("spSiteURL")} - - - -
-
-
-
- -
-
-
8
-
-
- - {t("odReverseProxyURLDes")} - -
- - { - setUseCDN( - e.target.value === "true" - ); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
- -
- - - -
-
-
-
- -
-
-
9
-
-
- - {t("nameThePolicyFirst")} - -
- - - {t("policyName")} - - - -
-
-
- -
- -
- - )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForPrivateBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
-
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("chunkSizeLabelOd")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
-
-
- {getNumber(4, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("createPlaceholderDes")} - -
- - - - } - label={t("createPlaceholder")} - /> - - } - label={t("notCreatePlaceholder")} - /> - - -
-
-
- -
-
-
- {getNumber(5, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("limitOdTPSDes")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - tps_limit: "5.0", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - tps_limit: "0", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
- - -
- - - {t("tps")} - - - - {t("tpsDes")} - - -
-
- - - {t("tpsBurst")} - - - - {t("tpsBurstDes")} - - -
-
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {props.policy - ? t("policySaved") - : t("policyAdded")} - - - {t("odOauthDes")} - -
- -
-
-
-
- - )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/QiniuGuide.js b/src/component/Admin/Policy/Guid/QiniuGuide.js deleted file mode 100644 index da9e8a5..0000000 --- a/src/component/Admin/Policy/Guid/QiniuGuide.js +++ /dev/null @@ -1,1053 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import { getNumber } from "../../../../utils"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, -})); - -const steps = [ - { - title: "storageBucket", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function RemoteGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - // const [useCDN, setUseCDN] = useState("false"); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "qiniu", - Name: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - IsPrivate: "true", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - mimetype: "", - chunk_size: 25 << 20, - placeholder_with_size: "false", - }, - } - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- - {props.policy - ? t("editQiniuStoragePolicy") - : t("addQiniuStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
0
-
-
- - ]} - /> - -
-
- -
-
-
1
-
-
- - , - ]} - /> - -
-
- -
-
-
2
-
-
- - {t("enterQiniuBucket")} - -
- - - {t("qiniuBucketName")} - - - -
-
-
- -
-
-
3
-
-
- - {t("bucketTypeDes")} - -
- - - - } - label={t("privateBucket")} - /> - - } - label={t("publicBucket")} - /> - - -
-
-
- -
-
-
4
-
-
- - {t("bucketCDNDes")} - -
- -
-
-
- -
-
-
5
-
-
- - {t("qiniuCredentialDes")} - -
- - - {t("ak")} - - - -
-
- - - {t("sk")} - - - -
-
-
- -
- -
-
- )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForPrivateBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
{ - e.preventDefault(); - setActiveStep(4); - }} - > -
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("limitMimeType")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - mimetype: "image/*", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - mimetype: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {getNumber(4, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== - "", - ])} -
-
-
- - {t("mimeTypeDes")} - -
- - - {t("mimeTypeList")} - - - -
-
-
-
- -
-
-
- {getNumber(4, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - policy.OptionsSerialized.mimetype !== "", - ])} -
-
-
- - {t("chunkSizeLabelQiniu")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
-
-
- {getNumber(5, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - policy.OptionsSerialized.mimetype !== "", - ])} -
-
-
- - {t("createPlaceholderDes")} - -
- - - - } - label={t("createPlaceholder")} - /> - - } - label={t("notCreatePlaceholder")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("nameThePolicy")} - -
- - - {t("policyName")} - - - -
-
-
-
- {" "} - -
- - )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/RemoteGuide.js b/src/component/Admin/Policy/Guid/RemoteGuide.js deleted file mode 100644 index 3ec3005..0000000 --- a/src/component/Admin/Policy/Guid/RemoteGuide.js +++ /dev/null @@ -1,1025 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import Alert from "@material-ui/lab/Alert"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import { getNumber, randomStr } from "../../../../utils"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontSize: "14px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - }, - }, -})); - -const steps = [ - { - title: "storageNode", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function RemoteGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState("false"); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "remote", - Name: "", - Server: "https://example.com:5212", - SecretKey: randomStr(64), - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - BaseURL: "", - IsPrivate: "true", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - chunk_size: 25 << 20, - }, - } - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const testSlave = () => { - setLoading(true); - - // 测试路径是否可用 - API.post("/admin/policy/test/slave", { - server: policy.Server, - secret: policy.SecretKey, - }) - .then(() => { - ToggleSnackbar("top", "right", t("communicationOK"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // 处理存储策略 - if (useCDN === "false" || policy.IsOriginLinkEnable === "false") { - policyCopy.BaseURL = ""; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- - {props.policy - ? t("editRemoteStoragePolicy") - : t("addRemoteStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > - - {t("remoteDescription")} - - -
-
-
1
-
-
- - {t("remoteCopyBinaryDescription")} - -
-
- -
-
-
2
-
-
- - {t("remoteSecretDescription")} - -
- - - {t("remoteSecret")} - - - -
-
-
- -
-
-
3
-
-
- - {t("modifyRemoteConfig")} -
- ]} - /> -
-
-                                [System]
-                                
- Mode = slave -
- Listen = :5212 -
-
- [Slave] -
- Secret = {policy.SecretKey} -
-
- [CORS] -
- AllowOrigins = *
- AllowMethods = OPTIONS,GET,POST -
- AllowHeaders = *
-
- - {t("remoteConfigDifference")} -
    -
  • - , - , - , - ]} - /> -
  • -
  • - , - , - ]} - /> -
  • -
  • - ]} - /> -
  • -
-
-
-
- -
-
-
4
-
-
- - {t("inputRemoteAddress")} -
- {t("inputRemoteAddressDes")} -
-
- - - {t("remoteAddress")} - - - -
-
-
- -
-
-
5
-
-
- - {t("testCommunicationDes")} - -
- -
-
-
- -
- -
- - )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - 是否需要对存储的物理文件进行重命名?此处的重命名不会影响最终呈现给用户的 - 文件名。文件名也可使用魔法变量, - 可用魔法变量可参考{" "} - { - e.preventDefault(); - setMagicVar("file"); - }} - > - 文件名魔法变量列表 - {" "} - 。 - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("useCDN")} -
- {t("useCDNDes")} -
- -
- - { - if ( - e.target.value === "false" - ) { - setPolicy({ - ...policy, - BaseURL: "", - }); - } - setUseCDN(e.target.value); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
-
-
- - -
-
-
3
-
-
- - {t("cdnDomain")} - - -
- -
-
-
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
{ - e.preventDefault(); - setActiveStep(4); - }} - > -
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("chunkSizeLabel")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("nameThePolicy")} - -
- - - {t("policyName")} - - - -
-
-
-
- {" "} - -
-
- )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/S3Guide.js b/src/component/Admin/Policy/Guid/S3Guide.js deleted file mode 100644 index 7a69568..0000000 --- a/src/component/Admin/Policy/Guid/S3Guide.js +++ /dev/null @@ -1,1183 +0,0 @@ -import { lighten, makeStyles } from "@material-ui/core/styles"; -import React, { useCallback, useState } from "react"; -import Stepper from "@material-ui/core/Stepper"; -import StepLabel from "@material-ui/core/StepLabel"; -import Step from "@material-ui/core/Step"; -import Typography from "@material-ui/core/Typography"; -import { useDispatch } from "react-redux"; -import Link from "@material-ui/core/Link"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import Input from "@material-ui/core/Input"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Radio from "@material-ui/core/Radio"; -import Collapse from "@material-ui/core/Collapse"; -import Button from "@material-ui/core/Button"; -import API from "../../../../middleware/Api"; -import MagicVar from "../../Dialogs/MagicVar"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import { useHistory } from "react-router"; -import { getNumber } from "../../../../utils"; -import Autocomplete from "@material-ui/lab/Autocomplete"; -import TextField from "@material-ui/core/TextField"; -import AlertDialog from "../../Dialogs/Alert"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, - viewButtonLabel: { textTransform: "none" }, - "@global": { - code: { - color: "rgba(0, 0, 0, 0.87)", - display: "inline-block", - padding: "2px 6px", - fontFamily: - ' Consolas, "Liberation Mono", Menlo, Courier, monospace', - borderRadius: "2px", - backgroundColor: "rgba(255,229,100,0.1)", - }, - pre: { - margin: "24px 0", - padding: "12px 18px", - overflow: "auto", - direction: "ltr", - borderRadius: "4px", - backgroundColor: "#272c34", - color: "#fff", - }, - }, -})); - -const steps = [ - { - title: "storageBucket", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "corsSettingStep", - optional: true, - }, - { - title: "finishStep", - optional: false, - }, -]; - -const regions = { - "us-east-2": "US East (Ohio)", - "us-east-1": "US East (N. Virginia)", - "us-west-1": "US West (N. California)", - "us-west-2": "US West (Oregon)", - "af-south-1": "Africa (Cape Town)", - "ap-east-1": "Asia Pacific (Hong Kong)", - "ap-south-1": "Asia Pacific (Mumbai)", - "ap-northeast-3": "Asia Pacific (Osaka-Local)", - "ap-northeast-2": "Asia Pacific (Seoul)", - "ap-southeast-1": "Asia Pacific (Singapore)", - "ap-southeast-2": "Asia Pacific (Sydney)", - "ap-northeast-1": "Asia Pacific (Tokyo)", - "ca-central-1": "Canada (Central)", - "cn-north-1": "China (Beijing)", - "cn-northwest-1": "China (Ningxia)", - "eu-central-1": "Europe (Frankfurt)", - "eu-west-1": "Europe (Ireland)", - "eu-west-2": "Europe (London)", - "eu-south-1": "Europe (Milan)", - "eu-west-3": "Europe (Paris)", - "eu-north-1": "Europe (Stockholm)", - "me-south-1": "Middle East (Bahrain)", - "sa-east-1": "South America (São Paulo)", -}; - -export default function S3Guide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [alertOpen, setAlertOpen] = useState(true); - const [skipped, setSkipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [useCDN, setUseCDN] = useState("false"); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "s3", - Name: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - Server: "", - IsPrivate: "true", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - region: "us-east-2", - chunk_size: 25 << 20, - placeholder_with_size: "false", - s3_path_style: "true", - }, - } - ); - const [policyID, setPolicyID] = useState( - props.policy ? props.policy.ID : 0 - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - if (useCDN === "false") { - policyCopy.BaseURL = ""; - } - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(4); - setPolicyID(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - const createCORS = () => { - setLoading(true); - API.post("/admin/policy/cors", { - id: policyID, - }) - .then(() => { - ToggleSnackbar("top", "right", t("corsPolicyAdded"), "success"); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
- setAlertOpen(false)} - title={t("warning")} - msg={t("s3SelfHostWarning")} - /> - - {props.policy - ? t("editS3StoragePolicy") - : t("addS3StoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - - {t("optional")} - - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
1
-
-
- - ]} - /> - -
- - - {t("bucketName")} - - - -
-
-
- -
-
-
2
-
-
- - {t("bucketTypeDes")} - -
- - - - } - label={t("publicAccessDisabled")} - /> - - } - label={t("publicAccessEnabled")} - /> - - -
-
-
- -
-
-
3
-
-
- - ]} - /> - -
- - - {t("endpoint")} - - - -
-
-
- -
-
-
4
-
-
- - ]} - /> - -
- - - - } - label={t("usePathEndpoint")} - /> - - } - label={t("useHostnameEndpoint")} - /> - - -
-
-
- -
-
-
5
-
-
- - {t("selectRegionDes")} - -
- - - handleOptionChange("region")({ - target: { value: value }, - }) - } - renderOption={(option) => ( - - {regions[option]} - - )} - renderInput={(params) => ( - - )} - /> - -
-
-
- -
-
-
6
-
-
- - {t("useCDN")} - -
- - { - setUseCDN(e.target.value); - }} - row - > - - } - label={t("use")} - /> - - } - label={t("notUse")} - /> - - -
-
-
- - -
-
-
7
-
-
- - {t("bucketCDNDomain")} - -
- -
-
-
-
- -
-
-
- {getNumber(7, [useCDN === "true"])} -
-
-
- - {t("enterAccessCredentials")} - -
- - - {t("accessKey")} - - - -
-
- - - {t("secretKey")} - - - -
-
-
- -
-
-
- {getNumber(8, [useCDN === "true"])} -
-
-
- - {t("nameThePolicyFirst")} - -
- - - {t("policyName")} - - - -
-
-
- -
- -
-
- )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForPrivateBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
-
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
-
-
- {getNumber(3, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("chunkSizeLabelS3")} -
- {t("chunkSizeDes")} -
-
- -
-
-
- -
-
-
- {getNumber(4, [ - policy.MaxSize !== "0", - policy.OptionsSerialized.file_type !== "", - ])} -
-
-
- - {t("createPlaceholderDes")} - -
- - - - } - label={t("createPlaceholder")} - /> - - } - label={t("notCreatePlaceholder")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("ossCORSDes")} - -
- -
-
-
-
- {" "} -
- - )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Guid/UpyunGuide.js b/src/component/Admin/Policy/Guid/UpyunGuide.js deleted file mode 100644 index 226cabf..0000000 --- a/src/component/Admin/Policy/Guid/UpyunGuide.js +++ /dev/null @@ -1,903 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Collapse from "@material-ui/core/Collapse"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; -import Stepper from "@material-ui/core/Stepper"; -import { lighten, makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../../redux/explorer"; -import API from "../../../../middleware/Api"; -import DomainInput from "../../Common/DomainInput"; -import SizeInput from "../../Common/SizeInput"; -import MagicVar from "../../Dialogs/MagicVar"; -import { Trans, useTranslation } from "react-i18next"; -import { transformPolicyRequest } from "../utils"; - -const useStyles = makeStyles((theme) => ({ - stepContent: { - padding: "16px 32px 16px 32px", - }, - form: { - maxWidth: 400, - marginTop: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - subStepContainer: { - display: "flex", - marginBottom: 20, - padding: 10, - transition: theme.transitions.create("background-color", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - "&:focus-within": { - backgroundColor: theme.palette.background.default, - }, - }, - stepNumber: { - width: 20, - height: 20, - backgroundColor: lighten(theme.palette.secondary.light, 0.2), - color: theme.palette.secondary.contrastText, - textAlign: "center", - borderRadius: " 50%", - }, - stepNumberContainer: { - marginRight: 10, - }, - stepFooter: { - marginTop: 32, - }, - button: { - marginRight: theme.spacing(1), - }, -})); - -const steps = [ - { - title: "storageBucket", - optional: false, - }, - { - title: "storagePathStep", - optional: false, - }, - { - title: "sourceLinkStep", - optional: false, - }, - { - title: "uploadSettingStep", - optional: false, - }, - { - title: "finishStep", - optional: false, - }, -]; - -export default function UpyunGuide(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - const history = useHistory(); - - const [activeStep, setActiveStep] = useState(0); - const [loading, setLoading] = useState(false); - const [skipped] = React.useState(new Set()); - const [magicVar, setMagicVar] = useState(""); - const [policy, setPolicy] = useState( - props.policy - ? props.policy - : { - Type: "upyun", - Name: "", - SecretKey: "", - AccessKey: "", - BaseURL: "", - IsPrivate: "false", - DirNameRule: "uploads/{year}/{month}/{day}", - AutoRename: "true", - FileNameRule: "{randomkey8}_{originname}", - IsOriginLinkEnable: "false", - MaxSize: "0", - OptionsSerialized: { - file_type: "", - token: "", - }, - } - ); - - const handleChange = (name) => (event) => { - setPolicy({ - ...policy, - [name]: event.target.value, - }); - }; - - const handleOptionChange = (name) => (event) => { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - [name]: event.target.value, - }, - }); - }; - - const isStepSkipped = (step) => { - return skipped.has(step); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitPolicy = (e) => { - e.preventDefault(); - setLoading(true); - - let policyCopy = { ...policy }; - policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized }; - - // 类型转换 - policyCopy = transformPolicyRequest(policyCopy); - - API.post("/admin/policy", { - policy: policyCopy, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - props.policy ? t("policySaved") : t("policyAdded"), - "success" - ); - setActiveStep(5); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - - setLoading(false); - }; - - return ( -
- - {props.policy - ? t("editUpyunStoragePolicy") - : t("addUpyunStoragePolicy")} - - - {steps.map((label, index) => { - const stepProps = {}; - const labelProps = {}; - if (label.optional) { - labelProps.optional = ( - 可选 - ); - } - if (isStepSkipped(index)) { - stepProps.completed = false; - } - return ( - - - {t(label.title)} - - - ); - })} - - - {activeStep === 0 && ( -
{ - e.preventDefault(); - setActiveStep(1); - }} - > -
-
-
0
-
-
- - ]} - /> - -
-
- -
-
-
1
-
-
- - , - ]} - /> - -
-
- -
-
-
2
-
-
- - {t("storageServiceNameDes")} - -
- - - {t("storageServiceName")} - - - -
-
-
- -
-
-
3
-
-
- - {t("operatorNameDes")} - -
- - - {t("operatorName")} - - - -
-
- - - {t("operatorPassword")} - - - -
-
-
- -
-
-
4
-
-
- - {t("upyunCDNDes")} - -
- -
-
-
- -
-
-
5
-
-
- - {t("upyunOptionalDes")} -
- {t("upyunTokenDes")} -
-
- - - - } - label={t("tokenEnabled")} - /> - - } - label={t("tokenDisabled")} - /> - - -
-
-
- - -
-
-
6
-
-
- - {t("upyunTokenSecretDes")} - -
- - - {t("upyunTokenSecret")} - - - -
-
-
-
- -
- -
-
- )} - - {activeStep === 1 && ( -
{ - e.preventDefault(); - setActiveStep(2); - }} - > -
-
-
1
-
-
- - setMagicVar("path")} - />, - ]} - /> - -
- - - {t("pathOfFolderToStoreFiles")} - - - -
-
-
- -
-
-
2
-
-
- - setMagicVar("file")} - />, - ]} - /> - -
- - - - } - label={t("autoRenameStoredFile")} - /> - - } - label={t("keepOriginalFileName")} - /> - - -
- - -
- - - {t("renameRule")} - - - -
-
-
-
- -
- - -
-
- )} - - {activeStep === 2 && ( -
{ - e.preventDefault(); - setActiveStep(3); - }} - > -
-
-
1
-
-
- - {t("enableGettingPermanentSourceLink")} -
- {t("enableGettingPermanentSourceLinkDes")} -
- -
- - { - if ( - policy.IsPrivate === "true" && - e.target.value === "true" - ) { - ToggleSnackbar( - "top", - "right", - t( - "cannotEnableForTokenProtectedBucket" - ), - "warning" - ); - } - handleChange("IsOriginLinkEnable")( - e - ); - }} - row - > - - } - label={t("allowed")} - /> - - } - label={t("forbidden")} - /> - - -
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 3 && ( -
{ - e.preventDefault(); - setActiveStep(4); - }} - > -
-
-
1
-
-
- - {t("limitFileSize")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - MaxSize: "10485760", - }); - } else { - setPolicy({ - ...policy, - MaxSize: "0", - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
2
-
-
- - {t("enterSizeLimit")} - -
- -
-
-
-
- -
-
-
- {policy.MaxSize !== "0" ? "3" : "2"} -
-
-
- - {t("limitFileExt")} - - -
- - { - if (e.target.value === "true") { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: - "jpg,png,mp4,zip,rar", - }, - }); - } else { - setPolicy({ - ...policy, - OptionsSerialized: { - ...policy.OptionsSerialized, - file_type: "", - }, - }); - } - }} - row - > - - } - label={t("limit")} - /> - - } - label={t("notLimit")} - /> - - -
-
-
- - -
-
-
- {policy.MaxSize !== "0" ? "4" : "3"} -
-
-
- - {t("enterFileExt")} - -
- - - {t("extList")} - - - -
-
-
-
- -
- {" "} - -
-
- )} - - {activeStep === 4 && ( -
-
-
-
- - {t("nameThePolicy")} - -
- - - {t("policyName")} - - - -
-
-
-
- {" "} - -
-
- )} - - {activeStep === 5 && ( - <> -
- - {props.policy ? t("policySaved") : t("policyAdded")} - - - {t("furtherActions")} - -
-
- -
- - )} - - setMagicVar("")} - /> - setMagicVar("")} - /> -
- ); -} diff --git a/src/component/Admin/Policy/Policy.js b/src/component/Admin/Policy/Policy.js deleted file mode 100644 index f0a78f6..0000000 --- a/src/component/Admin/Policy/Policy.js +++ /dev/null @@ -1,299 +0,0 @@ -import Button from "@material-ui/core/Button"; -import IconButton from "@material-ui/core/IconButton"; -import Menu from "@material-ui/core/Menu"; -import MenuItem from "@material-ui/core/MenuItem"; -import Paper from "@material-ui/core/Paper"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import Tooltip from "@material-ui/core/Tooltip"; -import { Delete, Edit } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory, useLocation } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { sizeToString } from "../../../utils"; -import AddPolicy from "../Dialogs/AddPolicy"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, -})); - -const columns = [ - { id: "#", label: "sharp", minWidth: 50 }, - { id: "name", label: "name", minWidth: 170 }, - { id: "type", label: "type", minWidth: 170 }, - { - id: "count", - label: "childFiles", - minWidth: 50, - align: "right", - }, - { - id: "size", - label: "totalSize", - minWidth: 100, - align: "right", - }, - { - id: "action", - label: "actions", - minWidth: 170, - align: "right", - }, -]; - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -export default function Policy() { - const { t } = useTranslation("dashboard", { keyPrefix: "policy" }); - const classes = useStyles(); - // const [loading, setLoading] = useState(false); - // const [tab, setTab] = useState(0); - const [policies, setPolicies] = useState([]); - const [statics, setStatics] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [addDialog, setAddDialog] = useState(false); - const [filter, setFilter] = useState("all"); - const [anchorEl, setAnchorEl] = React.useState(null); - const [editID, setEditID] = React.useState(0); - - const location = useLocation(); - const history = useHistory(); - const query = useQuery(); - - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - if (query.get("code") === "0") { - ToggleSnackbar("top", "right", t("authSuccess"), "success"); - } else if (query.get("msg") && query.get("msg") !== "") { - ToggleSnackbar( - "top", - "right", - query.get("msg") + ", " + query.get("err"), - "warning" - ); - } - }, [location]); - - const loadList = () => { - API.post("/admin/policy/list", { - page: page, - page_size: pageSize, - order_by: "id desc", - conditions: filter === "all" ? {} : { type: filter }, - }) - .then((response) => { - setPolicies(response.data.items); - setStatics(response.data.statics); - setTotal(response.data.total); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, filter]); - - const deletePolicy = (id) => { - API.delete("/admin/policy/" + id) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("policyDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const open = Boolean(anchorEl); - - return ( -
- setAddDialog(false)} /> -
- -
- - -
-
- - - - - - - {columns.map((column) => ( - - {t(column.label)} - - ))} - - - - {policies.map((row) => ( - - {row.ID} - {row.Name} - {t(row.Type)} - - {statics[row.ID] !== undefined && - statics[row.ID][0].toLocaleString()} - - - {statics[row.ID] !== undefined && - sizeToString(statics[row.ID][1])} - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - { - setEditID(row.ID); - handleClick(e); - }} - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
- - { - handleClose(e); - history.push("/admin/policy/edit/pro/" + editID); - }} - > - {t("editInProMode")} - - { - handleClose(e); - history.push("/admin/policy/edit/guide/" + editID); - }} - > - {t("editInWizardMode")} - - -
- ); -} diff --git a/src/component/Admin/Policy/utils.js b/src/component/Admin/Policy/utils.js deleted file mode 100644 index 4121086..0000000 --- a/src/component/Admin/Policy/utils.js +++ /dev/null @@ -1,73 +0,0 @@ -const boolFields = ["IsOriginLinkEnable", "AutoRename", "IsPrivate"]; -const numberFields = ["MaxSize"]; -const boolFieldsInOptions = ["placeholder_with_size", "s3_path_style"]; -const numberFieldsInOptions = ["chunk_size", "tps_limit", "tps_limit_burst"]; -const listJsonFieldsInOptions = ["file_type", "thumb_exts"]; - -export const transformResponse = (response) => { - boolFields.forEach( - (field) => - (response.data[field] = response.data[field] ? "true" : "false") - ); - numberFields.forEach( - (field) => (response.data[field] = response.data[field].toString()) - ); - boolFieldsInOptions.forEach( - (field) => - (response.data.OptionsSerialized[field] = response.data - .OptionsSerialized[field] - ? "true" - : "false") - ); - numberFieldsInOptions.forEach( - (field) => - (response.data.OptionsSerialized[field] = response.data - .OptionsSerialized[field] - ? response.data.OptionsSerialized[field].toString() - : 0) - ); - - listJsonFieldsInOptions.forEach((field) => { - response.data.OptionsSerialized[field] = response.data - .OptionsSerialized[field] - ? response.data.OptionsSerialized[field].join(",") - : ""; - }); - return response; -}; - -export const transformPolicyRequest = (policyCopy) => { - boolFields.forEach( - (field) => (policyCopy[field] = policyCopy[field] === "true") - ); - numberFields.forEach( - (field) => (policyCopy[field] = parseInt(policyCopy[field])) - ); - boolFieldsInOptions.forEach( - (field) => - (policyCopy.OptionsSerialized[field] = - policyCopy.OptionsSerialized[field] === "true") - ); - numberFieldsInOptions.forEach( - (field) => - (policyCopy.OptionsSerialized[field] = parseInt( - policyCopy.OptionsSerialized[field] - )) - ); - - listJsonFieldsInOptions.forEach((field) => { - policyCopy.OptionsSerialized[field] = policyCopy.OptionsSerialized[ - field - ] - ? policyCopy.OptionsSerialized[field].split(",") - : []; - if ( - policyCopy.OptionsSerialized[field].length === 1 && - policyCopy.OptionsSerialized[field][0] === "" - ) { - policyCopy.OptionsSerialized[field] = []; - } - }); - - return policyCopy; -}; diff --git a/src/component/Admin/Setting/Access.js b/src/component/Admin/Setting/Access.js deleted file mode 100644 index 85c9419..0000000 --- a/src/component/Admin/Setting/Access.js +++ /dev/null @@ -1,325 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Switch from "@material-ui/core/Switch"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import AlertDialog from "../Dialogs/Alert"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -export default function Access() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" }); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - register_enabled: "1", - default_group: "1", - email_active: "0", - login_captcha: "0", - reg_captcha: "0", - forget_captcha: "0", - authn_enabled: "0", - }); - const [siteURL, setSiteURL] = useState(""); - const [groups, setGroups] = useState([]); - const [httpAlert, setHttpAlert] = useState(false); - - const handleChange = (name) => (event) => { - let value = event.target.value; - if (event.target.checked !== undefined) { - value = event.target.checked ? "1" : "0"; - } - setOptions({ - ...options, - [name]: value, - }); - }; - - const handleInputChange = (name) => (event) => { - const value = event.target.value; - setOptions({ - ...options, - [name]: value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: [...Object.keys(options), "siteURL"], - }) - .then((response) => { - setSiteURL(response.data.siteURL); - delete response.data.siteURL; - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - - API.get("/admin/groups") - .then((response) => { - setGroups(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
- setHttpAlert(false)} - open={httpAlert} - /> -
-
- - {t("accountManagement")} - -
-
- - - } - label={t("allowNewRegistrations")} - /> - - {t("allowNewRegistrationsDes")} - - -
- -
- - - } - label={t("emailActivation")} - /> - - {t("emailActivationDes")} - - -
- -
- - - } - label={t("captchaForSignup")} - /> - - {t("captchaForSignupDes")} - - -
- -
- - - } - label={t("captchaForLogin")} - /> - - {t("captchaForLoginDes")} - - -
- -
- - - } - label={t("captchaForReset")} - /> - - {t("captchaForResetDes")} - - -
- -
- - { - if ( - !siteURL.startsWith( - "https://" - ) - ) { - setHttpAlert(true); - return; - } - handleChange("authn_enabled")( - e - ); - }} - /> - } - label={t("webauthn")} - /> - - {t("webauthnDes")} - - -
- -
- - - {t("defaultGroup")} - - - - {t("defaultGroupDes")} - - -
-
-
- -
- -
-
-
- ); -} diff --git a/src/component/Admin/Setting/Captcha.js b/src/component/Admin/Setting/Captcha.js deleted file mode 100644 index 0634fa2..0000000 --- a/src/component/Admin/Setting/Captcha.js +++ /dev/null @@ -1,537 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Button from "@material-ui/core/Button"; -import API from "../../../middleware/Api"; -import { useDispatch } from "react-redux"; -import Select from "@material-ui/core/Select"; -import MenuItem from "@material-ui/core/MenuItem"; -import Input from "@material-ui/core/Input"; -import Link from "@material-ui/core/Link"; -import { toggleSnackbar } from "../../../redux/explorer"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Switch from "@material-ui/core/Switch"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -export default function Captcha() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - captcha_type: "normal", - captcha_height: "1", - captcha_width: "1", - captcha_mode: "3", - captcha_CaptchaLen: "6", - captcha_ComplexOfNoiseText: "0", - captcha_ComplexOfNoiseDot: "0", - captcha_IsShowHollowLine: "0", - captcha_IsShowNoiseDot: "0", - captcha_IsShowNoiseText: "0", - captcha_IsShowSlimeLine: "0", - captcha_IsShowSineLine: "0", - captcha_ReCaptchaKey: "", - captcha_ReCaptchaSecret: "", - captcha_TCaptcha_CaptchaAppId: "", - captcha_TCaptcha_AppSecretKey: "", - captcha_TCaptcha_SecretId: "", - captcha_TCaptcha_SecretKey: "", - }); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleCheckChange = (name) => (event) => { - const value = event.target.checked ? "1" : "0"; - setOptions({ - ...options, - [name]: value, - }); - }; - - return ( -
-
-
- - {t("captcha")} - -
-
- - - {t("captchaType")} - - - - {t("captchaProvider")} - - -
-
-
- - {options.captcha_type === "normal" && ( -
- - {t("plainCaptchaTitle")} - -
-
- - - {t("captchaWidth")} - - - -
- -
- - - {t("captchaHeight")} - - - -
- -
- - - {t("captchaLength")} - - - -
-
- - - {t("captchaMode")} - - - - {t("captchaElement")} - - -
- {[ - { - name: "complexOfNoiseText", - field: "captcha_ComplexOfNoiseText", - }, - { - name: "complexOfNoiseDot", - field: "captcha_ComplexOfNoiseDot", - }, - { - name: "showHollowLine", - field: "captcha_IsShowHollowLine", - }, - { - name: "showNoiseDot", - field: "captcha_IsShowNoiseDot", - }, - { - name: "showNoiseText", - field: "captcha_IsShowNoiseText", - }, - { - name: "showSlimeLine", - field: "captcha_IsShowSlimeLine", - }, - { - name: "showSineLine", - field: "captcha_IsShowSineLine", - }, - ].map((input) => ( -
- - - } - label={t(input.name)} - /> - -
- ))} -
-
- )} - - {options.captcha_type === "recaptcha" && ( -
- - {t("reCaptchaV2")} - -
-
-
- - - {t("siteKey")} - - - - , - ]} - /> - - -
- -
- - - {t("siteSecret")} - - - - , - ]} - /> - - -
-
-
-
- )} - - {options.captcha_type === "tcaptcha" && ( -
- - {t("tencentCloudCaptcha")} - -
-
-
- - - {t("secretID")} - - - - , - ]} - /> - - -
- -
- - - {t("secretKey")} - - - - , - ]} - /> - - -
- -
- - - {t("tCaptchaAppID")} - - - - , - ]} - /> - - -
- -
- - - {t("tCaptchaSecretKey")} - - - - , - ]} - /> - - -
-
-
-
- )} - -
- -
-
-
- ); -} diff --git a/src/component/Admin/Setting/Image.js b/src/component/Admin/Setting/Image.js deleted file mode 100644 index dfc6253..0000000 --- a/src/component/Admin/Setting/Image.js +++ /dev/null @@ -1,667 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import SizeInput from "../Common/SizeInput"; -import Alert from "@material-ui/lab/Alert"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Switch from "@material-ui/core/Switch"; -import { Trans, useTranslation } from "react-i18next"; -import Link from "@material-ui/core/Link"; -import ThumbGenerators from "./ThumbGenerators"; -import PolicySelector from "../Common/PolicySelector"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -export default function ImageSetting() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - gravatar_server: "", - avatar_path: "", - avatar_size: "", - avatar_size_l: "", - avatar_size_m: "", - avatar_size_s: "", - thumb_width: "", - thumb_height: "", - office_preview_service: "", - thumb_file_suffix: "", - thumb_max_task_count: "", - thumb_encode_method: "", - thumb_gc_after_gen: "0", - thumb_encode_quality: "", - maxEditSize: "", - wopi_enabled: "0", - wopi_endpoint: "", - wopi_session_timeout: "0", - thumb_builtin_enabled: "0", - thumb_vips_enabled: "0", - thumb_vips_exts: "", - thumb_ffmpeg_enabled: "0", - thumb_vips_path: "", - thumb_ffmpeg_path: "", - thumb_ffmpeg_exts: "", - thumb_ffmpeg_seek: "", - thumb_libreoffice_path: "", - thumb_libreoffice_enabled: "0", - thumb_libreoffice_exts: "", - thumb_proxy_enabled: "0", - thumb_proxy_policy: [], - thumb_max_src_size: "", - thumb_libraw_enabled: "0", - thumb_libraw_path: "", - thumb_libraw_exts: "", - }); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - response.data.thumb_proxy_policy = JSON.parse( - response.data.thumb_proxy_policy - ).map((v) => { - return v.toString(); - }); - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const reload = () => { - API.get("/admin/reload/wopi") - // eslint-disable-next-line @typescript-eslint/no-empty-function - .then(() => {}) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - // eslint-disable-next-line @typescript-eslint/no-empty-function - .then(() => {}); - }; - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - let value = options[k]; - if (k === "thumb_proxy_policy") { - value = JSON.stringify(value.map((v) => parseInt(v))); - } - - option.push({ - key: k, - value, - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - reload(); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleCheckChange = (name) => (event) => { - const value = event.target.checked ? "1" : "0"; - setOptions({ - ...options, - [name]: value, - }); - }; - - return ( -
-
-
- - {t("avatar")} - -
-
- - - {t("gravatarServer")} - - - - {t("gravatarServerDes")} - - -
- -
- - - {t("avatarFilePath")} - - - - {t("avatarFilePathDes")} - - -
- -
- - {options.avatar_size !== "" && ( - - )} - - {t("avatarSizeDes")} - - -
- -
- - - {t("smallAvatarSize")} - - - -
- -
- - - {t("mediumAvatarSize")} - - - -
- -
- - - {t("largeAvatarSize")} - - - -
-
-
- -
- - {t("filePreview")} - - -
-
- - - {t("officePreviewService")} - - - - {t("officePreviewServiceDes")} -
- {"{$src}"} -{" "} - {t("officePreviewServiceSrcDes")} -
- {"{$srcB64}"} -{" "} - {t("officePreviewServiceSrcB64Des")} -
- {"{$name}"} -{" "} - {t("officePreviewServiceName")} -
-
-
- -
- - {options.maxEditSize !== "" && ( - - )} - - - {t("textEditMaxSizeDes")} - - -
-
-
- -
- - {t("wopiClient")} - - -
-
- - , - ]} - /> - -
- -
- - - } - label={t("enableWopi")} - /> - -
- - {options.wopi_enabled === "1" && ( - <> -
- - - {t("wopiEndpoint")} - - - - {t("wopiEndpointDes")} - - -
- -
- - - {t("wopiSessionTtl")} - - - - {t("wopiSessionTtlDes")} - - -
- - )} -
-
- -
- - {t("thumbnails")} - -
- - , - ]} - /> - -
- - {t("thumbnailBasic")} - - -
-
- - - {t("thumbWidth")} - - - -
- -
- - - {t("thumbHeight")} - - - -
- -
- - - {t("thumbSuffix")} - - - -
- -
- - - {t("thumbConcurrent")} - - - - {t("thumbConcurrentDes")} - - -
- -
- - - {t("thumbFormat")} - - - - {t("thumbFormatDes")} - - -
- -
- - - {t("thumbQuality")} - - - - {t("thumbQualityDes")} - - -
- -
- - {options.thumb_max_src_size !== "" && ( - - )} - - {t("thumbMaxSizeDes")} - - -
- -
- - - } - label={t("thumbGC")} - /> - -
-
- - - {t("generators")} - -
-
- -
-
- - - {t("generatorProxy")} - -
-
- - {t("generatorProxyWarning")} - -
- -
- - - } - label={t("enableThumbProxy")} - /> - -
- {options.thumb_proxy_enabled === "1" && ( - <> -
- t.Type !== "local"} - label={t("proxyPolicyList")} - helperText={t("proxyPolicyListDes")} - /> -
- - )} -
-
- -
- -
-
-
- ); -} diff --git a/src/component/Admin/Setting/Mail.js b/src/component/Admin/Setting/Mail.js deleted file mode 100644 index 1df2443..0000000 --- a/src/component/Admin/Setting/Mail.js +++ /dev/null @@ -1,429 +0,0 @@ -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import { makeStyles } from "@material-ui/core/styles"; -import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Switch from "@material-ui/core/Switch"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - buttonMargin: { - marginLeft: 8, - }, -})); - -export default function Mail() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" }); - const { t: tGlobal } = useTranslation("common"); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [test, setTest] = useState(false); - const [tesInput, setTestInput] = useState(""); - const [options, setOptions] = useState({ - fromName: "", - fromAdress: "", - smtpHost: "", - smtpPort: "", - replyTo: "", - smtpUser: "", - smtpPass: "", - smtpEncryption: "", - mail_keepalive: "30", - mail_activation_template: "", - mail_reset_pwd_template: "", - }); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const handleCheckChange = (name) => (event) => { - let value = event.target.value; - if (event.target.checked !== undefined) { - value = event.target.checked ? "1" : "0"; - } - setOptions({ - ...options, - [name]: value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const sendTestMail = () => { - setLoading(true); - API.post("/admin/test/mail", { - to: tesInput, - }) - .then(() => { - ToggleSnackbar("top", "right", t("testMailSent"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const reload = () => { - API.get("/admin/reload/email") - // eslint-disable-next-line @typescript-eslint/no-empty-function - .then(() => {}) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - // eslint-disable-next-line @typescript-eslint/no-empty-function - .then(() => {}); - }; - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - reload(); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
- setTest(false)} - aria-labelledby="form-dialog-title" - > - - {t("testSMTPSettings")} - - - - {t("testSMTPTooltip")} - - setTestInput(e.target.value)} - type="email" - fullWidth - /> - - - - - - - -
-
- - {t("smtp")} - - -
-
- - - {t("senderName")} - - - - {t("senderNameDes")} - - -
- -
- - - {t("senderAddress")} - - - - {t("senderAddressDes")} - - -
- -
- - - {t("smtpServer")} - - - - {t("smtpServerDes")} - - -
- -
- - - {t("smtpPort")} - - - - {t("smtpPortDes")} - - -
- -
- - - {t("smtpUsername")} - - - - {t("smtpUsernameDes")} - - -
- -
- - - {t("smtpPassword")} - - - - {t("smtpPasswordDes")} - - -
- -
- - - {t("replyToAddress")} - - - - {t("replyToAddressDes")} - - -
- -
- - - } - label={t("enforceSSL")} - /> - - {t("enforceSSLDes")} - - -
- -
- - - {t("smtpTTL")} - - - - {t("smtpTTLDes")} - - -
-
-
- -
- - {t("emailTemplates")} - - -
-
- - - {t("activateNewUser")} - - - - {t("activateNewUserDes")} - - -
- -
- - - {t("resetPassword")} - - - - {t("resetPasswordDes")} - - -
-
-
- -
- - {" "} - -
-
-
- ); -} diff --git a/src/component/Admin/Setting/SiteInformation.js b/src/component/Admin/Setting/SiteInformation.js deleted file mode 100644 index 58116b0..0000000 --- a/src/component/Admin/Setting/SiteInformation.js +++ /dev/null @@ -1,309 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -export default function SiteInformation() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - siteURL: "", - siteName: "", - siteTitle: "", - siteDes: "", - siteScript: "", - pwa_small_icon: "", - pwa_medium_icon: "", - pwa_large_icon: "", - pwa_display: "", - pwa_theme_color: "", - pwa_background_color: "", - }); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
-
-
- - {t("basicInformation")} - -
-
- - - {t("mainTitle")} - - - - {t("mainTitleDes")} - - -
-
- - - {t("subTitle")} - - - - {t("subTitleDes")} - - -
-
- - - {t("siteDescription")} - - - - {t("siteDescriptionDes")} - - -
-
- - - {t("siteURL")} - - - - {t("siteURLDes")} - - -
-
- - - {t("customFooterHTML")} - - - - {t("customFooterHTMLDes")} - - -
-
-
-
- - {t("pwa")} - -
-
- - - {t("smallIcon")} - - - - {t("smallIconDes")} - - -
-
- - - {t("mediumIcon")} - - - - {t("mediumIconDes")} - - -
-
- - - {t("largeIcon")} - - - - {t("largeIconDes")} - - -
-
- - - {t("displayMode")} - - - - {t("displayModeDes")} - - -
-
- - - {t("themeColor")} - - - - {t("themeColorDes")} - - -
-
-
-
- - - {t("backgroundColor")} - - - - {t("backgroundColorDes")} - - -
-
-
-
- -
-
-
- ); -} diff --git a/src/component/Admin/Setting/Theme.js b/src/component/Admin/Setting/Theme.js deleted file mode 100644 index 63e7e53..0000000 --- a/src/component/Admin/Setting/Theme.js +++ /dev/null @@ -1,465 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import IconButton from "@material-ui/core/IconButton"; -import InputLabel from "@material-ui/core/InputLabel"; -import Link from "@material-ui/core/Link"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; -import { Delete } from "@material-ui/icons"; -import Alert from "@material-ui/lab/Alert"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import CreateTheme from "../Dialogs/CreateTheme"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 500, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, - colorContainer: { - display: "flex", - }, - colorDot: { - width: 20, - height: 20, - borderRadius: "50%", - marginLeft: 6, - }, -})); - -export default function Theme() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const { t: tApp } = useTranslation(); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [theme, setTheme] = useState({}); - const [options, setOptions] = useState({ - themes: "{}", - defaultTheme: "", - home_view_method: "icon", - share_view_method: "list", - }); - const [themeConfig, setThemeConfig] = useState({}); - const [themeConfigError, setThemeConfigError] = useState({}); - const [create, setCreate] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const deleteTheme = (color) => { - if (color === options.defaultTheme) { - ToggleSnackbar( - "top", - "right", - t("cannotDeleteDefaultTheme"), - "warning" - ); - return; - } - if (Object.keys(theme).length <= 1) { - ToggleSnackbar("top", "right", t("keepAtLeastOneTheme"), "warning"); - return; - } - const themeCopy = { ...theme }; - delete themeCopy[color]; - const resStr = JSON.stringify(themeCopy); - setOptions({ - ...options, - themes: resStr, - }); - }; - - const addTheme = (newTheme) => { - setCreate(false); - if (theme[newTheme.palette.primary.main] !== undefined) { - ToggleSnackbar( - "top", - "right", - t("duplicatedThemePrimaryColor"), - "warning" - ); - return; - } - const res = { - ...theme, - [newTheme.palette.primary.main]: newTheme, - }; - const resStr = JSON.stringify(res); - setOptions({ - ...options, - themes: resStr, - }); - }; - - useEffect(() => { - const res = JSON.parse(options.themes); - const themeString = {}; - - Object.keys(res).forEach((k) => { - themeString[k] = JSON.stringify(res[k]); - }); - - setTheme(res); - setThemeConfig(themeString); - }, [options.themes]); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
-
-
- - {t("themes")} - -
-
- - - - {t("colors")} - - {t("themeConfig")} - - {t("actions")} - - - - {Object.keys(theme).map((k) => ( - - -
-
-
-
- - - { - setThemeConfig({ - ...themeConfig, - [k]: e.target.value, - }); - }} - onBlur={(e) => { - try { - const res = JSON.parse( - e.target.value - ); - if ( - !( - "palette" in - res - ) || - !( - "primary" in - res.palette - ) || - !( - "main" in - res.palette - .primary - ) || - !( - "secondary" in - res.palette - ) || - !( - "main" in - res.palette - .secondary - ) - ) { - throw e; - } - setTheme({ - ...theme, - [k]: res, - }); - } catch (e) { - setThemeConfigError( - { - ...themeConfigError, - [k]: true, - } - ); - return; - } - setThemeConfigError({ - ...themeConfigError, - [k]: false, - }); - }} - value={themeConfig[k]} - /> - - - - deleteTheme(k) - } - > - - - - - ))} - -
-
- -
- - - , - ]} - /> - - -
- -
- - - {t("defaultTheme")} - - - - {t("defaultThemeDes")} - - -
-
-
- -
- - {t("appearance")} - - -
-
- - - {t("personalFileListView")} - - - - {t("personalFileListViewDes")} - - -
-
- -
-
- - - {t("sharedFileListView")} - - - - {t("sharedFileListViewDes")} - - -
-
-
- -
- -
-
- - setCreate(false)} - /> -
- ); -} diff --git a/src/component/Admin/Setting/ThumbGenerators.js b/src/component/Admin/Setting/ThumbGenerators.js deleted file mode 100644 index 814f193..0000000 --- a/src/component/Admin/Setting/ThumbGenerators.js +++ /dev/null @@ -1,261 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Accordion from "@material-ui/core/Accordion"; -import AccordionSummary from "@material-ui/core/AccordionSummary"; -import AccordionDetails from "@material-ui/core/AccordionDetails"; -import Checkbox from "@material-ui/core/Checkbox"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Typography from "@material-ui/core/Typography"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; -import { useTranslation } from "react-i18next"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import { Button, TextField } from "@material-ui/core"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import API from "../../../middleware/Api"; - -const useStyles = makeStyles((theme) => ({ - root: { - width: "100%", - }, - secondaryHeading: { - fontSize: theme.typography.pxToRem(15), - color: theme.palette.text.secondary, - }, - column: { - flexBasis: "33.33%", - }, - details: { - display: "block", - }, -})); - -const generators = [ - { - name: "policyBuiltin", - des: "policyBuiltinDes", - readOnly: true, - }, - { - name: "libreOffice", - des: "libreOfficeDes", - enableFlag: "thumb_libreoffice_enabled", - executableSetting: "thumb_libreoffice_path", - inputs: [ - { - name: "thumb_libreoffice_exts", - label: "generatorExts", - des: "generatorExtsDes", - }, - ], - }, - { - name: "vips", - des: "vipsDes", - enableFlag: "thumb_vips_enabled", - executableSetting: "thumb_vips_path", - inputs: [ - { - name: "thumb_vips_exts", - label: "generatorExts", - des: "generatorExtsDes", - }, - ], - }, - { - name: "ffmpeg", - des: "ffmpegDes", - enableFlag: "thumb_ffmpeg_enabled", - executableSetting: "thumb_ffmpeg_path", - inputs: [ - { - name: "thumb_ffmpeg_exts", - label: "generatorExts", - des: "generatorExtsDes", - }, - { - name: "thumb_ffmpeg_seek", - label: "ffmpegSeek", - des: "ffmpegSeekDes", - required: true, - }, - ], - }, - { - name: "libRaw", - des: "libRawDes", - enableFlag: "thumb_libraw_enabled", - executableSetting: "thumb_libraw_path", - inputs: [ - { - name: "thumb_libraw_exts", - label: "generatorExts", - des: "generatorExtsDes", - }, - ], - }, - { - name: "cloudreveBuiltin", - des: "cloudreveBuiltinDes", - enableFlag: "thumb_builtin_enabled", - }, -]; - -export default function ThumbGenerators({ options, setOptions }) { - const classes = useStyles(); - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const [loading, setLoading] = useState(false); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const testExecutable = (name, executable) => { - setLoading(true); - API.post("/admin/test/thumb", { - name, - executable, - }) - .then((response) => { - ToggleSnackbar( - "top", - "right", - t("executableTestSuccess", { version: response.data }), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleEnableChange = (name) => (event) => { - const newOpts = { - ...options, - [name]: event.target.checked ? "1" : "0", - }; - setOptions(newOpts); - - if ( - newOpts["thumb_libreoffice_enabled"] === "1" && - newOpts["thumb_builtin_enabled"] === "0" && - newOpts["thumb_vips_enabled"] === "0" - ) { - ToggleSnackbar( - "top", - "center", - t("thumbDependencyWarning"), - "warning" - ); - } - }; - - return ( -
- {generators.map((generator) => ( - - } - aria-label="Expand" - aria-controls="additional-actions1-content" - id="additional-actions1-header" - > - event.stopPropagation()} - onFocus={(event) => event.stopPropagation()} - control={ - - } - label={t(generator.name)} - disabled={generator.readOnly} - /> - - - - {t(generator.des)} - - {generator.executableSetting && ( - - - - - ), - }} - required - /> - - {t("executableDes")} - - - )} - {generator.inputs && - generator.inputs.map((input) => ( - - - - {t(input.des)} - - - ))} - - - ))} -
- ); -} diff --git a/src/component/Admin/Setting/UploadDownload.js b/src/component/Admin/Setting/UploadDownload.js deleted file mode 100644 index 7a5ca9f..0000000 --- a/src/component/Admin/Setting/UploadDownload.js +++ /dev/null @@ -1,407 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import { makeStyles } from "@material-ui/core/styles"; -import Switch from "@material-ui/core/Switch"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import SizeInput from "../Common/SizeInput"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); - -export default function UploadDownload() { - const { t } = useTranslation("dashboard", { keyPrefix: "settings" }); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [options, setOptions] = useState({ - max_worker_num: "1", - max_parallel_transfer: "1", - temp_path: "", - chunk_retries: "0", - archive_timeout: "0", - download_timeout: "0", - preview_timeout: "0", - doc_preview_timeout: "0", - upload_credential_timeout: "0", - upload_session_timeout: "0", - slave_api_timeout: "0", - onedrive_monitor_timeout: "0", - share_download_session_timeout: "0", - onedrive_callback_check: "0", - reset_after_upload_failed: "0", - onedrive_source_timeout: "0", - slave_node_retry: "0", - slave_ping_interval: "0", - slave_recover_interval: "0", - slave_transfer_timeout: "0", - use_temp_chunk_buffer: "1", - public_resource_maxage: "0", - }); - - const handleCheckChange = (name) => (event) => { - const value = event.target.checked ? "1" : "0"; - setOptions({ - ...options, - [name]: value, - }); - }; - - const handleChange = (name) => (event) => { - setOptions({ - ...options, - [name]: event.target.value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.post("/admin/setting", { - keys: Object.keys(options), - }) - .then((response) => { - setOptions(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, []); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - const option = []; - Object.keys(options).forEach((k) => { - option.push({ - key: k, - value: options[k], - }); - }); - API.patch("/admin/setting", { - options: option, - }) - .then(() => { - ToggleSnackbar("top", "right", t("saved"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - return ( -
-
-
- - {t("transportation")} - -
-
- - - {t("workerNum")} - - - - {t("workerNumDes")} - - -
- -
- - - {t("transitParallelNum")} - - - - {t("transitParallelNumDes")} - - -
- -
- - - {t("tempFolder")} - - - - {t("tempFolderDes")} - - -
- -
- - - {t("failedChunkRetry")} - - - - {t("failedChunkRetryDes")} - - -
- -
- - - } - label={t("cacheChunks")} - /> - - {t("cacheChunksDes")} - - -
- -
- - - } - label={t("resetConnection")} - /> - - {t("resetConnectionDes")} - - -
-
-
- -
- - {t("expirationDuration")} - -
- {[ - { - name: "batchDownload", - field: "archive_timeout", - }, - { - name: "downloadSession", - field: "download_timeout", - }, - { - name: "previewURL", - field: "preview_timeout", - }, - { - name: "docPreviewURL", - field: "doc_preview_timeout", - }, - { - name: "staticResourceCache", - field: "public_resource_maxage", - des: "staticResourceCacheDes", - }, - { - name: "uploadSession", - field: "upload_session_timeout", - des: "uploadSessionDes", - }, - { - name: "downloadSessionForShared", - field: "share_download_session_timeout", - des: "downloadSessionForSharedDes", - }, - { - name: "onedriveMonitorInterval", - field: "onedrive_monitor_timeout", - des: "onedriveMonitorIntervalDes", - }, - { - name: "onedriveCallbackTolerance", - field: "onedrive_callback_check", - des: "onedriveCallbackToleranceDes", - }, - { - name: "onedriveDownloadURLCache", - field: "onedrive_source_timeout", - des: "onedriveDownloadURLCacheDes", - }, - ].map((input) => ( -
- - - {t(input.name)} - - - {input.des && ( - - {t(input.des)} - - )} - -
- ))} -
-
- -
- - {t("nodesCommunication")} - -
- {[ - { - name: "slaveAPIExpiration", - field: "slave_api_timeout", - des: "slaveAPIExpirationDes", - }, - { - name: "heartbeatInterval", - field: "slave_ping_interval", - des: "heartbeatIntervalDes", - }, - { - name: "heartbeatFailThreshold", - field: "slave_node_retry", - des: "heartbeatFailThresholdDes", - }, - { - name: "heartbeatRecoverModeInterval", - field: "slave_recover_interval", - des: "heartbeatRecoverModeIntervalDes", - }, - { - name: "slaveTransitExpiration", - field: "slave_transfer_timeout", - des: "slaveTransitExpirationDes", - }, - ].map((input) => ( -
- - - {t(input.name)} - - - - {t(input.des)} - - -
- ))} -
-
- -
- -
-
-
- ); -} diff --git a/src/component/Admin/Settings/Appearance/Appearance.tsx b/src/component/Admin/Settings/Appearance/Appearance.tsx new file mode 100644 index 0000000..4827ee3 --- /dev/null +++ b/src/component/Admin/Settings/Appearance/Appearance.tsx @@ -0,0 +1,28 @@ +import { Box, Stack } from "@mui/material"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { SettingSection } from "../Settings"; +import { SettingContext } from "../SettingWrapper"; +import ThemeOptions from "./ThemeOptions"; + +const Appearance = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + return ( + e.preventDefault()}> + + + setSettings({ theme_options: value })} + defaultTheme={values.defaultTheme || ""} + onDefaultThemeChange={(value: string) => setSettings({ defaultTheme: value })} + /> + + + + ); +}; + +export default Appearance; diff --git a/src/component/Admin/Settings/Appearance/ThemeOptionEditDialog.tsx b/src/component/Admin/Settings/Appearance/ThemeOptionEditDialog.tsx new file mode 100644 index 0000000..da9e42c --- /dev/null +++ b/src/component/Admin/Settings/Appearance/ThemeOptionEditDialog.tsx @@ -0,0 +1,220 @@ +import { + Badge, + Box, + Button, + createTheme, + DialogContent, + Divider, + Grid, + Link, + Paper, + Stack, + TextField, + ThemeProvider, + Typography, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { applyThemeWithOverrides } from "../../../../App"; +import CircularProgress from "../../../Common/CircularProgress"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import DraggableDialog from "../../../Dialogs/DraggableDialog"; +import SideNavItem from "../../../Frame/NavBar/SideNavItem"; +import Setting from "../../../Icons/Setting"; + +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor")); + +export interface ThemeOptionEditDialogProps { + open: boolean; + onClose: () => void; + id: string; + config: string; + onSave: (id: string, newId: string, config: string) => void; +} + +const ThemeOptionEditDialog = ({ open, onClose, id, config, onSave }: ThemeOptionEditDialogProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + const [editedConfig, setEditedConfig] = useState(config); + const [parsedConfig, setParsedConfig] = useState(null); + + useEffect(() => { + try { + setParsedConfig(JSON.parse(config)); + } catch (e) { + setParsedConfig(null); + } + }, [config]); + + useEffect(() => { + try { + setParsedConfig(JSON.parse(editedConfig)); + } catch (e) { + // Don't update parsedConfig if JSON is invalid + } + }, [editedConfig]); + + const handleSave = useCallback(() => { + try { + // Validate JSON + const parsed = JSON.parse(editedConfig); + // make sure minimum config is provided + if (!parsed.light?.palette?.primary?.main) { + throw new Error("Invalid theme config"); + } + // Get the new primary color (ID) + const newId = parsed.light.palette.primary.main; + onSave(id, newId, editedConfig); + } catch (e) { + enqueueSnackbar({ + message: t("settings.invalidThemeConfig"), + variant: "warning", + action: DefaultCloseAction, + }); + } + }, [editedConfig, id, onSave, enqueueSnackbar, t]); + + // Create preview themes + const lightTheme = useMemo(() => { + if (!parsedConfig) return null; + try { + return createTheme({ + palette: { + mode: "light", + ...parsedConfig.light.palette, + }, + }); + } catch (e) { + return null; + } + }, [parsedConfig]); + + const darkTheme = useMemo(() => { + if (!parsedConfig) return null; + try { + return createTheme({ + palette: { + mode: "dark", + ...parsedConfig.dark.palette, + }, + }); + } catch (e) { + return null; + } + }, [parsedConfig]); + + return ( + + + + + + {t("settings.themeConfiguration")} + + }> + setEditedConfig(e as string)} + /> + + + , + ]} + /> + + + + + {t("settings.themePreview")} + + + + {t("settings.lightTheme")} + + {lightTheme ? ( + + + + {t("settings.previewTitle")} + } + /> + + + + + + + + + + ) : ( + {t("settings.invalidThemePreview")} + )} + + + + + + + {t("settings.darkTheme")} + + {darkTheme ? ( + + + + {t("settings.previewTitle")} + } + /> + + + + + + + + + + ) : ( + {t("settings.invalidThemePreview")} + )} + + + + + + ); +}; + +export default ThemeOptionEditDialog; diff --git a/src/component/Admin/Settings/Appearance/ThemeOptions.tsx b/src/component/Admin/Settings/Appearance/ThemeOptions.tsx new file mode 100644 index 0000000..9e462f5 --- /dev/null +++ b/src/component/Admin/Settings/Appearance/ThemeOptions.tsx @@ -0,0 +1,320 @@ +import { + Box, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import { NoWrapTableCell, SecondaryButton, StyledCheckbox, StyledTableContainerPaper } from "../../../Common/StyledComponents"; +import Add from "../../../Icons/Add"; +import Delete from "../../../Icons/Delete"; +import Edit from "../../../Icons/Edit"; +import HexColorInput from "../../Settings/Filesystem/HexColorInput"; +import ThemeOptionEditDialog from "./ThemeOptionEditDialog"; + +export interface ThemeOptionsProps { + value: string; + onChange: (value: string) => void; + defaultTheme: string; + onDefaultThemeChange: (value: string) => void; +} + +interface ThemeOption { + id: string; + config: { + light: { + palette: { + primary: { + main: string; + light?: string; + dark?: string; + }; + secondary: { + main: string; + light?: string; + dark?: string; + }; + }; + }; + dark: { + palette: { + primary: { + main: string; + light?: string; + dark?: string; + }; + secondary: { + main: string; + light?: string; + dark?: string; + }; + }; + }; + }; +} + +const ThemeOptions = ({ value, onChange, defaultTheme, onDefaultThemeChange }: ThemeOptionsProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + const [options, setOptions] = useState>({}); + const [editingOption, setEditingOption] = useState<{ id: string; config: ThemeOption["config"] } | null>(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + useEffect(() => { + try { + const parsedOptions = JSON.parse(value); + setOptions(parsedOptions); + } catch (e) { + setOptions({}); + } + }, [value]); + + const handleSave = useCallback((newOptions: Record) => { + onChange(JSON.stringify(newOptions)); + }, [onChange]); + + const handleDelete = useCallback((id: string) => { + // Prevent deleting the default theme + if (id === defaultTheme) { + enqueueSnackbar({ + message: t("settings.cannotDeleteDefaultTheme"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + const newOptions = { ...options }; + delete newOptions[id]; + handleSave(newOptions); + }, [options, handleSave, defaultTheme, enqueueSnackbar, t]); + + const handleEdit = useCallback((id: string) => { + setEditingOption({ id, config: options[id] }); + setIsDialogOpen(true); + }, [options]); + + const handleAdd = useCallback(() => { + // Generate a new default theme option with a random color + const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`; + setEditingOption({ + id: randomColor, + config: { + light: { + palette: { + primary: { main: randomColor }, + secondary: { main: "#f50057" } + } + }, + dark: { + palette: { + primary: { main: randomColor }, + secondary: { main: "#f50057" } + } + } + } + }); + setIsDialogOpen(true); + }, []); + + const handleDialogClose = useCallback(() => { + setIsDialogOpen(false); + setEditingOption(null); + }, []); + + const handleDialogSave = useCallback((id: string, newId: string, config: string) => { + try { + const parsedConfig = JSON.parse(config); + const newOptions = { ...options }; + + // If ID has changed (primary color changed), delete the old entry and create a new one + if (id !== newId) { + // Check if the new ID already exists + if (newOptions[newId]) { + enqueueSnackbar({ + message: t("settings.duplicateThemeColor"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + // If we're changing the ID of the default theme, update the default theme reference + if (id === defaultTheme) { + onDefaultThemeChange(newId); + } + + delete newOptions[id]; + } + + newOptions[newId] = parsedConfig; + handleSave(newOptions); + setIsDialogOpen(false); + setEditingOption(null); + } catch (e) { + // Handle error + enqueueSnackbar({ + message: t("settings.invalidThemeConfig"), + variant: "warning", + action: DefaultCloseAction, + }); + } + }, [options, handleSave, enqueueSnackbar, defaultTheme, onDefaultThemeChange, t]); + + const handleColorChange = useCallback((id: string, type: 'primary' | 'secondary', mode: 'light' | 'dark', color: string) => { + const newOptions = { ...options }; + + if (type === 'primary' && mode === 'light') { + // If changing the primary color (which is the ID), we need to create a new entry + const newId = color; + + // Check if the new ID already exists + if (newOptions[newId] && newId !== id) { + enqueueSnackbar({ + message: t("settings.duplicateThemeColor"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + const config = { ...newOptions[id] }; + config[mode].palette[type].main = color; + + // Delete old entry and create new one with the updated ID + delete newOptions[id]; + newOptions[newId] = config; + + // If we're changing the ID of the default theme, update the default theme reference + if (id === defaultTheme) { + onDefaultThemeChange(newId); + } + } else { + // For other colors, just update the value + newOptions[id][mode].palette[type].main = color; + } + + handleSave(newOptions); + }, [options, handleSave, enqueueSnackbar, t, defaultTheme, onDefaultThemeChange]); + + const handleDefaultThemeChange = useCallback((id: string) => { + onDefaultThemeChange(id); + }, [onDefaultThemeChange]); + + const optionsArray = useMemo(() => { + return Object.entries(options).map(([id, config]) => ({ + id, + config, + })); + }, [options]); + + return ( + + + {t("settings.themeOptions")} + + + {t("settings.themeOptionsDes")} + + + {optionsArray.length > 0 && ( + + + + + {t("settings.defaultTheme")} + {t("settings.primaryColor")} + {t("settings.secondaryColor")} + {t("settings.primaryColorDark")} + {t("settings.secondaryColorDark")} + + + + + {optionsArray.map((option) => ( + + + handleDefaultThemeChange(option.id)} + /> + + + handleColorChange(option.id, 'primary', 'light', color)} + /> + + + handleColorChange(option.id, 'secondary', 'light', color)} + /> + + + handleColorChange(option.id, 'primary', 'dark', color)} + /> + + + handleColorChange(option.id, 'secondary', 'dark', color)} + /> + + + handleEdit(option.id)}> + + + handleDelete(option.id)} + disabled={optionsArray.length === 1 || option.id === defaultTheme} + > + + + + + ))} + +
+
+ )} + + } + onClick={handleAdd} + sx={{ mt: 2 }} + > + {t("settings.addThemeOption")} + + + {editingOption && ( + + )} +
+ ); +}; + +export default ThemeOptions; \ No newline at end of file diff --git a/src/component/Admin/Settings/Captcha/Captcha.tsx b/src/component/Admin/Settings/Captcha/Captcha.tsx new file mode 100644 index 0000000..1a8b134 --- /dev/null +++ b/src/component/Admin/Settings/Captcha/Captcha.tsx @@ -0,0 +1,170 @@ +import { useTranslation } from "react-i18next"; +import * as React from "react"; +import { useContext } from "react"; +import { SettingContext } from "../SettingWrapper.tsx"; +import { + Box, + Collapse, + FormControl, + FormControlLabel, + ListItemText, + Stack, + Switch, + Typography, +} from "@mui/material"; +import { + NoMarginHelperText, + SettingSection, + SettingSectionContent, +} from "../Settings.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { isTrueVal } from "../../../../session/utils.ts"; +import { DenseSelect } from "../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import { CaptchaType } from "../../../../api/site.ts"; +import GraphicCaptcha from "./GraphicCaptcha.tsx"; +import ReCaptcha from "./ReCaptcha.tsx"; +import TurnstileCaptcha from "./TurnstileCaptcha.tsx"; + +const Captcha = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + return ( + e.preventDefault()}> + + + + {t("nav.captcha")} + + + + + + setSettings({ + login_captcha: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.captchaForLogin")} + /> + + {t("settings.captchaForLoginDes")} + + + + + + + setSettings({ + reg_captcha: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.captchaForSignup")} + /> + + {t("settings.captchaForSignupDes")} + + + + + + + setSettings({ + forget_captcha: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.captchaForReset")} + /> + + {t("settings.captchaForResetDes")} + + + + + + + + {t("settings.captchaType")} + + + + + + setSettings({ + captcha_type: e.target.value as string, + }) + } + value={values.captcha_type} + > + + + {t("settings.plainCaptcha")} + + + + + {t("settings.reCaptchaV2")} + + + + + {t("settings.turnstile")} + + + + + {t("settings.captchaTypeDes")} + + + + + + + + + + + + + + + + + ); +}; + +export default Captcha; diff --git a/src/component/Admin/Settings/Captcha/GraphicCaptcha.tsx b/src/component/Admin/Settings/Captcha/GraphicCaptcha.tsx new file mode 100644 index 0000000..3652dfb --- /dev/null +++ b/src/component/Admin/Settings/Captcha/GraphicCaptcha.tsx @@ -0,0 +1,105 @@ +import { useTranslation } from "react-i18next"; +import { + FormControl, + FormControlLabel, + ListItemText, + Stack, + Switch, +} from "@mui/material"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { DenseSelect } from "../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import * as React from "react"; +import { isTrueVal } from "../../../../session/utils.ts"; + +export interface GraphicCaptchaProps { + values: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; +} + +const GraphicCaptcha = ({ values, setSettings }: GraphicCaptchaProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + + setSettings({ + captcha_mode: e.target.value as string, + }) + } + value={values.captcha_mode} + > + {[ + "captchaModeNumber", + "captchaModeLetter", + "captchaModeMath", + "captchaModeNumberLetter", + ].map((k, i) => ( + + + {t(`settings.${k}`)} + + + ))} + + + + {[ + { + name: "complexOfNoiseText", + field: "captcha_ComplexOfNoiseText", + }, + { + name: "complexOfNoiseDot", + field: "captcha_ComplexOfNoiseDot", + }, + { + name: "showHollowLine", + field: "captcha_IsShowHollowLine", + }, + { + name: "showNoiseDot", + field: "captcha_IsShowNoiseDot", + }, + { + name: "showNoiseText", + field: "captcha_IsShowNoiseText", + }, + { + name: "showSlimeLine", + field: "captcha_IsShowSlimeLine", + }, + { + name: "showSineLine", + field: "captcha_IsShowSineLine", + }, + ].map((v) => ( + + + + setSettings({ + [v.field]: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t(`settings.${v.name}`)} + /> + + + ))} + + ); +}; + +export default GraphicCaptcha; diff --git a/src/component/Admin/Settings/Captcha/ReCaptcha.tsx b/src/component/Admin/Settings/Captcha/ReCaptcha.tsx new file mode 100644 index 0000000..d97e17f --- /dev/null +++ b/src/component/Admin/Settings/Captcha/ReCaptcha.tsx @@ -0,0 +1,75 @@ +import { Trans, useTranslation } from "react-i18next"; +import { FormControl, Link, Stack } from "@mui/material"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import * as React from "react"; +import { NoMarginHelperText } from "../Settings.tsx"; + +export interface ReCaptchaProps { + values: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; +} + +const ReCaptcha = ({ values, setSettings }: ReCaptchaProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + + setSettings({ + captcha_ReCaptchaKey: e.target.value, + }) + } + required + /> + + , + ]} + /> + + + + + + + setSettings({ + captcha_ReCaptchaSecret: e.target.value, + }) + } + required + /> + + , + ]} + /> + + + + + ); +}; + +export default ReCaptcha; diff --git a/src/component/Admin/Settings/Captcha/TurnstileCaptcha.tsx b/src/component/Admin/Settings/Captcha/TurnstileCaptcha.tsx new file mode 100644 index 0000000..616e26f --- /dev/null +++ b/src/component/Admin/Settings/Captcha/TurnstileCaptcha.tsx @@ -0,0 +1,75 @@ +import { Trans, useTranslation } from "react-i18next"; +import { FormControl, Link, Stack } from "@mui/material"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import * as React from "react"; +import { NoMarginHelperText } from "../Settings.tsx"; + +export interface TurnstileCaptchaProps { + values: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; +} + +const Turnstile = ({ values, setSettings }: TurnstileCaptchaProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + + setSettings({ + captcha_turnstile_site_key: e.target.value, + }) + } + required + /> + + , + ]} + /> + + + + + + + setSettings({ + captcha_turnstile_site_secret: e.target.value, + }) + } + required + /> + + , + ]} + /> + + + + + ); +}; + +export default Turnstile; diff --git a/src/component/Admin/Settings/Email/Email.tsx b/src/component/Admin/Settings/Email/Email.tsx new file mode 100644 index 0000000..09606e7 --- /dev/null +++ b/src/component/Admin/Settings/Email/Email.tsx @@ -0,0 +1,239 @@ +import { + Box, + DialogContent, + FormControl, + FormControlLabel, + Stack, + Switch, + Typography +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendTestSMTP } from "../../../../api/api.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { isTrueVal } from "../../../../session/utils.ts"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents.tsx"; +import DraggableDialog, { StyledDialogContentText } from "../../../Dialogs/DraggableDialog.tsx"; +import MailOutlined from "../../../Icons/MailOutlined.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { + NoMarginHelperText, + SettingSection, + SettingSectionContent, +} from "../Settings.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import EmailTemplates from "./EmailTemplates.tsx"; + +const Email = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const { formRef, setSettings, values } = useContext(SettingContext); + const [testEmailOpen, setTestEmailOpen] = useState(false); + const [testEmailAddress, setTestEmailAddress] = useState(""); + const [sending, setSending] = useState(false); + + const handleTestEmail = async () => { + setSending(true); + try { + await dispatch(sendTestSMTP({ + to: testEmailAddress, + settings: values, + })); + enqueueSnackbar({ + message: t("settings.testMailSent"), + variant: "success", + action: DefaultCloseAction, + }); + setTestEmailOpen(false); + } catch (error) { + } finally { + setSending(false); + } + }; + + return ( + e.preventDefault()}> + + setTestEmailOpen(false), + }} + loading={sending} + showActions + showCancel + onAccept={handleTestEmail} + title={t("settings.testSMTPSettings")} + > + + + {t("settings.testSMTPTooltip")} + + + setTestEmailAddress(e.target.value)} + type="email" + fullWidth + /> + + + + + + {t("settings.smtp")} + + + + setSettings({ fromName: e.target.value })} + /> + + {t("settings.senderNameDes")} + + + + + + + setSettings({ fromAdress: e.target.value })} + /> + + {t("settings.senderAddressDes")} + + + + + + + setSettings({ smtpHost: e.target.value })} + /> + + {t("settings.smtpServerDes")} + + + + + + + setSettings({ smtpPort: e.target.value })} + /> + + {t("settings.smtpPortDes")} + + + + + + + setSettings({ smtpUser: e.target.value })} + /> + + {t("settings.smtpUsernameDes")} + + + + + + + setSettings({ smtpPass: e.target.value })} + /> + + {t("settings.smtpPasswordDes")} + + + + + + + setSettings({ replyTo: e.target.value })} + /> + + {t("settings.replyToAddressDes")} + + + + + + + + setSettings({ smtpEncryption: e.target.checked ? "1" : "0" }) + } + /> + } + label={t("settings.enforceSSL")} + /> + + {t("settings.enforceSSLDes")} + + + + + + + setSettings({ mail_keepalive: e.target.value })} + /> + + {t("settings.smtpTTLDes")} + + + + + + } + onClick={() => setTestEmailOpen(true)} + > + {t("settings.sendTestEmail")} + + + + + + {/* Email Templates Section */} + + + + ); +}; + +export default Email; \ No newline at end of file diff --git a/src/component/Admin/Settings/Email/EmailTemplateEditor.tsx b/src/component/Admin/Settings/Email/EmailTemplateEditor.tsx new file mode 100644 index 0000000..752e702 --- /dev/null +++ b/src/component/Admin/Settings/Email/EmailTemplateEditor.tsx @@ -0,0 +1,236 @@ +import { Add } from "@mui/icons-material"; +import { + Box, + Button, + DialogContent, + FormControl, + Link, + ListItemText, + Tab, + Tabs, + Typography, + useTheme, +} from "@mui/material"; +import React, { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { languages } from "../../../../i18n.ts"; +import CircularProgress from "../../../Common/CircularProgress.tsx"; +import { DenseFilledTextField, DenseSelect } from "../../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import MagicVarDialog from "../../Common/MagicVarDialog.tsx"; +import { NoMarginHelperText } from "../Settings.tsx"; + +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor.tsx")); + +interface TemplateItem { + language: string; + title: string; + body: string; +} + +interface EmailTemplateEditorProps { + value: string; + onChange: (value: string) => void; + templateType: string; + magicVars: MagicVar[]; +} + +const EmailTemplateEditor: React.FC = ({ value, onChange, templateType, magicVars }) => { + const theme = useTheme(); + const { t } = useTranslation("dashboard"); + const [templates, setTemplates] = useState([]); + const [currentTab, setCurrentTab] = useState(0); + const [addLanguageOpen, setAddLanguageOpen] = useState(false); + const [newLanguageCode, setNewLanguageCode] = useState(""); + const isUpdatingFromProp = useRef(false); + const [magicVarOpen, setMagicVarOpen] = useState(false); + + // Parse templates when component mounts or value changes + useEffect(() => { + try { + isUpdatingFromProp.current = true; + const parsedTemplates = value ? JSON.parse(value) : []; + setTemplates(parsedTemplates); + // If no templates, create a default English one + if (parsedTemplates.length === 0) { + setTemplates([{ language: "en-US", title: "", body: "" }]); + } + } catch (e) { + console.error("Failed to parse email template:", e); + setTemplates([{ language: "en-US", title: "", body: "" }]); + } finally { + // Use setTimeout to ensure this runs after React finishes the update + setTimeout(() => { + isUpdatingFromProp.current = false; + }, 0); + } + }, [value]); + + // Update the parent component when templates change, but only from user interaction + useEffect(() => { + if (templates.length > 0 && !isUpdatingFromProp.current) { + onChange(JSON.stringify(templates)); + } + }, [templates, onChange]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); + }; + + const updateTemplate = (index: number, field: keyof TemplateItem, newValue: string) => { + isUpdatingFromProp.current = false; // Ensure this is a user interaction + const updatedTemplates = [...templates]; + updatedTemplates[index] = { + ...updatedTemplates[index], + [field]: newValue, + }; + setTemplates(updatedTemplates); + }; + + const addNewLanguage = () => { + if (!newLanguageCode.trim()) return; + + // Check if language already exists + if (templates.some((t) => t.language === newLanguageCode)) { + // Could show an error message here + setNewLanguageCode(""); + setAddLanguageOpen(false); + return; + } + + // Add new language template + isUpdatingFromProp.current = false; // Ensure this is a user interaction + setTemplates([...templates, { language: newLanguageCode, title: "", body: "" }]); + + // Reset and close dialog + setNewLanguageCode(""); + setAddLanguageOpen(false); + + // Switch to the new tab + setCurrentTab(templates.length); + }; + + const openMagicVar = useCallback((e: React.MouseEvent) => { + setMagicVarOpen(true); + e.stopPropagation(); + e.preventDefault(); + }, []); + + return ( + + + + {templates.map((template, index) => ( + + ))} + + + + {templates.map((template, index) => ( + + ))} + {/* Add Language Dialog */} + setAddLanguageOpen(false), + }} + > + + + + setNewLanguageCode(e.target.value as string)}> + {languages.map((l) => ( + + + {l.displayName} + + + ))} + + {t("settings.languageCodeDes")} + + + + + setMagicVarOpen(false)} /> + + ); +}; + +export default EmailTemplateEditor; diff --git a/src/component/Admin/Settings/Email/EmailTemplates.tsx b/src/component/Admin/Settings/Email/EmailTemplates.tsx new file mode 100644 index 0000000..b05f75e --- /dev/null +++ b/src/component/Admin/Settings/Email/EmailTemplates.tsx @@ -0,0 +1,176 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; +import { Box, Typography } from "@mui/material"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import React, { useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm.tsx"; +import { MagicVar } from "../../Common/MagicVarDialog.tsx"; +import ProDialog from "../../Common/ProDialog.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import { SettingSection, SettingSectionContent } from "../Settings.tsx"; +import { AccordionSummary, StyledAccordion } from "../UserSession/SSOSettings.tsx"; +import EmailTemplateEditor from "./EmailTemplateEditor.tsx"; +interface EmailTemplate { + key: string; + title: string; + description: string; + magicVars: MagicVar[]; + pro: boolean; +} + +const commonMagicVars: MagicVar[] = [ + { + value: "settings.mainTitle", + name: "{{ .CommonContext.SiteBasic.Name }}", + example: "Cloudreve", + }, + { + value: "settings.siteDescription", + name: "{{ .CommonContext.SiteBasic.Description }}", + example: "Another Cloudreve instance", + }, + { + value: "settings.siteID", + name: "{{ .CommonContext.SiteBasic.ID }}", + example: "123e4567-e89b-12d3-a456-426614174000", + }, + { + value: "settings.logo", + name: "{{ .CommonContext.Logo.Normal }}", + example: "https://cloudreve.org/logo.svg", + }, + { + value: "settings.logo", + name: "{{ .CommonContext.Logo.Light }}", + example: "https://cloudreve.org/logo_light.svg", + }, + { + value: "settings.siteURL", + name: "{{ .CommonContext.SiteUrl }}", + example: "https://cloudreve.org", + }, +]; + +const userMagicVars: MagicVar[] = [ + { + value: "policy.magicVar.uid", + name: "{{ .User.ID }}", + example: "2534", + }, + { + value: "application:login.email", + name: "{{ .User.Email }}", + example: "example@cloudreve.org", + }, + { + value: "application:setting.nickname", + name: "{{ .User.Nick }}", + example: "Aaron Liu", + }, + { + value: "user.usedStorage", + name: "{{ .User.Storage }}", + example: "123221000", + }, +]; + +const EmailTemplates: React.FC = () => { + const { t } = useTranslation("dashboard"); + const { setSettings, values } = useContext(SettingContext); + const [proOpen, setProOpen] = useState(false); + + // Template setting keys + const templateSettings = [ + { + key: "mail_receipt_template", + title: "receiptEmailTemplate", + description: "receiptEmailTemplateDes", + pro: true, + }, + { + key: "mail_activation_template", + title: "activationEmailTemplate", + description: "activationEmailTemplateDes", + magicVars: [ + ...commonMagicVars, + ...userMagicVars, + { + value: "settings.activateUrl", + name: "{{ .Url }}", + example: "https://cloudreve.org/activate", + }, + ], + }, + { + key: "mail_exceed_quota_template", + title: "quotaExceededEmailTemplate", + description: "quotaExceededEmailTemplateDes", + pro: true, + }, + { + key: "mail_reset_template", + title: "resetPasswordEmailTemplate", + description: "resetPasswordEmailTemplateDes", + magicVars: [ + ...commonMagicVars, + ...userMagicVars, + { + value: "settings.resetUrl", + name: "{{ .Url }}", + example: "https://cloudreve.org/reset", + }, + ], + }, + ]; + + const handleProClick = (e: React.MouseEvent) => { + e.preventDefault(); + setProOpen(true); + }; + + return ( + + setProOpen(false)} /> + + {t("settings.emailTemplates")} + + + + {templateSettings.map((template) => ( + + }> + + {t("settings." + template.title)} + {template.pro && } + + + + + {t("settings." + template.description)}{" "} + + + + setSettings({ [template.key]: value })} + templateType={template.key} + /> + + + + + ))} + + + + ); +}; + +export default EmailTemplates; diff --git a/src/component/Admin/Settings/Event/Events.tsx b/src/component/Admin/Settings/Event/Events.tsx new file mode 100644 index 0000000..c384327 --- /dev/null +++ b/src/component/Admin/Settings/Event/Events.tsx @@ -0,0 +1,195 @@ +import { + Box, + Checkbox, + Divider, + FormControl, + FormControlLabel, + FormGroup, + Grid, + Stack, + Typography, +} from "@mui/material"; +import { useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AuditLogType } from "../../../../api/explorer"; +import { ProChip } from "../../../Pages/Setting/SettingForm"; +import ProDialog from "../../Common/ProDialog"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings"; +import { SettingContext } from "../SettingWrapper"; + +// Categorize audit events +export const eventCategories = { + system: { + title: "settings.systemEvents", + description: "settings.systemEventsDes", + events: [AuditLogType.server_start], + }, + user: { + title: "settings.userEvents", + description: "settings.userEventsDes", + events: [ + AuditLogType.user_signup, + AuditLogType.user_activated, + AuditLogType.user_login, + AuditLogType.user_login_failed, + AuditLogType.user_token_refresh, + AuditLogType.user_changed, + AuditLogType.user_exceed_quota_notified, + AuditLogType.change_nick, + AuditLogType.change_avatar, + AuditLogType.change_password, + AuditLogType.enable_2fa, + AuditLogType.disable_2fa, + AuditLogType.add_passkey, + AuditLogType.remove_passkey, + AuditLogType.link_account, + AuditLogType.unlink_account, + ], + }, + file: { + title: "settings.fileEvents", + description: "settings.fileEventsDes", + events: [ + AuditLogType.file_create, + AuditLogType.file_rename, + AuditLogType.set_file_permission, + AuditLogType.entity_uploaded, + AuditLogType.entity_downloaded, + AuditLogType.copy_from, + AuditLogType.copy_to, + AuditLogType.move_to, + AuditLogType.delete_file, + AuditLogType.move_to_trash, + AuditLogType.update_metadata, + AuditLogType.get_direct_link, + ], + }, + share: { + title: "settings.shareEvents", + description: "settings.shareEventsDes", + events: [AuditLogType.share, AuditLogType.share_link_viewed, AuditLogType.edit_share, AuditLogType.delete_share], + }, + version: { + title: "settings.versionEvents", + description: "settings.versionEventsDes", + events: [AuditLogType.set_current_version, AuditLogType.delete_version], + }, + media: { + title: "settings.mediaEvents", + description: "settings.mediaEventsDes", + events: [AuditLogType.thumb_generated, AuditLogType.live_photo_uploaded], + }, + filesystem: { + title: "settings.filesystemEvents", + description: "settings.filesystemEventsDes", + events: [AuditLogType.mount, AuditLogType.relocate, AuditLogType.create_archive, AuditLogType.extract_archive], + }, + webdav: { + title: "settings.webdavEvents", + description: "settings.webdavEventsDes", + events: [ + AuditLogType.webdav_login_failed, + AuditLogType.webdav_account_create, + AuditLogType.webdav_account_update, + AuditLogType.webdav_account_delete, + ], + }, + payment: { + title: "settings.paymentEvents", + description: "settings.paymentEventsDes", + events: [ + AuditLogType.payment_created, + AuditLogType.points_change, + AuditLogType.payment_paid, + AuditLogType.payment_fulfilled, + AuditLogType.payment_fulfill_failed, + AuditLogType.storage_added, + AuditLogType.group_changed, + AuditLogType.membership_unsubscribe, + AuditLogType.redeem_gift_code, + ], + }, + email: { + title: "settings.emailEvents", + description: "settings.emailEventsDes", + events: [AuditLogType.email_sent], + }, +}; + +// Get event name from AuditLogType +export const getEventName = (eventType: number): string => { + return Object.entries(AuditLogType).find(([_, value]) => value === eventType)?.[0] || `event_${eventType}`; +}; + +const Events = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + const [proOpen, setProOpen] = useState(false); + + const handleProClick = (e: React.MouseEvent) => { + e.preventDefault(); + setProOpen(true); + }; + + return ( + e.preventDefault()}> + setProOpen(false)} /> + + + + {t("settings.auditLog")} + + + {t("settings.auditLogDes")} + + + {Object.entries(eventCategories).map(([categoryKey, category]) => ( + + + {t(category.title)} + + {t(category.description)} + + + + + + + } + label={t("settings.toggleAll")} + /> + {t("settings.toggleAllDes")} + + + + {category.events.map((eventType) => ( + + } + label={t(`settings.event.${getEventName(eventType)}`, getEventName(eventType))} + /> + + ))} + + + + + ))} + + + + ); +}; + +export default Events; diff --git a/src/component/Admin/Settings/Filesystem/EmojiList.tsx b/src/component/Admin/Settings/Filesystem/EmojiList.tsx new file mode 100644 index 0000000..6a9b73d --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/EmojiList.tsx @@ -0,0 +1,133 @@ +import { Box, IconButton, Stack, Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material"; +import { memo, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + DenseFilledTextField, + NoWrapCell, + NoWrapTableCell, + SecondaryButton, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; +import Dismiss from "../../../Icons/Dismiss.tsx"; + +export interface EmojiListProps { + config: string; + onChange: (value: string) => void; +} + +const EmojiList = memo(({ config, onChange }: EmojiListProps) => { + const { t } = useTranslation("dashboard"); + const [render, setRender] = useState(false); + const configParsed = useMemo((): { [key: string]: string[] } => JSON.parse(config), [config]); + const [inputCache, setInputCache] = useState<{ + [key: number]: string | undefined; + }>({}); + return ( + + + {!render && ( + setRender(!render)}> + {t("settings.showSettings")} + + )} + {render && Object.keys(configParsed ?? {}).length > 0 && ( + + + + + {t("settings.category")} + {t("settings.emojiOptions")} + + + + + {Object.keys(configParsed ?? {}).map((r, i) => ( + + + { + const newConfig = { + ...configParsed, + [e.target.value]: configParsed[r], + }; + delete newConfig[r]; + onChange(JSON.stringify(newConfig)); + }} + /> + + + { + onChange( + JSON.stringify({ + ...configParsed, + [r]: inputCache[i]?.split(",") ?? configParsed[r], + }), + ); + setInputCache({ + ...inputCache, + [i]: undefined, + }); + }} + onChange={(e) => + setInputCache({ + ...inputCache, + [i]: e.target.value, + }) + } + /> + + + + { + const newConfig = { + ...configParsed, + }; + + delete newConfig[r]; + onChange(JSON.stringify(newConfig)); + }} + size={"small"} + > + + + + + ))} + +
+
+ )} +
+ {render && ( + + } + onClick={() => + onChange( + JSON.stringify({ + ...configParsed, + [""]: [], + }), + ) + } + > + {t("settings.addCategorize")} + + + )} +
+ ); +}); + +export default EmojiList; diff --git a/src/component/Admin/Settings/Filesystem/FileIconList.tsx b/src/component/Admin/Settings/Filesystem/FileIconList.tsx new file mode 100644 index 0000000..8d37c86 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/FileIconList.tsx @@ -0,0 +1,259 @@ +import { + Box, + IconButton, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import * as React from "react"; +import { memo, useMemo, useState } from "react"; +import { + builtInIcons, + FileTypeIconSetting, +} from "../../../FileManager/Explorer/FileTypeIcon.tsx"; +import { + DenseFilledTextField, + NoWrapCell, + NoWrapTableCell, + SecondaryButton, + StyledTableContainerPaper, +} from "../../../Common/StyledComponents.tsx"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import HexColorInput from "./HexColorInput.tsx"; +import Dismiss from "../../../Icons/Dismiss.tsx"; +import Add from "../../../Icons/Add.tsx"; + +export interface FileIconListProps { + config: string; + onChange: (value: string) => void; +} + +const IconPreview = ({ icon }: { icon: FileTypeIconSetting }) => { + const theme = useTheme(); + const IconComponent = useMemo(() => { + if (icon.icon) { + return builtInIcons[icon.icon]; + } + }, [icon.icon]); + + const iconColor = useMemo(() => { + if (theme.palette.mode == "dark") { + return icon.color_dark ?? icon.color ?? theme.palette.action.active; + } else { + return icon.color ?? theme.palette.action.active; + } + }, [icon.color, icon.color_dark, theme]); + + if (IconComponent) { + return ( + + ); + } + return ( + + ); +}; + +const FileIconList = memo(({ config, onChange }: FileIconListProps) => { + const { t } = useTranslation("dashboard"); + const configParsed = useMemo( + (): FileTypeIconSetting[] => JSON.parse(config), + [config], + ); + const [inputCache, setInputCache] = useState<{ + [key: number]: string | undefined; + }>({}); + return ( + + {configParsed?.length > 0 && ( + + + + + + {t("settings.icon")} + + + {t("settings.iconUrl")} + + + {t("settings.iconColor")} + + + {t("settings.iconColorDark")} + + + {t("settings.exts")} + + + + + + {configParsed.map((r, i) => ( + + + + + + {!r.icon ? ( + + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { ...r, img: e.target.value as string }, + ...configParsed.slice(i + 1), + ]), + ) + } + /> + ) : ( + t("settings.builtinIcon") + )} + + + {!r.icon ? ( + "-" + ) : ( + + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + color: color, + }, + ...configParsed.slice(i + 1), + ]), + ) + } + /> + )} + + + {!r.icon ? ( + "-" + ) : ( + + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + color_dark: color, + }, + ...configParsed.slice(i + 1), + ]), + ) + } + /> + )} + + + { + onChange( + JSON.stringify([ + ...configParsed.slice(0, i), + { + ...r, + exts: inputCache[i]?.split(",") ?? r.exts, + }, + ...configParsed.slice(i + 1), + ]), + ); + setInputCache({ + ...inputCache, + [i]: undefined, + }); + }} + onChange={(e) => + setInputCache({ + ...inputCache, + [i]: e.target.value, + }) + } + /> + + + {!r.icon && ( + + onChange( + JSON.stringify( + configParsed.filter((_, index) => index !== i), + ), + ) + } + size={"small"} + > + + + )} + + + ))} + +
+
+ )} + } + sx={{ mt: 1 }} + onClick={() => + onChange( + JSON.stringify([ + ...configParsed, + { + img: "", + exts: [], + }, + ]), + ) + } + > + {t("settings.addIcon")} + +
+ ); +}); + +export default FileIconList; diff --git a/src/component/Admin/Settings/Filesystem/Filesystem.tsx b/src/component/Admin/Settings/Filesystem/Filesystem.tsx new file mode 100644 index 0000000..ef8e6a2 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/Filesystem.tsx @@ -0,0 +1,633 @@ +import { DeleteOutline } from "@mui/icons-material"; +import { + Box, + Collapse, + FormControl, + FormControlLabel, + Link, + ListItemText, + Stack, + Switch, + Typography, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import * as React from "react"; +import { useCallback, useContext, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { sendClearBlobUrlCache } from "../../../../api/api.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { isTrueVal } from "../../../../session/utils.ts"; +import SizeInput from "../../../Common/SizeInput.tsx"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import EmojiList from "./EmojiList.tsx"; +import FileIconList from "./FileIconList.tsx"; +import FileViewerList from "./ViewerSetting/FileViewerList.tsx"; + +const Filesystem = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + const [loading, setLoading] = useState(false); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const clearBlobUrlCache = () => { + setLoading(true); + dispatch(sendClearBlobUrlCache()) + .then(() => { + setLoading(false); + enqueueSnackbar(t("settings.cacheCleared"), { variant: "success", action: DefaultCloseAction }); + }) + .catch(() => { + setLoading(false); + }); + }; + + const iconOnChange = useCallback( + (s: string) => + setSettings({ + explorer_icons: s, + }), + [], + ); + + const viewerOnChange = useCallback( + (s: string) => + setSettings({ + file_viewers: s, + }), + [], + ); + + const onMimeMappingChange = useCallback((e: React.ChangeEvent) => { + setSettings({ + mime_mapping: e.target.value, + }); + }, []); + + const onEmojiChange = useCallback( + (s: string) => + setSettings({ + emojis: s, + }), + [], + ); + + return ( + e.preventDefault()}> + + + + {t("nav.fileSystem")} + + + + + + setSettings({ + maxEditSize: e.toString(), + }) + } + /> + {t("settings.textEditMaxSizeDes")} + + + + + + setSettings({ + cron_trash_bin_collect: e.target.value, + }) + } + required + /> + + ]} + /> + + + + + + + setSettings({ + cron_entity_collect: e.target.value, + }) + } + required + /> + + ]} + /> + + + + + + + setSettings({ + public_resource_maxage: e.target.value, + }) + } + required + /> + {t("settings.publicResourceMaxAgeDes")} + + + + + ( + + {v == "0" ? t("settings.offsetPagination") : t("settings.cursorPagination")} + + )} + onChange={(e) => + setSettings({ + use_cursor_pagination: e.target.value as string, + }) + } + MenuProps={{ + PaperProps: { sx: { maxWidth: 230 } }, + MenuListProps: { + sx: { + "& .MuiMenuItem-root": { + whiteSpace: "normal", + }, + }, + }, + }} + value={values.use_cursor_pagination} + > + + + + {t("settings.offsetPagination")} + + + {t("settings.offsetPaginationDes")} + + + + + + + {t("settings.cursorPagination")} + + + {t("settings.cursorPaginationDes")} + + + + + {t("settings.defaultPaginationDes")} + + + + + + setSettings({ + max_page_size: e.target.value, + }) + } + required + /> + {t("settings.maxPageSizeDes")} + + + + + + setSettings({ + max_batched_file: e.target.value, + }) + } + required + /> + {t("settings.maxBatchSizeDes")} + + + + + + setSettings({ + max_recursive_searched_folder: e.target.value, + }) + } + required + /> + {t("settings.maxRecursiveSearchDes")} + + + + + + setSettings({ + map_provider: e.target.value as string, + }) + } + value={values.map_provider} + > + + + {t("settings.mapGoogle")} + + + + + {t("settings.mapOpenStreetMap")} + + + + {t("settings.mapProviderDes")} + + + + + + + setSettings({ + map_google_tile_type: e.target.value as string, + }) + } + value={values.map_google_tile_type} + > + + + {t("settings.tileTypeTerrain")} + + + + + {t("settings.tileTypeSatellite")} + + + + + {t("settings.tileTypeGeneral")} + + + + {t("settings.tileTypeDes")} + + + + + + + {t("settings.mimeMappingDes")} + + + + + + + {t("settings.fileIcons")} + + + + + + + + {t("settings.fileViewers")} + + + + + + + + {t("settings.searchQuery")} + + + + + setSettings({ + explorer_category_image_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_video_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_audio_query: e.target.value, + }) + } + required + /> + + + + setSettings({ + explorer_category_document_query: e.target.value, + }) + } + required + /> + + + + + + {t("settings.emojiOptions")} + + + + + + + + {t("settings.advanceOptions")} + + + + + setSettings({ + archive_timeout: e.target.value, + }) + } + required + /> + + + + setSettings({ + upload_session_timeout: e.target.value, + }) + } + required + /> + {t("settings.uploadSessionDes")} + + + + setSettings({ + slave_api_timeout: e.target.value, + }) + } + required + /> + {t("settings.slaveAPIExpirationDes")} + + + + setSettings({ + folder_props_timeout: e.target.value, + }) + } + required + /> + {t("settings.folderPropsTimeoutDes")} + + + + setSettings({ + chunk_retries: e.target.value, + }) + } + required + /> + {t("settings.failedChunkRetryDes")} + + + + + setSettings({ + use_temp_chunk_buffer: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.cacheChunks")} + /> + {t("settings.cacheChunksDes")} + + + + + setSettings({ + max_parallel_transfer: e.target.value, + }) + } + required + /> + {t("settings.transitParallelNumDes")} + + + + setSettings({ + cron_oauth_cred_refresh: e.target.value, + }) + } + /> + + ]} + /> + + + + + setSettings({ + viewer_session_timeout: e.target.value, + }) + } + required + /> + {t("settings.wopiSessionTimeoutDes")} + + + + setSettings({ + entity_url_default_ttl: e.target.value, + }) + } + required + /> + {t("settings.fileBlobTimeoutDes")} + + + + setSettings({ + entity_url_cache_margin: e.target.value, + }) + } + required + /> + {t("settings.fileBlobMarginDes")} + + + + + } + variant="contained" + loading={loading} + color="primary" + onClick={clearBlobUrlCache} + > + {t("settings.clearBlobUrlCache")} + + + {t("settings.clearBlobUrlCacheDes")} + + + + + + + ); +}; + +export default Filesystem; diff --git a/src/component/Admin/Settings/Filesystem/HexColorInput.tsx b/src/component/Admin/Settings/Filesystem/HexColorInput.tsx new file mode 100644 index 0000000..e32eb63 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/HexColorInput.tsx @@ -0,0 +1,43 @@ +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import { InputAdornment } from "@mui/material"; +import CircleColorSelector, { + customizeMagicColor, +} from "../../../FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx"; +import * as React from "react"; + +export interface HexColorInputProps { + currentColor: string; + onColorChange: (color: string) => void; + required?: boolean; +} + +const HexColorInput = ({ + currentColor, + onColorChange, + ...rest +}: HexColorInputProps) => { + return ( + { + onColorChange(e.target.value); + }} + type="text" + InputProps={{ + endAdornment: ( + + onColorChange(color)} + /> + + ), + }} + {...rest} + /> + ); +}; + +export default HexColorInput; diff --git a/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerEditDialog.tsx b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerEditDialog.tsx new file mode 100644 index 0000000..ad9e7d2 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerEditDialog.tsx @@ -0,0 +1,415 @@ +import { + DialogContent, + FormControlLabel, + IconButton, + Link, + ListItemText, + Switch, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + useTheme, +} from "@mui/material"; +import FormControl from "@mui/material/FormControl"; +import Grid from "@mui/material/Grid2"; +import { useSnackbar } from "notistack"; +import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Viewer, ViewerType } from "../../../../../api/explorer.ts"; +import { builtInViewers } from "../../../../../redux/thunks/viewer.ts"; +import { isTrueVal } from "../../../../../session/utils.ts"; +import CircularProgress from "../../../../Common/CircularProgress.tsx"; +import SizeInput from "../../../../Common/SizeInput.tsx"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar.tsx"; +import { + DenseFilledTextField, + DenseSelect, + NoWrapTableCell, + SecondaryButton, + StyledTableContainerPaper, +} from "../../../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../../../Dialogs/DraggableDialog.tsx"; +import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu.tsx"; +import { ViewerIDWithDefaultIcons } from "../../../../FileManager/Dialogs/OpenWith.tsx"; +import Add from "../../../../Icons/Add.tsx"; +import Dismiss from "../../../../Icons/Dismiss.tsx"; +import SettingForm from "../../../../Pages/Setting/SettingForm.tsx"; +import MagicVarDialog, { MagicVar } from "../../../Common/MagicVarDialog.tsx"; +import { NoMarginHelperText } from "../../Settings.tsx"; + +const MonacoEditor = lazy(() => import("../../../../Viewers/CodeViewer/MonacoEditor.tsx")); + +export interface FileViewerEditDialogProps { + viewer: Viewer; + onChange: (viewer: Viewer) => void; + open: boolean; + onClose: () => void; +} + +const magicVars: MagicVar[] = [ + { + name: "{$src}", + value: "settings.srcEncodedVar", + example: "https%3A%2F%2Fcloudreve.org%2Fapi%2Fv4%2Ffile%2Fcontent%2FzOie%2F0%2Ftext.txt%3Fsign%3Dxxx", + }, + { + name: "{$src_raw}", + value: "settings.srcVar", + example: "https://cloudreve.org/api/v4/file/content/zOie/0/text.txt?sign=xxx", + }, + { + name: "{$name}", + value: "settings.nameEncodedVar", + example: "sampleFile%5B1%5D.txt", + }, + { + name: "{$version}", + value: "settings.versionEntityVar", + example: "zOie", + }, + { + name: "{$id}", + value: "settings.fileIdVar", + example: "jm8AF8", + }, + { + name: "{$user_id}", + value: "settings.userIdVar", + example: "lpua", + }, + { + name: "{$user_display_name}", + value: "settings.userDisplayNameVar", + example: "Aaron%20Liu", + }, +]; + +const FileViewerEditDialog = ({ viewer, onChange, open, onClose }: FileViewerEditDialogProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + const [viewerShadowed, setViewerShadowed] = useState({ ...viewer }); + const formRef = React.useRef(null); + const [magicVarOpen, setMagicVarOpen] = useState(false); + const [wopiCached, setWopiCached] = useState(""); + const withDefaultIcon = useMemo(() => { + return ViewerIDWithDefaultIcons.includes(viewer.id); + }, [viewer.id]); + + useEffect(() => { + setViewerShadowed({ ...viewer }); + setWopiCached(""); + }, [viewer]); + + const onSubmit = useCallback(() => { + if (formRef.current && !formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + + let changed = viewerShadowed; + + if (wopiCached != "") { + try { + const parsed = JSON.parse(wopiCached); + changed = { ...viewerShadowed, wopi_actions: parsed }; + setViewerShadowed({ ...changed }); + } catch (e) { + enqueueSnackbar({ + message: t("settings.invalidWopiActionMapping"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + } + + onChange(changed); + onClose(); + }, [viewerShadowed, wopiCached, formRef]); + + const openMagicVar = useCallback((e: React.MouseEvent) => { + setMagicVarOpen(true); + e.stopPropagation(); + e.preventDefault(); + }, []); + + return ( + + + setMagicVarOpen(false)} /> +
+ + + { + setViewerShadowed({ + ...viewerShadowed, + icon: e.target.value, + }); + }} + /> + {withDefaultIcon && {t("settings.builtInIconUrlDes")}} + + + { + setViewerShadowed({ + ...viewerShadowed, + display_name: e.target.value, + }); + }} + /> + {t("settings.displayNameDes")} + + + + setViewerShadowed({ + ...viewerShadowed, + exts: e.target.value.split(",").map((ext) => ext.trim()), + }) + } + /> + + {viewer.type == ViewerType.custom && ( + + + setViewerShadowed({ + ...viewerShadowed, + url: e.target.value, + }) + } + /> + + ]} + /> + + + )} + + + + setViewerShadowed({ + ...viewerShadowed, + max_size: e ? e : undefined, + }) + } + /> + {t("settings.maxSizeDes")} + + + {viewer.type == ViewerType.custom && ( + + + setViewerShadowed({ + ...viewerShadowed, + props: { + ...viewerShadowed.props, + openInNew: e.target.checked.toString(), + }, + }) + } + /> + } + label={t("settings.openInNew")} + /> + {t("settings.openInNewDes")} + + )} + {viewer.id == builtInViewers.drawio && ( + + + setViewerShadowed({ + ...viewerShadowed, + props: { + ...viewerShadowed.props, + host: e.target.value, + }, + }) + } + /> + {t("settings.drawioHostDes")} + + )} + {viewer.type == ViewerType.wopi && ( + + }> + setWopiCached(e as string)} + /> + + + )} + + {viewerShadowed?.templates && viewerShadowed.templates.length > 0 && ( + + + + + {t("settings.ext")} + {t("settings.displayName")} + + + + + {viewerShadowed.templates?.map((t, i) => ( + + + { + const newExt = e.target.value as string; + setViewerShadowed({ + ...viewerShadowed, + templates: viewerShadowed.templates?.map((template, index) => + index == i ? { ...template, ext: newExt } : template, + ), + }); + }} + > + {viewerShadowed.exts.map((ext) => ( + + + {ext} + + + ))} + + + + { + setViewerShadowed({ + ...viewerShadowed, + templates: viewerShadowed.templates?.map((template, index) => + index == i + ? { + ...template, + display_name: e.target.value, + } + : template, + ), + }); + }} + /> + + + + setViewerShadowed({ + ...viewerShadowed, + templates: viewerShadowed.templates?.filter((_, index) => index != i), + }) + } + > + + + + + ))} + +
+
+ )} + } + onClick={() => + setViewerShadowed({ + ...viewerShadowed, + templates: [ + ...(viewerShadowed.templates ?? []), + { ext: viewerShadowed.exts?.[0] ?? "", display_name: "" }, + ], + }) + } + > + {t("settings.addNewFileAction")} + + {t("settings.newFileActionDes")} +
+
+
+
+
+ ); +}; + +export default FileViewerEditDialog; diff --git a/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerList.tsx b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerList.tsx new file mode 100644 index 0000000..9843915 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerList.tsx @@ -0,0 +1,243 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; +import { + AccordionDetails, + Box, + Link, + ListItemIcon, + Menu, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import * as React from "react"; +import { memo, useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Viewer, ViewerGroup, ViewerType } from "../../../../../api/explorer.ts"; +import { uuidv4 } from "../../../../../util"; +import { NoWrapTableCell, SecondaryButton } from "../../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu.tsx"; +import Add from "../../../../Icons/Add.tsx"; +import DesktopFlow from "../../../../Icons/DesktopFlow.tsx"; +import DocumentDataLink from "../../../../Icons/DocumentDataLink.tsx"; +import { AccordionSummary, StyledAccordion } from "../../UserSession/SSOSettings.tsx"; +import FileViewerEditDialog from "./FileViewerEditDialog.tsx"; +import FileViewerRow from "./FileViewerRow.tsx"; +import ImportWopiDialog from "./ImportWopiDialog.tsx"; + +interface ViewerGroupProps { + group: ViewerGroup; + index: number; + onDelete: (e: React.MouseEvent) => void; + onGroupChange: (g: ViewerGroup) => void; +} + +const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange }: ViewerGroupProps) => { + const { t } = useTranslation("dashboard"); + + const onViewerChange = useMemo(() => { + return group.viewers.map((_, index) => (vChanged: Viewer) => { + onGroupChange({ + viewers: group.viewers.map((v, i) => (i == index ? vChanged : v)), + }); + }); + }, [group.viewers]); + + const onViewerDeleted = useMemo(() => { + return group.viewers.map((_, index) => (e: React.MouseEvent) => { + onGroupChange({ + viewers: group.viewers.filter((_, i) => i != index), + }); + e.preventDefault(); + e.stopPropagation(); + }); + }, [group.viewers]); + + return ( + + }> + + {t("settings.viewerGroupTitle", { index: index + 1 })} + {index > 0 && ( + + {t("policy.delete")} + + )} + + + + + + + + {t("settings.icon")} + {t("settings.viewerType")} + {t("settings.displayName")} + {t("settings.exts")} + {t("settings.newFileAction")} + {t("settings.viewerEnabled")} + + + + + {group.viewers.map((viewer, index) => ( + + ))} + +
+
+
+
+ ); +}); + +export interface FileViewerListProps { + config: string; + onChange: (value: string) => void; +} + +const FileViewerList = memo(({ config, onChange }: FileViewerListProps) => { + const { t } = useTranslation("dashboard"); + const addNewPopupState = usePopupState({ + variant: "popover", + popupId: "addNewViewer", + }); + const [createNewOpen, setCreateNewOpen] = useState(false); + const [newViewer, setNewViewer] = useState(undefined); + const [importOpen, setImportOpen] = useState(false); + + const configParsed = useMemo((): ViewerGroup[] => JSON.parse(config), [config]); + + const onNewViewerChange = useCallback( + (v: Viewer) => { + setNewViewer(v); + const newViewerSetting = [...configParsed]; + newViewerSetting[0].viewers.push(v); + onChange(JSON.stringify(newViewerSetting)); + }, + [configParsed], + ); + + const onGroupDelete = useMemo(() => { + return configParsed.map((_, index) => (e: React.MouseEvent) => { + onChange(JSON.stringify([...configParsed].filter((_, i) => i != index))); + e.preventDefault(); + e.stopPropagation(); + }); + }, [configParsed]); + + const onGroupChange = useMemo(() => { + return configParsed.map((_, index) => (g: ViewerGroup) => { + onChange(JSON.stringify([...configParsed].map((item, i) => (i == index ? g : item)))); + }); + }, [configParsed]); + + const { onClose, ...menuProps } = bindMenu(addNewPopupState); + + const onCreateNewClosed = useCallback(() => { + setCreateNewOpen(false); + }, []); + + const openCreateNew = useCallback(() => { + setNewViewer({ + id: uuidv4(), + icon: "", + type: ViewerType.custom, + display_name: "", + exts: [], + }); + setCreateNewOpen(true); + onClose(); + }, [onClose, setNewViewer]); + + const openImportNew = useCallback(() => { + setImportOpen(true); + onClose(); + }, [onClose, setImportOpen]); + + const onImportedNew = useCallback( + (v: ViewerGroup) => { + const newViewerSetting = [...configParsed]; + newViewerSetting.push(v); + onChange(JSON.stringify(newViewerSetting)); + }, + [configParsed], + ); + + return ( + + {configParsed?.length > 0 && + configParsed.map((item: ViewerGroup, index) => ( + + ))} + } sx={{ mt: 1 }}> + {t("settings.addViewer")} + + + + + + + {t("settings.embeddedWebpageViewer")} + + + + + + {t("settings.wopiViewer")} + + + {newViewer && ( + + )} + setImportOpen(false)} open={importOpen} /> + + ); +}); + +export default FileViewerList; diff --git a/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerRow.tsx b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerRow.tsx new file mode 100644 index 0000000..0901149 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/ViewerSetting/FileViewerRow.tsx @@ -0,0 +1,101 @@ +import * as React from "react"; +import { memo, useCallback, useState } from "react"; +import { Viewer, ViewerType } from "../../../../../api/explorer.ts"; +import { useTranslation } from "react-i18next"; +import { IconButton, TableRow } from "@mui/material"; +import { + DenseFilledTextField, + NoWrapCell, + StyledCheckbox, +} from "../../../../Common/StyledComponents.tsx"; +import { ViewerIcon } from "../../../../FileManager/Dialogs/OpenWith.tsx"; +import Dismiss from "../../../../Icons/Dismiss.tsx"; +import Edit from "../../../../Icons/Edit.tsx"; +import FileViewerEditDialog from "./FileViewerEditDialog.tsx"; + +export interface FileViewerRowProps { + viewer: Viewer; + onChange: (viewer: Viewer) => void; + onDelete: (e: React.MouseEvent) => void; +} + +const FileViewerRow = memo( + ({ viewer, onChange, onDelete }: FileViewerRowProps) => { + const { t } = useTranslation("dashboard"); + const [extCached, setExtCached] = useState(""); + const [editOpen, setEditOpen] = useState(false); + const onClose = useCallback(() => { + setEditOpen(false); + }, [setEditOpen]); + return ( + + + + + + {t(`settings.${viewer.type}ViewerType`)} + + {t(viewer.display_name, { + ns: "application", + })} + + + { + onChange({ + ...viewer, + exts: + extCached == "" + ? viewer.exts + : extCached?.split(",")?.map((ext) => ext.trim()), + }); + setExtCached(""); + }} + onChange={(e) => setExtCached(e.target.value)} + /> + + + {viewer.templates?.length + ? t("settings.nMapping", { num: viewer.templates?.length }) + : t("share.none")} + + + + onChange({ + ...viewer, + disabled: !e.target.checked, + }) + } + /> + + + setEditOpen(true)}> + + + {viewer.type != ViewerType.builtin && ( + + + + )} + + + ); + }, +); + +export default FileViewerRow; diff --git a/src/component/Admin/Settings/Filesystem/ViewerSetting/ImportWopiDialog.tsx b/src/component/Admin/Settings/Filesystem/ViewerSetting/ImportWopiDialog.tsx new file mode 100644 index 0000000..dd965c6 --- /dev/null +++ b/src/component/Admin/Settings/Filesystem/ViewerSetting/ImportWopiDialog.tsx @@ -0,0 +1,73 @@ +import { useTranslation } from "react-i18next"; +import { ViewerGroup } from "../../../../../api/explorer.ts"; +import DraggableDialog from "../../../../Dialogs/DraggableDialog.tsx"; +import { useCallback, useState } from "react"; +import { useAppDispatch } from "../../../../../redux/hooks.ts"; +import { getWopiDiscovery } from "../../../../../api/api.ts"; +import SettingForm from "../../../../Pages/Setting/SettingForm.tsx"; +import { DialogContent } from "@mui/material"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents.tsx"; +import { NoMarginHelperText } from "../../Settings.tsx"; + +export interface ImportWopiDialogProps { + open: boolean; + onClose: () => void; + onImported: (v: ViewerGroup) => void; +} + +const ImportWopiDialog = ({ + open, + onClose, + onImported, +}: ImportWopiDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [endpoint, setEndpoint] = useState(""); + const [loading, setLoading] = useState(false); + + const onSubmit = useCallback(() => { + setLoading(true); + dispatch( + getWopiDiscovery({ + endpoint, + }), + ) + .then((res) => { + onImported(res); + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [endpoint, onClose, onImported]); + + return ( + + + + setEndpoint(e.target.value)} + /> + {t("settings.wopiDes")} + + + + ); +}; + +export default ImportWopiDialog; diff --git a/src/component/Admin/Settings/Media/Extractors.tsx b/src/component/Admin/Settings/Media/Extractors.tsx new file mode 100644 index 0000000..046c558 --- /dev/null +++ b/src/component/Admin/Settings/Media/Extractors.tsx @@ -0,0 +1,262 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; +import { LoadingButton } from "@mui/lab"; +import { + AccordionDetails, + Box, + FormControl, + FormControlLabel, + InputAdornment, + Switch, + Typography, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import * as React from "react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendTestThumbGeneratorExecutable } from "../../../../api/api.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { isTrueVal } from "../../../../session/utils.ts"; +import SizeInput from "../../../Common/SizeInput.tsx"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { + DenseFilledTextField, + StyledCheckbox, +} from "../../../Common/StyledComponents.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { NoMarginHelperText, SettingSectionContent } from "../Settings.tsx"; +import { + AccordionSummary, + StyledAccordion, +} from "../UserSession/SSOSettings.tsx"; + +export interface ExtractorsProps { + values: { + [key: string]: any; + }; + setSetting: (v: { [key: string]: any }) => void; +} + +interface ExtractorRenderProps { + name: string; + des: string; + enableFlag?: string; + executableSetting?: string; + maxSizeLocalSetting?: string; + maxSizeRemoteSetting?: string; + additionalSettings?: { + name: string; + label: string; + des: string; + type?: "switch"; + }[]; +} + +const extractors: ExtractorRenderProps[] = [ + { + name: "exif", + des: "exifDes", + enableFlag: "media_meta_exif", + maxSizeLocalSetting: "media_meta_exif_size_local", + maxSizeRemoteSetting: "media_meta_exif_size_remote", + additionalSettings: [ + { + name: "media_meta_exif_brute_force", + label: "exifBruteForce", + des: "exifBruteForceDes", + type: "switch", + }, + ], + }, + { + name: "music", + des: "musicDes", + enableFlag: "media_meta_music", + maxSizeLocalSetting: "media_meta_music_size_local", + maxSizeRemoteSetting: "media_exif_music_size_remote", + }, + { + name: "ffprobe", + des: "ffprobeDes", + enableFlag: "media_meta_ffprobe", + executableSetting: "media_meta_ffprobe_path", + maxSizeLocalSetting: "media_meta_ffprobe_size_local", + maxSizeRemoteSetting: "media_meta_ffprobe_size_remote", + }, +]; + +const Extractors = ({ values, setSetting }: ExtractorsProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [testing, setTesting] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + + const handleEnableChange = + (name: string) => (e: React.ChangeEvent) => { + setSetting({ + [name]: e.target.checked ? "1" : "0", + }); + }; + + const doTest = (name: string, executable: string) => { + setTesting(true); + dispatch( + sendTestThumbGeneratorExecutable({ + name, + executable, + }), + ) + .then((res) => { + enqueueSnackbar({ + message: t("settings.executableTestSuccess", { version: res }), + variant: "success", + action: DefaultCloseAction, + }); + }) + .finally(() => { + setTesting(false); + }); + }; + + return ( + + {extractors.map((e) => ( + + }> + event.stopPropagation()} + onFocus={(event) => event.stopPropagation()} + control={ + + } + label={t(`settings.${e.name}`)} + /> + + + + {t(`settings.${e.des}`)} + + + {e.executableSetting && ( + + + + + doTest( + e.name, + values[e.executableSetting ?? ""], + ) + } + loading={testing} + color="primary" + > + {t("settings.executableTest")} + + + ), + }} + onChange={(ev) => + setSetting({ + [e.executableSetting ?? ""]: ev.target.value, + }) + } + /> + + {t("settings.executableDes")} + + + + )} + {e.maxSizeLocalSetting && ( + + + + setSetting({ + [e.maxSizeLocalSetting ?? ""]: v.toString(), + }) + } + /> + + {t("settings.maxSizeLocalDes")} + + + + )} + {e.maxSizeRemoteSetting && ( + + + + setSetting({ + [e.maxSizeRemoteSetting ?? ""]: v.toString(), + }) + } + /> + + {t("settings.maxSizeRemoteDes")} + + + + )} + {e.additionalSettings?.map((setting) => ( + + + {setting.type === "switch" ? ( + + setSetting({ + [setting.name]: ev.target.checked ? "1" : "0", + }) + } + /> + } + label={t(`settings.${setting.label}`)} + /> + ) : ( + + setSetting({ + [setting.name]: ev.target.value, + }) + } + /> + )} + + {t(`settings.${setting.des}`)} + + + + ))} + + + + ))} + + ); +}; + +export default Extractors; \ No newline at end of file diff --git a/src/component/Admin/Settings/Media/Generators.tsx b/src/component/Admin/Settings/Media/Generators.tsx new file mode 100644 index 0000000..e2bbf38 --- /dev/null +++ b/src/component/Admin/Settings/Media/Generators.tsx @@ -0,0 +1,284 @@ +import { useTranslation } from "react-i18next"; +import { + AccordionDetails, + Box, + FormControl, + FormControlLabel, + InputAdornment, + Typography, +} from "@mui/material"; +import { isTrueVal } from "../../../../session/utils.ts"; +import { + AccordionSummary, + StyledAccordion, +} from "../UserSession/SSOSettings.tsx"; +import { ExpandMoreRounded } from "@mui/icons-material"; +import { + DenseFilledTextField, + StyledCheckbox, +} from "../../../Common/StyledComponents.tsx"; +import { useSnackbar } from "notistack"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { NoMarginHelperText, SettingSectionContent } from "../Settings.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import * as React from "react"; +import { useState } from "react"; +import { LoadingButton } from "@mui/lab"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { sendTestThumbGeneratorExecutable } from "../../../../api/api.ts"; +import SizeInput from "../../../Common/SizeInput.tsx"; + +export interface GeneratorsProps { + values: { + [key: string]: any; + }; + setSetting: (v: { [key: string]: any }) => void; +} + +interface GeneratorRenderProps { + name: string; + des: string; + enableFlag?: string; + executableSetting?: string; + maxSizeSetting?: string; + readOnly?: boolean; + inputs?: { + name: string; + label: string; + des: string; + required?: boolean; + }[]; +} + +const generators: GeneratorRenderProps[] = [ + { + name: "policyBuiltin", + des: "policyBuiltinDes", + readOnly: true, + }, + { + name: "musicCover", + des: "musicCoverDes", + enableFlag: "thumb_music_cover_enabled", + maxSizeSetting: "thumb_music_cover_max_size", + inputs: [ + { + name: "thumb_music_cover_exts", + label: "generatorExts", + des: "generatorExtsDes", + }, + ], + }, + { + name: "libreOffice", + des: "libreOfficeDes", + enableFlag: "thumb_libreoffice_enabled", + maxSizeSetting: "thumb_libreoffice_max_size", + executableSetting: "thumb_libreoffice_path", + inputs: [ + { + name: "thumb_libreoffice_exts", + label: "generatorExts", + des: "generatorExtsDes", + }, + ], + }, + { + name: "vips", + des: "vipsDes", + enableFlag: "thumb_vips_enabled", + maxSizeSetting: "thumb_vips_max_size", + executableSetting: "thumb_vips_path", + inputs: [ + { + name: "thumb_vips_exts", + label: "generatorExts", + des: "generatorExtsDes", + }, + ], + }, + { + name: "ffmpeg", + des: "ffmpegDes", + enableFlag: "thumb_ffmpeg_enabled", + maxSizeSetting: "thumb_ffmpeg_max_size", + executableSetting: "thumb_ffmpeg_path", + inputs: [ + { + name: "thumb_ffmpeg_exts", + label: "generatorExts", + des: "generatorExtsDes", + }, + { + name: "thumb_ffmpeg_seek", + label: "ffmpegSeek", + des: "ffmpegSeekDes", + required: true, + }, + ], + }, + { + name: "cloudreveBuiltin", + maxSizeSetting: "thumb_builtin_max_size", + des: "cloudreveBuiltinDes", + enableFlag: "thumb_builtin_enabled", + }, +]; + +const Generators = ({ values, setSetting }: GeneratorsProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [testing, setTesting] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + + const handleEnableChange = + (name: string) => (e: React.ChangeEvent) => { + setSetting({ + [name]: e.target.checked ? "1" : "0", + }); + const newValues = { ...values, [name]: e.target.checked ? "1" : "0" }; + if ( + (newValues["thumb_libreoffice_enabled"] === "1" || + newValues["thumb_music_cover_enabled"] === "1") && + newValues["thumb_builtin_enabled"] === "0" && + newValues["thumb_vips_enabled"] === "0" + ) { + enqueueSnackbar({ + message: t("settings.thumbDependencyWarning"), + variant: "warning", + action: DefaultCloseAction, + }); + } + }; + + const doTest = (name: string, executable: string) => { + setTesting(true); + dispatch( + sendTestThumbGeneratorExecutable({ + name, + executable, + }), + ) + .then((res) => { + enqueueSnackbar({ + message: t("settings.executableTestSuccess", { version: res }), + variant: "success", + action: DefaultCloseAction, + }); + }) + .finally(() => { + setTesting(false); + }); + }; + + return ( + + {generators.map((g) => ( + + }> + event.stopPropagation()} + onFocus={(event) => event.stopPropagation()} + control={ + + } + label={t(`settings.${g.name}`)} + disabled={g.readOnly} + /> + + + + {t(`settings.${g.des}`)} + + + {g.executableSetting && ( + + + + + doTest( + g.name, + values[g.executableSetting ?? ""], + ) + } + loading={testing} + color="primary" + > + {t("settings.executableTest")} + + + ), + }} + onChange={(e) => + setSetting({ + [g.executableSetting ?? ""]: e.target.value, + }) + } + /> + + {t("settings.executableDes")} + + + + )} + {g.maxSizeSetting && ( + + + + setSetting({ + [g.maxSizeSetting ?? ""]: e.toString(), + }) + } + /> + + {t("settings.thumbMaxSizeDes")} + + + + )} + {g.inputs?.map((input) => ( + + + + setSetting({ + [input.name]: e.target.value, + }) + } + required={!!input.required} + /> + + {t(`settings.${input.des}`)} + + + + ))} + + + + ))} + + ); +}; + +export default Generators; diff --git a/src/component/Admin/Settings/Media/Media.tsx b/src/component/Admin/Settings/Media/Media.tsx new file mode 100644 index 0000000..7428da4 --- /dev/null +++ b/src/component/Admin/Settings/Media/Media.tsx @@ -0,0 +1,195 @@ +import { useTranslation } from "react-i18next"; +import * as React from "react"; +import { useContext } from "react"; +import { SettingContext } from "../SettingWrapper.tsx"; +import { + Alert, + Box, + Collapse, + FormControlLabel, + ListItemText, + Stack, + Switch, + Typography, +} from "@mui/material"; +import { + NoMarginHelperText, + SettingSection, + SettingSectionContent, +} from "../Settings.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { + DenseFilledTextField, + DenseSelect, +} from "../../../Common/StyledComponents.tsx"; +import FormControl from "@mui/material/FormControl"; +import { isTrueVal } from "../../../../session/utils.ts"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import Generators from "./Generators.tsx"; +import Extractors from "./Extractors.tsx"; + +const Media = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + return ( + e.preventDefault()}> + + + + {t("settings.thumbnails")} + + + {t("settings.thumbnailBasic")} + + + + + { + setSettings({ + thumb_width: e.target.value, + }); + }} + /> + + + + + { + setSettings({ + thumb_height: e.target.value, + }); + }} + /> + + + + + { + setSettings({ + thumb_entity_suffix: e.target.value, + }); + }} + /> + + {t("settings.notAppliedToNativeGenerator", { + prefix: t("settings.thumbSuffixDes"), + })} + + + + + + { + setSettings({ + thumb_encode_method: e.target.value as string, + }); + }} + required + > + {["jpg", "png"].map((f) => ( + + + {f} + + + ))} + + + {t("settings.notAppliedToNativeGenerator", { prefix: "" })} + + + + + + + { + setSettings({ + thumb_encode_quality: e.target.value, + }); + }} + /> + + {t("settings.notAppliedToNativeGenerator", { + prefix: t("settings.thumbQualityDes"), + })} + + + + + + + + setSettings({ + thumb_gc_after_gen: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.thumbGC")} + /> + + {t("settings.notAppliedToNativeGenerator", { prefix: "" })} + + + + + + {t("settings.generators")} + + + + + {t("settings.generatorProxyWarning")} + + + + + + + + {t("settings.extractMediaMeta")} + + + + + {t("settings.extractMediaMetaDes")} + + + + + + + + ); +}; + +export default Media; diff --git a/src/component/Admin/Settings/Queue/Queue.tsx b/src/component/Admin/Settings/Queue/Queue.tsx new file mode 100644 index 0000000..e48065e --- /dev/null +++ b/src/component/Admin/Settings/Queue/Queue.tsx @@ -0,0 +1,55 @@ +import { Box, Grid, Stack } from "@mui/material"; +import { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getQueueMetrics } from "../../../../api/api.ts"; +import { QueueMetric } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { SecondaryButton } from "../../../Common/StyledComponents.tsx"; +import ArrowSync from "../../../Icons/ArrowSync.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import QueueCard from "./QueueCard.tsx"; + +const Queue = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const { formRef, setSettings, values } = useContext(SettingContext); + const [metrics, setMetrics] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchQueueMetrics = () => { + setLoading(true); + dispatch(getQueueMetrics()) + .then((res) => { + setMetrics(res); + }).finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + fetchQueueMetrics(); + }, []); + + return + + } + > + {t("node.refresh")} + + + + {!loading && metrics.map((metric) => ( + + ))} + {loading && Array.from(Array(5)).map((_, index) => ( + + ))} + + ; +}; + +export default Queue; \ No newline at end of file diff --git a/src/component/Admin/Settings/Queue/QueueCard.tsx b/src/component/Admin/Settings/Queue/QueueCard.tsx new file mode 100644 index 0000000..f098e48 --- /dev/null +++ b/src/component/Admin/Settings/Queue/QueueCard.tsx @@ -0,0 +1,162 @@ +import { Box, Divider, Grid, IconButton, Skeleton, Stack, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { QueueMetric, QueueType } from "../../../../api/dashboard.ts"; +import Setting from "../../../Icons/Setting.tsx"; +import { StorageBar, StorageBlock, StoragePart } from "../../../Pages/Setting/StorageSetting.tsx"; +import { BorderedCard } from "../../Common/AdminCard.tsx"; +import QueueSettingDialog from "./QueueSettingDialog.tsx"; + +export interface QueueCardProps { + queue?: QueueType; + settings: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; + metrics?: QueueMetric; + loading: boolean; +} + +export const QueueCard = ({ queue, settings, metrics, setSettings, loading }: QueueCardProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [settingDialogOpen, setSettingDialogOpen] = useState(false); + + if (loading) { + return ( + + + + + + + + + + + {Array.from(Array(5)).map((_, index) => ( + + ))} + + + + ); + } + + return + + + {t(`queue.queueName_${queue}`)} + setSettingDialogOpen(true)}> + + + + {t(`queue.queueName_${queue}Des`)} + + {metrics && <> + + theme.palette.success.light, + width: `${(metrics.success_tasks / metrics.submitted_tasks) * 100}%`, + }} + /> + theme.palette.error.light, + width: `${(metrics.failure_tasks / metrics.submitted_tasks) * 100}%`, + }} + /> + theme.palette.action.active, + width: `${(metrics.suspending_tasks / metrics.submitted_tasks) * 100}%`, + }} + /> + theme.palette.info.light, + width: `${(metrics.busy_workers / metrics.submitted_tasks) * 100}%`, + }} + /> + + + + theme.palette.success.light, + }} + /> + {t("queue.success", { + count: metrics.success_tasks, + })} + + + theme.palette.error.light, + }} + /> + {t("queue.failed", { + count: metrics.failure_tasks, + })} + + + theme.palette.info.light, + }} + /> + {t("queue.busyWorker", { + count: metrics.busy_workers, + })} + + + theme.palette.action.active, + }} + /> + {t("queue.suspending", { + count: metrics.suspending_tasks, + })} + + + + theme.palette.grey[ + theme.palette.mode === "light" ? 200 : 800 + ], + }} + /> + {t("queue.submited", { + count: metrics.submitted_tasks, + })} + + + } + + {queue && ( + setSettingDialogOpen(false)} + queue={queue} + settings={settings} + setSettings={setSettings} + /> + )} + + ; +}; + +export default QueueCard; \ No newline at end of file diff --git a/src/component/Admin/Settings/Queue/QueueSettingDialog.tsx b/src/component/Admin/Settings/Queue/QueueSettingDialog.tsx new file mode 100644 index 0000000..56e215d --- /dev/null +++ b/src/component/Admin/Settings/Queue/QueueSettingDialog.tsx @@ -0,0 +1,226 @@ +import { Box, FormControl, FormHelperText } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { QueueType } from "../../../../api/dashboard.ts"; +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; + +export interface QueueSettingDialogProps { + open: boolean; + onClose: () => void; + queue: QueueType; + settings: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; +} + +const NoMarginHelperText = (props: any) => ( + +); + +const QueueSettingDialog = ({ + open, + onClose, + queue, + settings, + setSettings, +}: QueueSettingDialogProps) => { + const { t } = useTranslation("dashboard"); + const formRef = useRef(null); + const [localSettings, setLocalSettings] = useState<{ [key: string]: string }>({}); + + // Initialize local settings when dialog opens or queue changes + useEffect(() => { + if (open) { + const queueSettings: { [key: string]: string } = {}; + const settingKeys = [ + "worker_num", + "max_execution", + "backoff_factor", + "backoff_max_duration", + "max_retry", + "retry_delay" + ]; + + settingKeys.forEach(key => { + const fullKey = `queue_${queue}_${key}`; + queueSettings[key] = settings[fullKey] || ""; + }); + + setLocalSettings(queueSettings); + } + }, [open, queue, settings]); + + const handleSave = () => { + if (formRef.current?.reportValidity()) { + // Apply all settings at once + const updatedSettings: { [key: string]: string } = {}; + Object.entries(localSettings).forEach(([key, value]) => { + updatedSettings[`queue_${queue}_${key}`] = value as string; + }); + + setSettings(updatedSettings); + onClose(); + } + }; + + const updateLocalSetting = (key: string, value: string) => { + setLocalSettings((prev: { [key: string]: string }) => ({ + ...prev, + [key]: value + })); + }; + + return ( + + + + + updateLocalSetting("worker_num", e.target.value)} + type="number" + slotProps={ + { + htmlInput: { + min: 1, + } + } + } + required + /> + + {t("queue.workerNumDes")} + + + + + + + updateLocalSetting("max_execution", e.target.value)} + type="number" + inputProps={{ + min: 1, + }} + required + /> + + {t("queue.maxExecutionDes")} + + + + + + + updateLocalSetting("backoff_factor", e.target.value)} + type="number" + slotProps={ + { + htmlInput: { + min: 1, + step: 0.1, + } + } + } + required + /> + + {t("queue.backoffFactorDes")} + + + + + + + updateLocalSetting("backoff_max_duration", e.target.value)} + type="number" + slotProps={ + { + htmlInput: { + min: 1, + } + } + } + required + /> + + {t("queue.backoffMaxDurationDes")} + + + + + + + updateLocalSetting("max_retry", e.target.value)} + type="number" + slotProps={ + { + htmlInput: { + min: 0, + } + } + } + required + /> + + {t("queue.maxRetryDes")} + + + + + + + updateLocalSetting("retry_delay", e.target.value)} + type="number" + slotProps={ + { + htmlInput: { + min: 0, + } + } + } + required + /> + + {t("queue.retryDelayDes")} + + + + + + ); +}; + +export default QueueSettingDialog; \ No newline at end of file diff --git a/src/component/Admin/Settings/Server/ServerSetting.tsx b/src/component/Admin/Settings/Server/ServerSetting.tsx new file mode 100644 index 0000000..47799e9 --- /dev/null +++ b/src/component/Admin/Settings/Server/ServerSetting.tsx @@ -0,0 +1,124 @@ +import { Box, FormControl, Link, Stack, Typography } from "@mui/material"; +import { useContext } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents"; +import ArrowSync from "../../../Icons/ArrowSync"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings"; +import { SettingContext } from "../SettingWrapper"; + +const ServerSetting = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + const rotateSecretKey = () => { + setSettings({ secret_key: "[Placeholder]" }); + }; + + return ( + e.preventDefault()}> + + + + {t("settings.server")} + + + + + setSettings({ temp_path: e.target.value })} + /> + {t("settings.tempPathDes")} + + + + + setSettings({ siteID: e.target.value })} + /> + {t("settings.siteIDDes")} + + + + } variant="contained"> + {t("settings.rotateSecretKey")} + + {t("settings.siteSecretKeyDes")} + + + + setSettings({ hash_id_salt: e.target.value })} + /> + {t("settings.hashidSaltDes")} + + + + + setSettings({ access_token_ttl: e.target.value })} + /> + {t("settings.accessTokenTTLDes")} + + + + + setSettings({ refresh_token_ttl: e.target.value })} + /> + {t("settings.refreshTokenTTLDes")} + + + + + setSettings({ cron_garbage_collect: e.target.value })} + /> + + ]} + /> + + + + + + + + ); +}; + +export default ServerSetting; diff --git a/src/component/Admin/Settings/SettingWrapper.tsx b/src/component/Admin/Settings/SettingWrapper.tsx new file mode 100644 index 0000000..a8a262a --- /dev/null +++ b/src/component/Admin/Settings/SettingWrapper.tsx @@ -0,0 +1,191 @@ +import { LoadingButton } from "@mui/lab"; +import { Box, Grow, styled } from "@mui/material"; +import * as React from "react"; +import { createContext, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getSettings, sendSetSetting } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import { SecondaryButton } from "../../Common/StyledComponents.tsx"; +import ArrowHookUpRight from "../../Icons/ArrowHookUpRight.tsx"; +import Save from "../../Icons/Save.tsx"; + +export interface SettingsWrapperProps { + settings: string[]; + children: React.ReactNode; +} + +export interface SettingContextProps { + values: { + [key: string]: string; + }; + setSettings: (settings: { [key: string]: string }) => void; + formRef?: React.RefObject; +} + +const SavingFloatContainer = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + marginTop: theme.spacing(2), + position: "fixed", + backgroundColor: theme.palette.background.paper, + bottom: 23, + zIndex: theme.zIndex.modal, +})); + +export interface SavingFloatProps { + disabled?: boolean; + in: boolean; + submitting: boolean; + revert: () => void; + submit: () => void; +} + +export const SavingFloat = ({ in: inProp, submitting, revert, submit, disabled }: SavingFloatProps) => { + const { t } = useTranslation("dashboard"); + return ( + <> + + + + } + disabled={disabled} + > + {t("settings.save")} + + } + > + {t("settings.revert")} + + + + + ); +}; + +export const SettingContext = createContext({ + values: {}, + setSettings: () => {}, +}); + +const SettingsWrapper = ({ settings, children }: SettingsWrapperProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState<{ [key: string]: string }>({}); + const [modifiedValues, setModifiedValues] = useState<{ + [key: string]: string; + }>({}); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const setSettings = (settings: { [key: string]: string }) => { + setModifiedValues((prev) => ({ ...prev, ...settings })); + }; + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + useEffect(() => { + setLoading(true); + dispatch( + getSettings({ + keys: settings, + }), + ) + .then((res) => { + setValues(res); + setModifiedValues(res); + }) + .finally(() => { + setLoading(false); + }); + }, [settings]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + const modified: { [key: string]: string } = {}; + Object.keys(modifiedValues).forEach((key) => { + if (modifiedValues[key] !== values[key]) { + modified[key] = modifiedValues[key]; + } + }); + + setSubmitting(true); + dispatch( + sendSetSetting({ + settings: modified, + }), + ) + .then((res) => { + setValues((s) => ({ ...s, ...res })); + setModifiedValues((s) => ({ ...s, ...res })); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + {children} + + + )} + + + + + ); +}; + +export default SettingsWrapper; diff --git a/src/component/Admin/Settings/Settings.tsx b/src/component/Admin/Settings/Settings.tsx new file mode 100644 index 0000000..2c63551 --- /dev/null +++ b/src/component/Admin/Settings/Settings.tsx @@ -0,0 +1,370 @@ +import { Box, Container, FormHelperText, InputAdornment, styled } from "@mui/material"; +import { useQueryState } from "nuqs"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { QueueType } from "../../../api/dashboard.ts"; +import ResponsiveTabs, { Tab } from "../../Common/ResponsiveTabs.tsx"; +import Bot from "../../Icons/Bot.tsx"; +import Color from "../../Icons/Color.tsx"; +import CubeSync from "../../Icons/CubeSync.tsx"; +import CubeTree from "../../Icons/CubeTree.tsx"; +import Currency from "../../Icons/Currency.tsx"; +import FilmstripImage from "../../Icons/FilmstripImage.tsx"; +import Globe from "../../Icons/Globe.tsx"; +import MailOutlined from "../../Icons/MailOutlined.tsx"; +import PersonPasskey from "../../Icons/PersonPasskey.tsx"; +import SendLogging from "../../Icons/SendLogging.tsx"; +import Server from "../../Icons/Server.tsx"; +import PageContainer from "../../Pages/PageContainer.tsx"; +import PageHeader, { PageTabQuery } from "../../Pages/PageHeader.tsx"; +import Appearance from "./Appearance/Appearance.tsx"; +import Captcha from "./Captcha/Captcha.tsx"; +import Email from "./Email/Email.tsx"; +import Events from "./Event/Events.tsx"; +import Filesystem from "./Filesystem/Filesystem.tsx"; +import Media from "./Media/Media.tsx"; +import Queue from "./Queue/Queue.tsx"; +import ServerSetting from "./Server/ServerSetting.tsx"; +import SettingsWrapper from "./SettingWrapper.tsx"; +import SiteInformation from "./SiteInformation/SiteInformation.tsx"; +import UserSession from "./UserSession/UserSession.tsx"; +import VAS from "./VAS/VAS.tsx"; + +export const StyledInputAdornment = styled(InputAdornment)(({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, +})); + +export const SettingSection = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: theme.spacing(1), + [theme.breakpoints.up("md")]: { + padding: theme.spacing(0, 4), + }, +})); +export const SettingSectionContent = styled(Box)(({ theme }) => ({ + [theme.breakpoints.up("md")]: { + padding: theme.spacing(0, 4), + }, + display: "flex", + flexDirection: "column", + gap: theme.spacing(3), +})); +export const NoMarginHelperText = styled(FormHelperText)(() => ({ + marginLeft: 0, + marginRight: 0, +})); + +const allQueueSettings = Object.values(QueueType) + .map((queue) => [ + `queue_${queue}_worker_num`, + `queue_${queue}_max_execution`, + `queue_${queue}_backoff_factor`, + `queue_${queue}_backoff_max_duration`, + `queue_${queue}_max_retry`, + `queue_${queue}_retry_delay`, + ]) + .flat(); + +export enum SettingsPageTab { + SiteInformation = "siteInformation", + UserSession = "userSession", + Captcha = "captcha", + FileSystem = "fileSystem", + MediaProcessing = "mediaProcessing", + VAS = "vas", + Email = "email", + Queue = "queue", + Appearance = "appearance", + Events = "events", + Server = "server", +} + +const Settings = () => { + const { t } = useTranslation("dashboard"); + const [tab, setTab] = useQueryState(PageTabQuery); + + const tabs: Tab[] = useMemo(() => { + const res = []; + res.push( + ...[ + { + label: t("nav.basicSetting"), + value: SettingsPageTab.SiteInformation, + icon: , + }, + { + label: t("nav.userSession"), + value: SettingsPageTab.UserSession, + icon: , + }, + { + label: t("nav.captcha"), + value: SettingsPageTab.Captcha, + icon: , + }, + { + label: t("nav.fileSystem"), + value: SettingsPageTab.FileSystem, + icon: , + }, + { + label: t("nav.mediaProcessing"), + value: SettingsPageTab.MediaProcessing, + icon: , + }, + { + label: t("vas.vas"), + value: SettingsPageTab.VAS, + icon: , + }, + { + label: t("nav.email"), + value: SettingsPageTab.Email, + icon: , + }, + { + label: t("nav.queue"), + value: SettingsPageTab.Queue, + icon: , + }, + { + label: t("nav.appearance"), + value: SettingsPageTab.Appearance, + icon: , + }, + { + label: t("nav.events"), + value: SettingsPageTab.Events, + icon: , + }, + { + label: t("nav.server"), + value: SettingsPageTab.Server, + icon: , + }, + ], + ); + return res; + }, [t]); + + return ( + + + + setTab(newValue)} + tabs={tabs} + /> + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${tab}`} + > + + {(!tab || tab === SettingsPageTab.SiteInformation) && ( + + + + )} + {tab === SettingsPageTab.UserSession && ( + + + + )} + {tab === SettingsPageTab.Captcha && ( + + + + )} + {tab === SettingsPageTab.FileSystem && ( + + + + )} + {tab === SettingsPageTab.MediaProcessing && ( + + + + )} + {tab === SettingsPageTab.VAS && ( + + + + )} + {tab === SettingsPageTab.Email && ( + + + + )} + {tab === SettingsPageTab.Queue && ( + + + + )} + {tab === SettingsPageTab.Appearance && ( + + + + )} + {tab === SettingsPageTab.Events && ( + + + + )} + {tab === SettingsPageTab.Server && ( + + + + )} + + + + + + ); +}; + +export default Settings; diff --git a/src/component/Admin/Settings/SiteInformation/GeneralImagePreview.tsx b/src/component/Admin/Settings/SiteInformation/GeneralImagePreview.tsx new file mode 100644 index 0000000..a2a53f0 --- /dev/null +++ b/src/component/Admin/Settings/SiteInformation/GeneralImagePreview.tsx @@ -0,0 +1,39 @@ +import { Box } from "@mui/material"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; + +export interface GeneralImagePreviewProps { + src: string; +} + +const GeneralImagePreview = ({ src }: GeneralImagePreviewProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + return ( + + `1px solid ${t.palette.divider}`, + p: 1, + display: "inline-block", + borderRadius: (theme) => `${theme.shape.borderRadius}px`, + }} + > + + + + ); +}; + +export default GeneralImagePreview; diff --git a/src/component/Admin/Settings/SiteInformation/LogoPreview.tsx b/src/component/Admin/Settings/SiteInformation/LogoPreview.tsx new file mode 100644 index 0000000..b2d9a0f --- /dev/null +++ b/src/component/Admin/Settings/SiteInformation/LogoPreview.tsx @@ -0,0 +1,65 @@ +import { Box, Stack } from "@mui/material"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; + +export interface LogoPreviewProps { + logoLight: string; + logoDark: string; +} + +const LogoPreview = ({ logoLight, logoDark }: LogoPreviewProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + return ( + + theme.palette.grey[100], + p: 1, + borderRadius: (theme) => `${theme.shape.borderRadius}px`, + }} + > + + + theme.palette.grey[900], + p: 1, + borderRadius: (theme) => `${theme.shape.borderRadius}px`, + }} + > + + + + ); +}; + +export default LogoPreview; diff --git a/src/component/Admin/Settings/SiteInformation/SiteInformation.tsx b/src/component/Admin/Settings/SiteInformation/SiteInformation.tsx new file mode 100644 index 0000000..0dd3162 --- /dev/null +++ b/src/component/Admin/Settings/SiteInformation/SiteInformation.tsx @@ -0,0 +1,252 @@ +import { Box, FormControl, FormControlLabel, Stack, Switch, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { isTrueVal } from "../../../../session/utils.ts"; +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import SettingForm from "../../../Pages/Setting/SettingForm.tsx"; +import { NoMarginHelperText, SettingSection, SettingSectionContent, StyledInputAdornment } from "../Settings.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import GeneralImagePreview from "./GeneralImagePreview.tsx"; +import LogoPreview from "./LogoPreview.tsx"; +import SiteURLInput from "./SiteURLInput.tsx"; + +const SiteInformation = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + return ( + e.preventDefault()}> + + + + {t("settings.basicInformation")} + + + + + setSettings({ siteName: e.target.value })} + value={values.siteName} + required + inputProps={{ maxLength: 255 }} + /> + {t("settings.mainTitleDes")} + + + + + setSettings({ siteDes: e.target.value })} + value={values.siteDes} + multiline + rows={4} + /> + {t("settings.siteDescriptionDes")} + + + + + setSettings({ siteURL: v })} /> + + + + + setSettings({ siteScript: e.target.value })} + value={values.siteScript} + multiline + rows={4} + /> + {t("settings.customFooterHTMLDes")} + + + + + + {t("settings.announcementDes")} + + + + + setSettings({ tos_url: e.target.value })} + value={values.tos_url} + /> + {t("settings.tosUrlDes")} + + + + + setSettings({ privacy_policy_url: e.target.value })} + value={values.privacy_policy_url} + /> + {t("settings.privacyUrlDes")} + + + + + + + {t("settings.branding")} + + + + + + } + > + + setSettings({ site_logo: e.target.value })} + value={values.site_logo} + required + InputProps={{ + startAdornment: ( + + {t("settings.light")} + + ), + }} + /> + setSettings({ site_logo_light: e.target.value })} + value={values.site_logo_light} + required + InputProps={{ + startAdornment: ( + + {t("settings.dark")} + + ), + }} + /> + {t("settings.logoDes")} + + + + + + } + > + + setSettings({ pwa_small_icon: e.target.value })} + value={values.pwa_small_icon} + required + /> + {t("settings.smallIconDes")} + + + + + + } + > + + setSettings({ pwa_medium_icon: e.target.value })} + value={values.pwa_medium_icon} + required + /> + {t("settings.mediumIconDes")} + + + + + + } + > + + setSettings({ pwa_large_icon: e.target.value })} + value={values.pwa_large_icon} + required + /> + {t("settings.largeIconDes")} + + + + + + + {t("vas.mobileApp")} + + + + + + setSettings({ + show_app_promotion: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("vas.showAppPromotion")} + /> + {t("vas.showAppPromotionDes")} + + + + + + {t("vas.appLinkDes")} + + + + + + {t("vas.appLinkDes")} + + + + + + + ); +}; + +export default SiteInformation; diff --git a/src/component/Admin/Settings/SiteInformation/SiteURLInput.tsx b/src/component/Admin/Settings/SiteInformation/SiteURLInput.tsx new file mode 100644 index 0000000..081b535 --- /dev/null +++ b/src/component/Admin/Settings/SiteInformation/SiteURLInput.tsx @@ -0,0 +1,108 @@ +import { useTranslation } from "react-i18next"; +import { useMemo } from "react"; +import { + Box, + Collapse, + Divider, + IconButton, + InputAdornment, + Stack, +} from "@mui/material"; +import { + DenseFilledTextField, + SecondaryButton, +} from "../../../Common/StyledComponents.tsx"; +import FormControl from "@mui/material/FormControl"; +import Dismiss from "../../../Icons/Dismiss.tsx"; +import Add from "../../../Icons/Add.tsx"; +import { TransitionGroup } from "react-transition-group"; +import { NoMarginHelperText, StyledInputAdornment } from "../Settings.tsx"; + +export interface SiteURLInputProps { + urls: string; + onChange: (url: string) => void; +} + +const SiteURLInput = ({ urls, onChange }: SiteURLInputProps) => { + const { t } = useTranslation("dashboard"); + const urlSplit = useMemo(() => { + return urls.split(",").map((url) => url); + }, [urls]); + + const onUrlChange = + (index: number) => (e: React.ChangeEvent) => { + const newUrls = [...urlSplit]; + newUrls[index] = e.target.value; + onChange(newUrls.join(",")); + }; + + const removeUrl = (index: number) => () => { + const newUrls = [...urlSplit]; + newUrls.splice(index, 1); + onChange(newUrls.join(",")); + }; + + return ( + + + + {t("settings.primarySiteURL")} + + ), + }} + required + /> + + {t("settings.primarySiteURLDes")} + + + + {t("settings.secondaryDes")} + + {urlSplit.slice(1).map((url, index) => ( + + + + {t("settings.secondarySiteURL")} + + ), + endAdornment: ( + + + + + + ), + }} + required + /> + + + ))} + + + } + onClick={() => onChange(`${urls},`)} + > + {t("settings.addSecondary")} + + + + ); +}; + +export default SiteURLInput; diff --git a/src/component/Admin/Settings/UserSession/SSOSettings.tsx b/src/component/Admin/Settings/UserSession/SSOSettings.tsx new file mode 100644 index 0000000..266388d --- /dev/null +++ b/src/component/Admin/Settings/UserSession/SSOSettings.tsx @@ -0,0 +1,60 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; +import { Accordion, AccordionDetails, FormControlLabel, styled } from "@mui/material"; +import MuiAccordionSummary, { AccordionSummaryProps } from "@mui/material/AccordionSummary"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { StyledCheckbox } from "../../../Common/StyledComponents.tsx"; +import ProDialog from "../../Common/ProDialog.tsx"; + +export const AccordionSummary = styled((props: AccordionSummaryProps) => )( + ({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + paddingLeft: theme.spacing(4), + "& .MuiFormControlLabel-label": { + fontSize: theme.typography.body2.fontSize, + }, + "& .MuiCheckbox-root": { + marginRight: theme.spacing(2), + }, + }), +); + +export const StyledAccordion = styled(Accordion)(({ theme }) => ({ + boxShadow: "none", + border: `1px solid ${theme.palette.divider}`, + "&::before": { + display: "none", + }, +})); + +export interface SettingSectionProps {} + +const SSOSettings = () => { + const [open, setOpen] = useState(false); + const { t } = useTranslation("dashboard"); + const onClick = useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + setOpen(true); + }, []); + return ( + <> + setOpen(false)} /> +
+ + }> + } label={t("vas.qqConnect")} /> + + + + + }> + } label={t("settings.logto")} /> + + + +
+ + ); +}; + +export default SSOSettings; diff --git a/src/component/Admin/Settings/UserSession/UserSession.tsx b/src/component/Admin/Settings/UserSession/UserSession.tsx new file mode 100644 index 0000000..94e8c85 --- /dev/null +++ b/src/component/Admin/Settings/UserSession/UserSession.tsx @@ -0,0 +1,203 @@ +import { Box, FormControl, FormControlLabel, Link, ListItemText, Stack, Switch, Typography } from "@mui/material"; +import { useContext } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { isTrueVal } from "../../../../session/utils.ts"; +import SizeInput from "../../../Common/SizeInput.tsx"; +import { DenseFilledTextField, DenseSelect } from "../../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm.tsx"; +import GroupSelectionInput from "../../Common/GroupSelectionInput.tsx"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import SSOSettings from "./SSOSettings.tsx"; + +const UserSession = () => { + const { t } = useTranslation("dashboard"); + const { formRef, setSettings, values } = useContext(SettingContext); + + return ( + e.preventDefault()}> + + + + {t("settings.accountManagement")} + + + + + + setSettings({ + register_enabled: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.allowNewRegistrations")} + /> + {t("settings.allowNewRegistrationsDes")} + + + + + + setSettings({ + email_active: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.emailActivation")} + /> + + ]} + /> + + + + + + + setSettings({ + authn_enabled: e.target.checked ? "1" : "0", + }) + } + /> + } + label={t("settings.webauthn")} + /> + {t("settings.webauthnDes")} + + + + + + setSettings({ + default_group: g, + }) + } + /> + {t("settings.defaultGroupDes")} + + + + + + {["filterEmailProviderDisabled", "filterEmailProviderWhitelist", "filterEmailProviderBlacklist"].map( + (v, i) => ( + + + {t(`vas.${v}`)} + + + ), + )} + + {t("vas.filterEmailProviderDes")} + + + + + + + {t("settings.thirdPartySignIn")} + + + + + + + + + + {t("settings.avatar")} + + + + + + setSettings({ + avatar_path: e.target.value, + }) + } + required + /> + {t("settings.avatarFilePathDes")} + + + + + + setSettings({ + avatar_size: e.toString(), + }) + } + /> + {t("settings.avatarSizeDes")} + + + + + + setSettings({ + avatar_size_l: e.target.value, + }) + } + type={"number"} + inputProps={{ step: 1, min: 1 }} + required + /> + {t("settings.avatarImageSizeDes")} + + + + + + setSettings({ + gravatar_server: e.target.value, + }) + } + required + /> + {t("settings.gravatarServerDes")} + + + + + + + ); +}; + +export default UserSession; diff --git a/src/component/Admin/Settings/VAS/GiftCodes.tsx b/src/component/Admin/Settings/VAS/GiftCodes.tsx new file mode 100644 index 0000000..8f68739 --- /dev/null +++ b/src/component/Admin/Settings/VAS/GiftCodes.tsx @@ -0,0 +1,116 @@ +import { Box, Chip, Stack, Table, TableBody, TableContainer, TableHead, TableRow } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { AnyAction } from "redux"; +import { ThunkDispatch } from "redux-thunk"; +import { NoWrapCell, SecondaryButton, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; +import TablePagination from "../../Common/TablePagination.tsx"; + +interface GiftCodesProps { + storageProductsConfig: string; + groupProductsConfig: string; +} + +// Simplified GiftCode interface for our component use +interface GiftCode { + id: number; + code: string; + used: boolean; + qyt: number; +} + +// Pagination params +interface PaginationParams { + page: number; + perPage: number; + total: number; +} + +const GiftCodeStatusChip = ({ used }: { used: boolean }) => { + const { t } = useTranslation("dashboard"); + + return ( + + ); +}; + +const GiftCodes = ({ storageProductsConfig, groupProductsConfig }: GiftCodesProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useDispatch>(); + const { enqueueSnackbar } = useSnackbar(); + const [dialogOpen, setDialogOpen] = useState(false); + const [giftCodes, setGiftCodes] = useState([]); + const [loading, setLoading] = useState(false); + + // Pagination state + const [pagination, setPagination] = useState({ + page: 1, + perPage: 10, + total: 0, + }); + + const handleChangeRowsPerPage = (pageSize: number) => { + setPagination({ + page: 1, + perPage: pageSize, + total: pagination.total, + }); + }; + + return ( + + + } onClick={() => setDialogOpen(true)}> + {t("giftCodes.generateGiftCodes")} + + + + + + + + + # + {t("giftCodes.giftCodeProduct")} + {t("giftCodes.giftCodeAmount")} + {t("giftCodes.giftCode")} + {t("giftCodes.giftCodeStatus")} + + + + + {giftCodes.length === 0 && !loading && ( + + + {t("giftCodes.noGiftCodes")} + + + )} + +
+
+ {pagination?.total > 0 && ( + + setPagination({ ...pagination, page })} + /> + + )} +
+
+ ); +}; + +export default GiftCodes; diff --git a/src/component/Admin/Settings/VAS/GroupProducts.tsx b/src/component/Admin/Settings/VAS/GroupProducts.tsx new file mode 100644 index 0000000..ca1f214 --- /dev/null +++ b/src/component/Admin/Settings/VAS/GroupProducts.tsx @@ -0,0 +1,43 @@ +import { Box, Table, TableBody, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { NoWrapCell, SecondaryButton, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; + +const GroupProducts = () => { + const { t } = useTranslation("dashboard"); + + return ( + + + }> + {t("settings.addGroupProduct")} + + + + + + + + {t("settings.displayName")} + {t("settings.price")} + {t("settings.duration")} + {t("settings.description")} + {t("settings.actions")} + + + + + + + {t("application:setting.listEmpty")} + + + + +
+
+
+ ); +}; + +export default GroupProducts; diff --git a/src/component/Admin/Settings/VAS/PaymentProviders.tsx b/src/component/Admin/Settings/VAS/PaymentProviders.tsx new file mode 100644 index 0000000..917004e --- /dev/null +++ b/src/component/Admin/Settings/VAS/PaymentProviders.tsx @@ -0,0 +1,22 @@ +import { Add } from "@mui/icons-material"; +import { Box, Stack } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { SecondaryButton } from "../../../Common/StyledComponents"; + +export interface PaymentProviderProps {} + +const PaymentProviders: React.FC = ({}) => { + const { t } = useTranslation("dashboard"); + + return ( + + + }> + {t("settings.addPaymentProvider")} + + + + ); +}; + +export default PaymentProviders; diff --git a/src/component/Admin/Settings/VAS/StorageProducts.tsx b/src/component/Admin/Settings/VAS/StorageProducts.tsx new file mode 100644 index 0000000..2898a0a --- /dev/null +++ b/src/component/Admin/Settings/VAS/StorageProducts.tsx @@ -0,0 +1,43 @@ +import { Box, Table, TableBody, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { NoWrapCell, SecondaryButton, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; + +const StorageProducts = () => { + const { t } = useTranslation("dashboard"); + + return ( + + + }> + {t("settings.addStorageProduct")} + + + + + + + + {t("settings.displayName")} + {t("settings.price")} + {t("settings.duration")} + {t("settings.storageSize")} + {t("settings.actions")} + + + + + + + {t("application:setting.listEmpty")} + + + + +
+
+
+ ); +}; + +export default StorageProducts; diff --git a/src/component/Admin/Settings/VAS/VAS.tsx b/src/component/Admin/Settings/VAS/VAS.tsx new file mode 100644 index 0000000..be19599 --- /dev/null +++ b/src/component/Admin/Settings/VAS/VAS.tsx @@ -0,0 +1,225 @@ +import { + Box, + Button, + FormControl, + FormControlLabel, + InputAdornment, + Link, + Stack, + Switch, + Typography, +} from "@mui/material"; +import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useContext, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx"; +import SettingForm, { ProChip } from "../../../Pages/Setting/SettingForm.tsx"; +import ProDialog from "../../Common/ProDialog.tsx"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../Settings.tsx"; +import { SettingContext } from "../SettingWrapper.tsx"; +import GiftCodes from "./GiftCodes.tsx"; +import GroupProducts from "./GroupProducts.tsx"; +import PaymentProviders from "./PaymentProviders.tsx"; +import StorageProducts from "./StorageProducts.tsx"; +interface CurrencyOption { + code: string; + symbol: string; + unit: number; + label: string; +} + +const VAS = () => { + const { t } = useTranslation("dashboard"); + const [proOpen, setProOpen] = useState(false); + const { formRef, setSettings, values } = useContext(SettingContext); + const currencyPopupState = usePopupState({ + variant: "popover", + popupId: "currencySelector", + }); + const paymentConfig = useMemo(() => JSON.parse(values.payment || "{}"), [values.payment]); + const storageProducts = useMemo(() => values.storage_products || "[]", [values.storage_products]); + const groupSellData = useMemo(() => values.group_sell_data || "[]", [values.group_sell_data]); + + const onProClick = (e: React.MouseEvent) => { + e.preventDefault(); + setProOpen(true); + }; + + return ( + + setProOpen(false)} /> + + + + {t("settings.creditAndVAS")} + + + + + } label={t("settings.enableCredit")} /> + {t("settings.enableCreditDes")} + + + + + + + + {t("settings.creditPriceDes")} + + + + + + + {t("settings.shareScoreRateDes")} + + + + + + + + {t("vas.banBufferPeriodDes")} + + + + + + + + ]} + /> + + + + + + + + + ]} + /> + + + + + + + } label={t("settings.anonymousPurchase")} /> + {t("settings.anonymousPurchaseDes")} + + + + + + } label={t("settings.shopNavEnabled")} /> + {t("settings.shopNavEnabledDes")} + + + + + + + + {t("settings.paymentSettings")} + + + + + + + + ), + }, + }} + /> + {t("settings.currencyCodeDes")} + + + + + + + {t("settings.currencySymbolDes")} + + + + + + + {t("settings.currencyUnitDes")} + + + + + + {t("settings.paymentProviders")} + + + + + + + + + + + {t("settings.storageProductSettings")} + + + + + + {t("settings.storageProductsDes")} + + + + + + + + {t("settings.groupProductSettings")} + + + + + + {t("settings.groupProductsDes")} + + + + + + + + {t("giftCodes.giftCodesSettings")} + + + + + + + + ); +}; + +export default VAS; diff --git a/src/component/Admin/Share/Share.js b/src/component/Admin/Share/Share.js deleted file mode 100644 index 3bf6014..0000000 --- a/src/component/Admin/Share/Share.js +++ /dev/null @@ -1,498 +0,0 @@ -import { lighten } from "@material-ui/core"; -import Badge from "@material-ui/core/Badge"; -import Button from "@material-ui/core/Button"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import Link from "@material-ui/core/Link"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Toolbar from "@material-ui/core/Toolbar"; -import Tooltip from "@material-ui/core/Tooltip"; -import Typography from "@material-ui/core/Typography"; -import { Delete, FilterList } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import ShareFilter from "../Dialogs/ShareFilter"; -import { formatLocalTime } from "../../../utils/datetime"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, - highlight: - theme.palette.type === "light" - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, -})); - -export default function Share() { - const { t } = useTranslation("dashboard", { keyPrefix: "share" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [shares, setShares] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [filter, setFilter] = useState({}); - const [users, setUsers] = useState({}); - const [ids, setIds] = useState({}); - const [search, setSearch] = useState({}); - const [orderBy, setOrderBy] = useState(["id", "desc"]); - const [filterDialog, setFilterDialog] = useState(false); - const [selected, setSelected] = useState([]); - const [loading, setLoading] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const loadList = () => { - API.post("/admin/share/list", { - page: page, - page_size: pageSize, - order_by: orderBy.join(" "), - conditions: filter, - searches: search, - }) - .then((response) => { - setUsers(response.data.users); - setIds(response.data.ids); - setShares(response.data.items); - setTotal(response.data.total); - setSelected([]); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, orderBy, filter, search]); - - const deletePolicy = (id) => { - setLoading(true); - API.post("/admin/share/delete", { id: [id] }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteBatch = () => { - setLoading(true); - API.post("/admin/share/delete", { id: selected }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = shares.map((n) => n.ID); - setSelected(newSelecteds); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - selected.slice(0, selectedIndex), - selected.slice(selectedIndex + 1) - ); - } - - setSelected(newSelected); - }; - - const isSelected = (id) => selected.indexOf(id) !== -1; - - return ( -
- setFilterDialog(false)} - setSearch={setSearch} - setFilter={setFilter} - /> -
-
- - setFilterDialog(true)} - > - - - - - - -
-
- - - {selected.length > 0 && ( - - - {tDashboard("user.selectedObjects", { - num: selected.length, - })} - - - - - - - - )} - - - - - - 0 && - selected.length < shares.length - } - checked={ - shares.length > 0 && - selected.length === shares.length - } - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all desserts", - }} - /> - - - - setOrderBy([ - "id", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - # - {orderBy[0] === "id" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "source_name", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("objectName")} - {orderBy[0] === "source_name" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {tDashboard("policy.type")} - - - - setOrderBy([ - "views", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("views")} - {orderBy[0] === "views" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "downloads", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("downloads")} - {orderBy[0] === "downloads" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {t("autoExpire")} - - - {t("owner")} - - - {t("createdAt")} - - - {tDashboard("policy.actions")} - - - - - {shares.map((row) => ( - - - - handleClick(event, row.ID) - } - checked={isSelected(row.ID)} - /> - - {row.ID} - - - - {row.Views} - - - {row.Downloads} - - - {row.RemainDownloads > -1 && - t("afterNDownloads", { - num: row.RemainDownloads, - })} - {row.RemainDownloads === -1 && - t("none")} - - - - {users[row.UserID] - ? users[row.UserID].Nick - : tDashboard( - "file.unknownUploader" - )} - - - - {formatLocalTime(row.CreatedAt)} - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/Share/ShareDialog/ShareDialog.tsx b/src/component/Admin/Share/ShareDialog/ShareDialog.tsx new file mode 100644 index 0000000..5bc0fa6 --- /dev/null +++ b/src/component/Admin/Share/ShareDialog/ShareDialog.tsx @@ -0,0 +1,84 @@ +import { Box, DialogContent } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getShareDetail } from "../../../../api/api.ts"; +import { Share } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import ShareForm from "./ShareForm.tsx"; + +export interface ShareDialogProps { + open: boolean; + onClose: () => void; + shareID?: number; +} + +const ShareDialog = ({ open, onClose, shareID }: ShareDialogProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ edges: {}, id: 0 }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!shareID || !open) { + return; + } + setLoading(true); + dispatch(getShareDetail(shareID)) + .then((res) => { + setValues(res); + }) + .catch(() => { + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [open]); + + return ( + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && } + + + + + + + ); +}; + +export default ShareDialog; diff --git a/src/component/Admin/Share/ShareDialog/ShareForm.tsx b/src/component/Admin/Share/ShareDialog/ShareForm.tsx new file mode 100644 index 0000000..79e1181 --- /dev/null +++ b/src/component/Admin/Share/ShareDialog/ShareForm.tsx @@ -0,0 +1,122 @@ +import { Box, Grid2 as Grid, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Share } from "../../../../api/dashboard"; +import { NoWrapTypography } from "../../../Common/StyledComponents"; +import TimeBadge from "../../../Common/TimeBadge"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import FileTypeIcon from "../../../FileManager/Explorer/FileTypeIcon"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import FileDialog from "../../File/FileDialog/FileDialog"; +import UserDialog from "../../User/UserDialog/UserDialog"; + +const ShareForm = ({ values }: { values: Share }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation("dashboard"); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(0); + const [fileDialogOpen, setFileDialogOpen] = useState(false); + const [fileDialogID, setFileDialogID] = useState(0); + + const userClicked = (e: React.MouseEvent) => { + e.preventDefault(); + setUserDialogOpen(true); + setUserDialogID(values?.edges?.user?.id ?? 0); + }; + + const fileClicked = (e: React.MouseEvent) => { + e.preventDefault(); + setFileDialogOpen(true); + setFileDialogID(values?.edges?.file?.id ?? 0); + }; + + return ( + <> + setUserDialogOpen(false)} userID={userDialogID} /> + setFileDialogOpen(false)} fileID={fileDialogID} /> + + + + + {values.id} + + + + + + + {values.share_link} + + + + + + + + + + {values?.edges?.user?.nick} + + + + + + + + + {values.views ?? 0} + + + + + + {values.downloads ?? 0} + + + + + + {values?.edges?.file ? ( + + + + + {values?.edges?.file?.name} + + + + ) : ( + {t("share.deleted")} + )} + + + + + + + + + + + ); +}; + +export default ShareForm; diff --git a/src/component/Admin/Share/ShareFilterPopover.tsx b/src/component/Admin/Share/ShareFilterPopover.tsx new file mode 100644 index 0000000..9304719 --- /dev/null +++ b/src/component/Admin/Share/ShareFilterPopover.tsx @@ -0,0 +1,111 @@ +import { Box, Button, Popover, PopoverProps, Stack } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseFilledTextField } from "../../Common/StyledComponents"; +import SettingForm from "../../Pages/Setting/SettingForm"; + +export interface ShareFilterPopoverProps extends PopoverProps { + user: string; + setUser: (user: string) => void; + file: string; + setFile: (file: string) => void; + clearFilters: () => void; +} + +const ShareFilterPopover = ({ + user, + setUser, + file, + setFile, + clearFilters, + onClose, + open, + ...rest +}: ShareFilterPopoverProps) => { + const { t } = useTranslation("dashboard"); + + // Create local state to track changes before applying + const [localUser, setLocalUser] = useState(user); + const [localFile, setLocalFile] = useState(file); + + // Initialize local state when popup opens + useEffect(() => { + if (open) { + setLocalUser(user); + setLocalFile(file); + } + }, [open]); + + // Apply filters and close popover + const handleApplyFilters = () => { + setUser(localUser); + setFile(localFile); + onClose?.({}, "backdropClick"); + }; + + // Reset filters and close popover + const handleResetFilters = () => { + setLocalUser(""); + setLocalFile(""); + clearFilters(); + onClose?.({}, "backdropClick"); + }; + + return ( + + + + setLocalUser(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalFile(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + + + + + + ); +}; + +export default ShareFilterPopover; diff --git a/src/component/Admin/Share/ShareList.tsx b/src/component/Admin/Share/ShareList.tsx new file mode 100644 index 0000000..bcaf9fe --- /dev/null +++ b/src/component/Admin/Share/ShareList.tsx @@ -0,0 +1,336 @@ +import { Delete } from "@mui/icons-material"; +import { + Badge, + Box, + Button, + Checkbox, + Container, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { batchDeleteShares, getShareList } from "../../../api/api"; +import { AdminListService, Share } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import ArrowSync from "../../Icons/ArrowSync"; +import Filter from "../../Icons/Filter"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import FileDialog from "../File/FileDialog/FileDialog"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import UserDialog from "../User/UserDialog/UserDialog"; +import ShareDialog from "./ShareDialog/ShareDialog"; +import ShareFilterPopover from "./ShareFilterPopover"; +import ShareRow from "./ShareRow"; + +export const UserQuery = "user"; +export const FileQuery = "file"; + +const ShareList = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [shares, setShares] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [user, setUser] = useQueryState(UserQuery, { defaultValue: "" }); + const [file, setFile] = useQueryState(FileQuery, { defaultValue: "" }); + + const [count, setCount] = useState(0); + const [selected, setSelected] = useState([]); + const filterPopupState = usePopupState({ + variant: "popover", + popupId: "shareFilterPopover", + }); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(undefined); + const [openFile, setOpenFile] = useState(undefined); + const [openFileDialogOpen, setOpenFileDialogOpen] = useState(false); + const [openShare, setOpenShare] = useState(undefined); + const [openShareDialogOpen, setOpenShareDialogOpen] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 10; + + const clearFilters = useCallback(() => { + setUser(""); + setFile(""); + }, [setUser, setFile]); + + useEffect(() => { + fetchShares(); + }, [page, pageSize, orderBy, orderDirection, user, file]); + + const fetchShares = () => { + setLoading(true); + setSelected([]); + + const params: AdminListService = { + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: { + share_file_id: file, + share_user_id: user, + }, + }; + + dispatch(getShareList(params)) + .then((res) => { + setShares(res.shares); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleDelete = () => { + setDeleteLoading(true); + dispatch(confirmOperation(t("share.confirmBatchDelete", { num: selected.length }))) + .then(() => { + dispatch(batchDeleteShares({ ids: Array.from(selected) })) + .then(() => { + fetchShares(); + }) + .finally(() => { + setDeleteLoading(false); + }); + setDeleteLoading(false); + }) + .finally(() => { + setDeleteLoading(false); + }); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = shares.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleSelect = useCallback( + (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); + } + setSelected(newSelected); + }, + [selected], + ); + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + const hasActiveFilters = useMemo(() => { + return !!(user || file); + }, [user, file]); + + const handleUserDialogOpen = (id: number) => { + setUserDialogID(id); + setUserDialogOpen(true); + }; + + const handleOpenFile = (fileID: number) => { + setOpenFile(fileID); + setOpenFileDialogOpen(true); + }; + + const handleOpenShare = (shareID: number) => { + setOpenShare(shareID); + setOpenShareDialogOpen(true); + }; + + return ( + + setUserDialogOpen(false)} userID={userDialogID} /> + setOpenFileDialogOpen(false)} fileID={openFile} /> + setOpenShareDialogOpen(false)} shareID={openShare} /> + + + + + + }> + {t("node.refresh")} + + + + } variant="contained" {...bindTrigger(filterPopupState)}> + {t("user.filter")} + + + + {selected.length > 0 && !isMobile && ( + <> + + + + )} + + {isMobile && selected.length > 0 && ( + + + + )} + + + + + + 0 && selected.length < shares.length} + checked={shares.length > 0 && selected.length === shares.length} + onChange={handleSelectAllClick} + /> + + + + {t("group.#")} + + + {t("share.srcFileName")} + + + {t("share.views")} + + + + + {t("share.downloads")} + + + + + {t("share.price")} + + + {t("share.autoExpire")} + {t("share.owner")} + + + {t("share.createdAt")} + + + + + + + {!loading && + shares.map((share) => ( + + ))} + {loading && + shares.length > 0 && + shares.slice(0, 10).map((share) => )} + {loading && + shares.length === 0 && + Array.from(Array(10)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default ShareList; diff --git a/src/component/Admin/Share/ShareRow.tsx b/src/component/Admin/Share/ShareRow.tsx new file mode 100644 index 0000000..0fce281 --- /dev/null +++ b/src/component/Admin/Share/ShareRow.tsx @@ -0,0 +1,226 @@ +import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Share } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { NoWrapTableCell, NoWrapTypography } from "../../Common/StyledComponents"; +import TimeBadge from "../../Common/TimeBadge"; +import UserAvatar from "../../Common/User/UserAvatar"; +import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon"; +import { ShareExpires } from "../../FileManager/TopBar/ShareInfoPopover"; +import Delete from "../../Icons/Delete"; +import Open from "../../Icons/Open"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { batchDeleteShares } from "../../../api/api"; + +export interface ShareRowProps { + share?: Share; + loading?: boolean; + deleting?: boolean; + selected?: boolean; + onDelete?: () => void; + onDetails?: (id: number) => void; + onSelect?: (id: number) => void; + openUserDialog?: (id: number) => void; + openFileDialog?: (id: number) => void; +} + +const ShareRow = ({ + share, + loading, + deleting, + selected, + onDelete, + onDetails, + onSelect, + openUserDialog, + openFileDialog, +}: ShareRowProps) => { + const navigate = useNavigate(); + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [deleteLoading, setDeleteLoading] = useState(false); + const [openLoading, setOpenLoading] = useState(false); + const onRowClick = () => { + onDetails?.(share?.id ?? 0); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(confirmOperation(t("share.confirmDelete"))).then(() => { + if (share?.id) { + setDeleteLoading(true); + dispatch(batchDeleteShares({ ids: [share.id] })) + .then(() => { + onDelete?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + } + }); + }; + + const onSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onSelect?.(share?.id ?? 0); + }; + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const userClicked = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + openUserDialog?.(share?.edges?.user?.id ?? 0); + }; + + const fileClicked = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + openFileDialog?.(share?.edges?.file?.id ?? 0); + }; + + const onOpenLink = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + window.open(share?.share_link ?? "", "_blank"); + }; + + return ( + + + + + + {share?.id} + + + {share?.edges?.file ? ( + + + + + {share?.edges?.file?.name} + + + + ) : ( + {t("share.deleted")} + )} + + + {share?.views ?? 0} + + + {share?.downloads ?? 0} + + + {share?.price} + + + + + + + + {share?.edges?.user && ( + + + + + {share?.edges?.user?.nick} + + + + )} + + + + + + + + + + + + + + + + ); +}; + +export default ShareRow; diff --git a/src/component/Admin/StoragePolicy/AddWizardDialog.tsx b/src/component/Admin/StoragePolicy/AddWizardDialog.tsx new file mode 100644 index 0000000..98d921f --- /dev/null +++ b/src/component/Admin/StoragePolicy/AddWizardDialog.tsx @@ -0,0 +1,87 @@ +import { Box, DialogContent } from "@mui/material"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { upsertStoragePolicy } from "../../../api/api"; +import { StoragePolicy } from "../../../api/dashboard"; +import { PolicyType } from "../../../api/explorer"; +import { useAppDispatch } from "../../../redux/hooks"; +import AutoHeight from "../../Common/AutoHeight"; +import FacebookCircularProgress from "../../Common/CircularProgress"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; +import { PolicyPropsMap } from "./StoragePolicySetting"; + +export interface AddWizardDialogProps { + open: boolean; + onClose: () => void; + type: PolicyType; +} + +export interface AddWizardProps { + onSubmit: (data: StoragePolicy) => void; +} + +const AddWizardDialog = ({ open, onClose, type }: AddWizardDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const Wizard = PolicyPropsMap[type].wizard; + + const onSubmit = useCallback( + (data: StoragePolicy) => { + setLoading(true); + dispatch(upsertStoragePolicy({ policy: data })) + .then((res) => { + onClose(); + navigate(`/admin/policy/${res.id}`); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch], + ); + + return ( + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {!loading && Wizard && } + {loading && ( + + + + )} + + + + + + + ); +}; + +export default AddWizardDialog; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketACLInput.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketACLInput.tsx new file mode 100644 index 0000000..6dc6835 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketACLInput.tsx @@ -0,0 +1,88 @@ +import { Box, ListItemText, OutlinedSelectProps, Typography } from "@mui/material"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { PolicyType } from "../../../../api/explorer"; +import { DenseSelect } from "../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu"; + +export interface BucketACLInputProps extends OutlinedSelectProps { + value?: boolean; + phraseVariant?: PolicyType; + onChange: (value: boolean) => void; +} + +const BucketACLInput = ({ value, phraseVariant = PolicyType.oss, onChange, ...props }: BucketACLInputProps) => { + const { t } = useTranslation("dashboard"); + const { privateLocale, publicLocale } = useMemo(() => { + switch (phraseVariant) { + case PolicyType.oss: + case PolicyType.obs: + return { privateLocale: "policy.privateBucket", publicLocale: "policy.publicBucket" }; + case PolicyType.cos: + return { privateLocale: "policy.accessTypePrivate", publicLocale: "policy.accessTypePulic" }; + case PolicyType.upyun: + return { privateLocale: "policy.tokenEnabled", publicLocale: "policy.tokenDisabled" }; + default: + return { privateLocale: "policy.privateBucket", publicLocale: "policy.publicBucket" }; + } + }, [phraseVariant]); + return ( + ( + + {value === "1" ? t(privateLocale) : t(publicLocale)} + + )} + onChange={(e) => onChange(e.target.value === "1")} + {...props} + MenuProps={{ + PaperProps: { sx: { maxWidth: 230 } }, + MenuListProps: { + sx: { + "& .MuiMenuItem-root": { + whiteSpace: "normal", + }, + }, + }, + }} + > + + + + {t(privateLocale)} + + + {t("policy.privateDes")} + + + + + + + {t(publicLocale)} + + + {t("policy.publicDes")} + + + + + ); +}; + +export default BucketACLInput; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketCorsTable.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketCorsTable.tsx new file mode 100644 index 0000000..d1cf2aa --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/BucketCorsTable.tsx @@ -0,0 +1,41 @@ +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { NoWrapTableCell, StyledTableContainerPaper } from "../../../Common/StyledComponents"; + +export interface BucketCorsTableProps { + exposedHeaders?: string[]; +} + +const BucketCorsTable = ({ exposedHeaders }: BucketCorsTableProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + + {t("policy.origin")} + {t("policy.allowMethods")} + {t("policy.allowHeaders")} + {t("policy.exposeHeaders")} + {t("policy.maxAge")} + + + + + * + + {["GET", "POST", "PUT", "DELETE", "HEAD"].map((t) => ( +
{t}
+ ))} +
+ * + {exposedHeaders?.map((h) =>
{h}
)}
+ 3600 +
+
+
+
+ ); +}; + +export default BucketCorsTable; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/EditStoragePolicy.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/EditStoragePolicy.tsx new file mode 100644 index 0000000..0c73f7a --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/EditStoragePolicy.tsx @@ -0,0 +1,26 @@ +import { Container } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import PageContainer from "../../../Pages/PageContainer"; +import PageHeader from "../../../Pages/PageHeader"; +import StoragePolicyForm from "./StoragePolicyForm"; +import StoragePolicySettingWrapper from "./StoragePolicySettingWrapper"; + +const EditStoragePolicy = () => { + const { t } = useTranslation("dashboard"); + const { id } = useParams(); + const [name, setName] = useState(""); + return ( + + + + setName(p.name)}> + + + + + ); +}; + +export default EditStoragePolicy; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx new file mode 100644 index 0000000..364ba76 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/BasicInfoSection.tsx @@ -0,0 +1,590 @@ +import { + Checkbox, + CircularProgress, + Collapse, + FormControl, + FormControlLabel, + Link, + ListItemText, + SelectChangeEvent, + Typography, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { createStoragePolicyCors, getOneDriveDriverRoot } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import NodeSelectionInput from "../../../Common/NodeSelectionInput"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { PolicyPropsMap } from "../../StoragePolicySetting"; +import GraphEndpointSelection from "../../Wizards/OneDrive/GraphEndpointSelection"; +import BucketACLInput from "../BucketACLInput"; +import BucketCorsTable from "../BucketCorsTable"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; +import OdSignInStatus from "./OdSignInStatus"; + +export const SharePointDriverPending = "sharepoint_pending"; + +const BasicInfoSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + const [corsLoading, setCorsLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + + // Extract sharepoint URL from od_driver if it exists and is not the default + const initialSharepointUrl = useMemo(() => { + if (values.settings?.od_driver && values.settings.od_driver !== "me/drive") { + return values.settings.od_driver; + } + return ""; + }, [values.settings?.od_driver]); + + const [sharepointUrl, setSharepointUrl] = useState(initialSharepointUrl); + const [sharepointLoading, setSharepointLoading] = useState(false); + + const onNameChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, name: e.target.value })); + }, + [setPolicy], + ); + + const onNodeChange = useCallback( + (value: number) => { + setPolicy((p: StoragePolicy) => ({ ...p, node_id: value > 0 ? value : undefined })); + }, + [setPolicy], + ); + + const onBucketNameChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, bucket_name: e.target.value })); + }, + [setPolicy], + ); + + const showBucket = useMemo(() => { + return ( + values.type === PolicyType.oss || + values.type === PolicyType.cos || + values.type === PolicyType.obs || + values.type === PolicyType.qiniu || + values.type === PolicyType.s3 || + values.type === PolicyType.upyun + ); + }, [values.type]); + + const showEndpoint = useMemo(() => { + return values.type === PolicyType.cos || values.type === PolicyType.obs || values.type === PolicyType.s3; + }, [values.type]); + + const showCors = useMemo(() => { + return ( + values.type === PolicyType.oss || + values.type === PolicyType.cos || + values.type === PolicyType.obs || + values.type === PolicyType.s3 + ); + }, [values.type]); + + const policyProps = useMemo(() => { + return PolicyPropsMap[values.type]; + }, [values.type]); + + const onBucketTypeChange = useCallback( + (value: boolean) => { + setPolicy((p: StoragePolicy) => ({ ...p, is_private: value ? true : undefined })); + }, + [setPolicy], + ); + + const onEndpointChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, server: e.target.value })); + }, + [setPolicy], + ); + + const onIntranetEndpointChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, server_side_endpoint: e.target.value ? e.target.value : undefined }, + })); + }, + [setPolicy], + ); + + const onUseCnameChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, use_cname: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onAccessKeyChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, access_key: e.target.value })); + }, + [setPolicy], + ); + + const onSecretKeyChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, secret_key: e.target.value })); + }, + [setPolicy], + ); + + const onS3ForcePathStyleChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, s3_path_style: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onS3RegionChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, settings: { ...p.settings, region: e.target.value } })); + }, + [setPolicy], + ); + + const onS3DeleteBatchSizeChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { + ...p.settings, + s3_delete_batch_size: parseInt(e.target.value) ? parseInt(e.target.value) : undefined, + }, + })); + }, + [setPolicy], + ); + + const onGraphEndpointChange = useCallback( + (value: string) => { + setPolicy((p: StoragePolicy) => ({ ...p, server: value })); + }, + [setPolicy], + ); + + const onDriverTypeChange = useCallback( + (e: SelectChangeEvent) => { + const value = e.target.value as string; + if (value === "default") { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, od_driver: "me/drive" }, + })); + } else { + // When switching to SharePoint, set an empty URL initially + setSharepointUrl(""); + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, od_driver: SharePointDriverPending }, // Temporary value to trigger the collapse + })); + } + }, + [setPolicy, setSharepointUrl], + ); + + const onSharepointUrlChange = useCallback((e: React.ChangeEvent) => { + setSharepointUrl(e.target.value); + }, []); + + const handleSharepointUrlBlur = useCallback(() => { + if (!sharepointUrl || sharepointUrl.startsWith("sites/")) return; + + setSharepointLoading(true); + dispatch(getOneDriveDriverRoot(values.id, sharepointUrl)) + .then((res) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, od_driver: res }, + })); + setSharepointUrl(res); + }) + .finally(() => { + setSharepointLoading(false); + }); + }, [sharepointUrl, setPolicy]); + + const handleCreateCors = useCallback(() => { + setCorsLoading(true); + dispatch( + createStoragePolicyCors({ + policy: values, + }), + ) + .then(() => { + enqueueSnackbar(t("policy.corsPolicyAdded"), { + variant: "success", + action: DefaultCloseAction, + }); + }) + .finally(() => { + setCorsLoading(false); + }); + }, [dispatch, values, t]); + + const onOdTpsLimitChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, tps_limit: parseFloat(e.target.value) ? parseFloat(e.target.value) : undefined }, + })); + }, + [setPolicy], + ); + + const onOdTpsLimitBurstChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { + ...p.settings, + tps_limit_burst: parseInt(e.target.value) ? parseInt(e.target.value) : undefined, + }, + })); + }, + [setPolicy], + ); + + const onTokenChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, token: e.target.value ? e.target.value : undefined }, + })); + }, + [setPolicy], + ); + return ( + + + {t("policy.basicInfo")} + + + + + + {t("policy.policyName")} + + + {showBucket && ( + <> + + + {policyProps.bucketNameDes} + + + + + {t("policy.bucketTypeDes")} + + + {values.type === PolicyType.upyun && ( + + + + {t("policy.upyunTokenSecretDes")} + + + )} + {showEndpoint && ( + + + {policyProps.endpointDes} + {values.type == PolicyType.obs && ( + <> + + } + label={t("policy.thisIsACustomDomain")} + /> + {t("policy.thisIsACustomDomainDes")} + + )} + {values.type === PolicyType.s3 && ( + <> + + } + label={t("policy.usePathEndpoint")} + /> + + ]} /> + + + )} + + )} + {values.type === PolicyType.oss && ( + <> + + + + , , ]} /> + + + } + label={t("policy.thisIsACustomDomain")} + /> + {t("policy.thisIsACustomDomainDes")} + + + + {t("policy.ossLANEndpointDes")} + + + )} + {values.type == PolicyType.s3 && ( + + + + ]} /> + + + )} + + + + + {policyProps.credentialDes} + + + + )} + {values.type === PolicyType.remote && ( + + + + ]} + /> + + + )} + {showCors && ( + + + + {t("policy.ossCORSDes")} + + + {t("policy.letCloudreveHelpMe")} + + + )} + {values.type === PolicyType.s3 && ( + + + + ]} /> + + + )} + {values.type == PolicyType.onedrive && ( + <> + + + {t("policy.aadAccountCloudDes")} + + + + + + + + + + {t("policy.saveToDefaultOneDrive")} + + + + + {t("policy.saveToSharePoint")} + + + + {t("policy.driverRootDes")} + + + + : null, + }, + }} + /> + {t("policy.sharePointUrlDes")} + + + + + + + {t("policy.tpsDes")} + + {t("policy.tpsBurstDes")} + + + + )} + + + ); +}; + +export default BasicInfoSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/DownloadSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/DownloadSection.tsx new file mode 100644 index 0000000..30c105c --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/DownloadSection.tsx @@ -0,0 +1,168 @@ +import { Box, Checkbox, Collapse, FormControl, FormControlLabel, Switch, Typography } from "@mui/material"; +import { useCallback, useContext } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { TrafficDiagram } from "../../TrafficDiagram"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; + +const DownloadSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + + const onDownloadCdnChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, custom_proxy: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onProxyServerChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, proxy_server: e.target.value }, + })); + }, + [setPolicy], + ); + + const onInternalProxyChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, internal_proxy: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onStreamSaverChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, stream_saver: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onSkipSignChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, source_auth: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + return ( + + + {t("policy.download")} + + + + + + } + label={t("policy.useDownloadCdn")} + /> + + + + {t("policy.downloadCdnDes")} + {values.type == PolicyType.cos && values.is_private && ( + + + } + label={t("policy.skipSign")} + /> + {t("policy.skipSignDes")} + + )} + + + + + {values.type !== PolicyType.local && ( + + + } + label={t("policy.downloadRelay")} + /> + + + + + + )} + {values.type === PolicyType.onedrive && ( + + + } + label={t("policy.streamSaver")} + /> + + + + + + )} + {values.type !== PolicyType.local && ( + + + + )} + + + ); +}; + +export default DownloadSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/MediaMetadataSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/MediaMetadataSection.tsx new file mode 100644 index 0000000..9e598d1 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/MediaMetadataSection.tsx @@ -0,0 +1,124 @@ +import { FormControl, FormControlLabel, Link, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { PolicyPropsMap } from "../../StoragePolicySetting"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; + +const MediaMetadataSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + + const policyProps = useMemo(() => { + return PolicyPropsMap[values.type]; + }, [values.type]); + + const onNativeMediaMetaExtsChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { + ...p.settings, + media_meta_exts: e.target.value === "" ? undefined : e.target.value.split(",").map((ext) => ext.trim()), + }, + })); + }, + [setPolicy], + ); + + const onMediaMetaGeneratorProxyChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, media_meta_generator_proxy: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const noNativeExtractor = useMemo(() => { + return values.type === PolicyType.s3 || values.type === PolicyType.onedrive; + }, [values.type]); + + if (values.type === PolicyType.local) { + return null; + } + + return ( + + + {t("settings.extractMediaMeta")} + + + {!noNativeExtractor && ( + ]} + /> + } + lgWidth={5} + > + + + + ] + : [] + } + /> + {policyProps.nativeExtractorDes && ( + ]} + /> + )} + + + + )} + + + + } + label={t("policy.mediaExtractorProxy")} + /> + + ]} + /> + + + + + + ); +}; + +export default MediaMetadataSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/OdSignInStatus.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/OdSignInStatus.tsx new file mode 100644 index 0000000..b4abd32 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/OdSignInStatus.tsx @@ -0,0 +1,98 @@ +import { Box, Typography } from "@mui/material"; +import { useContext, useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getPolicyOauthCredentialRefreshTime, getPolicyOauthUrl } from "../../../../../api/api"; +import { OauthCredentialStatus } from "../../../../../api/dashboard"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import FacebookCircularProgress from "../../../../Common/CircularProgress"; +import { SecondaryButton } from "../../../../Common/StyledComponents"; +import TimeBadge from "../../../../Common/TimeBadge"; +import CheckCircleFilled from "../../../../Icons/CheckCircleFilled"; +import DismissCircleFilled from "../../../../Icons/DismissCircleFilled"; +import OneDriveAuthDialog from "../../Wizards/OneDrive/OneDriveAuthDialog"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; + +const OdSignInStatus = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + const dispatch = useAppDispatch(); + const [status, setStatus] = useState(undefined); + const [loading, setLoading] = useState(false); + const [authorizing, setAuthorizing] = useState(false); + const [authDialogOpen, setAuthDialogOpen] = useState(false); + + useEffect(() => { + if (!values.access_key) { + setStatus({ valid: false, last_refresh_time: "" }); + return; + } + setLoading(true); + dispatch(getPolicyOauthCredentialRefreshTime(values.id.toString())) + .then((res) => { + setStatus(res); + }) + .finally(() => { + setLoading(false); + }); + }, [values.id, values.secret_key]); + + const authorized = !loading && status && !!status.last_refresh_time; + + const handleOpenAuthDialog = () => { + setAuthDialogOpen(true); + }; + + const handleCloseAuthDialog = () => { + setAuthDialogOpen(false); + }; + + const handleConfirmAuth = (appId: string, appSecret: string) => { + setAuthorizing(true); + dispatch(getPolicyOauthUrl({ id: values.id, secret: appSecret, app_id: appId })) + .then((res) => { + window.location.href = res; + }) + .catch(() => { + setAuthorizing(false); + }); + }; + + return ( + + {loading && } + {!loading && ( + <> + {!authorized && ( + + + {t("policy.notGranted")} + + )} + {authorized && ( + + + ]} + /> + + )} + + {t(authorized ? "policy.authorizeAgain" : "policy.authorizeNow")} + + + )} + + + + ); +}; + +export default OdSignInStatus; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/StorageAndUploadSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/StorageAndUploadSection.tsx new file mode 100644 index 0000000..3a4d30e --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/StorageAndUploadSection.tsx @@ -0,0 +1,225 @@ +import { FormControl, FormControlLabel, Link, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import SizeInput from "../../../../Common/SizeInput"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import MagicVarDialog from "../../../Common/MagicVarDialog"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { PolicyPropsMap } from "../../StoragePolicySetting"; +import { TrafficDiagram } from "../../TrafficDiagram"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; +import { fileMagicVars, pathMagicVars } from "./magicVars"; + +const StorageAndUploadSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + const [magicVarDialogOpen, setMagicVarDialogOpen] = useState(false); + const [dialogType, setDialogType] = useState<"path" | "file">("path"); + + const policyProps = useMemo(() => { + return PolicyPropsMap[values.type]; + }, [values.type]); + + const showPreallocate = useMemo(() => { + return values.type === PolicyType.local || values.type === PolicyType.remote; + }, [values.type]); + + const onDirNameChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, dir_name_rule: e.target.value })); + }, + [setPolicy], + ); + + const onFileNameChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ ...p, file_name_rule: e.target.value })); + }, + [setPolicy], + ); + + const handlePathMagicVarClick = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setDialogType("path"); + setMagicVarDialogOpen(true); + }, []); + + const handleFileMagicVarClick = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setDialogType("file"); + setMagicVarDialogOpen(true); + }, []); + + const onMaxSizeChange = useCallback( + (e: number) => { + setPolicy((p: StoragePolicy) => ({ ...p, max_size: e ? e : undefined })); + }, + [setPolicy], + ); + + const fileExts = useMemo(() => { + return values.settings?.file_type?.join() ?? ""; + }, [values.settings?.file_type]); + + const onFileExtsChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { + ...(p.settings ?? {}), + file_type: e.target.value === "" ? undefined : e.target.value.split(",").map((ext) => ext.trim()), + }, + })); + }, + [setPolicy], + ); + + const onChunkSizeChange = useCallback( + (size: number) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, chunk_size: size === 0 ? undefined : size }, + })); + }, + [setPolicy], + ); + + const onPreallocateChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, pre_allocate: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onUploadRelayChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, relay: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + return ( + + + {t("policy.storageAndUpload")} + + + + + + + ]} + /> + {t("policy.nameRuleImmutable")} + + + + + + + + ]} + /> + {t("policy.nameRuleImmutable")} + + + + + + + {t("policy.maxSizeOfSingleFileDes")} + + + + + + {t("policy.enterFileExt")} + + + {values.type !== PolicyType.upyun && ( + + + + + {t("policy.chunkSizeDesSuffix", { + prefix: t(policyProps.chunkSizeDes ?? ""), + })} + + + + )} + {showPreallocate && ( + + + } + label={t("policy.preallocate")} + /> + {t("policy.preallocateDes")} + + + )} + {values.type !== PolicyType.local && ( + <> + + + } + label={t("policy.uploadRelay")} + /> + {t("policy.uploadRelayDes")} + + + + + + + )} + + setMagicVarDialogOpen(false)} + vars={dialogType === "path" ? pathMagicVars : fileMagicVars} + /> + + ); +}; + +export default StorageAndUploadSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/ThumbnailsSection.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/ThumbnailsSection.tsx new file mode 100644 index 0000000..13d8ca3 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/ThumbnailsSection.tsx @@ -0,0 +1,154 @@ +import { Checkbox, FormControl, FormControlLabel, Link, Switch, Typography } from "@mui/material"; +import { useCallback, useContext, useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import SizeInput from "../../../../Common/SizeInput"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../../Settings/Settings"; +import { PolicyPropsMap } from "../../StoragePolicySetting"; +import { StoragePolicySettingContext } from "../StoragePolicySettingWrapper"; + +const ThumbnailsSection = () => { + const { t } = useTranslation("dashboard"); + const { values, setPolicy } = useContext(StoragePolicySettingContext); + + const policyProps = useMemo(() => { + return PolicyPropsMap[values.type]; + }, [values.type]); + + const noNativeThumbnail = useMemo(() => { + return values.type === PolicyType.local || values.type === PolicyType.s3; + }, [values.type]); + + const onNativeThumbnailChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { + ...p.settings, + thumb_exts: e.target.value === "" ? undefined : e.target.value.split(",").map((ext) => ext.trim()), + }, + })); + }, + [setPolicy], + ); + + const onNativeThumbnailSupportAllExtsChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, thumb_support_all_exts: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + const onNativeThumbnailMaxSizeChange = useCallback( + (size: number) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, thumb_max_size: size === 0 ? undefined : size }, + })); + }, + [setPolicy], + ); + + const onThumbProxyChange = useCallback( + (e: React.ChangeEvent) => { + setPolicy((p: StoragePolicy) => ({ + ...p, + settings: { ...p.settings, thumb_generator_proxy: e.target.checked ? true : undefined }, + })); + }, + [setPolicy], + ); + + if (values.type === PolicyType.local) { + return null; + } + + return ( + + + {t("settings.thumbnails")} + + + {!noNativeThumbnail && ( + <> + + + + + } + label={t("policy.nativeThumbNailsSupportAllExts")} + /> + + {t("policy.nativeThumbNailsGeneralDes")} + {policyProps.nativeThumbDes && ( + ] : [] + } + /> + )} + + + + + + + {t("policy.nativeThumbnailMaxSizeDes")} + + + + )} + + + + } + label={t("policy.thumbProxy")} + /> + + ]} + /> + + + + + + ); +}; + +export default ThumbnailsSection; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts new file mode 100644 index 0000000..8a3d0bd --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/index.ts @@ -0,0 +1,7 @@ +export { default as BasicInfoSection } from './BasicInfoSection'; +export { default as DownloadSection } from './DownloadSection'; +export * from './magicVars'; +export { default as MediaMetadataSection } from './MediaMetadataSection'; +export { default as StorageAndUploadSection } from './StorageAndUploadSection'; +export { default as ThumbnailsSection } from './ThumbnailsSection'; + diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/magicVars.ts b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/magicVars.ts new file mode 100644 index 0000000..49cf450 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/FormSections/magicVars.ts @@ -0,0 +1,34 @@ +import { MagicVar } from "../../../Common/MagicVarDialog"; + +export const commonMagicVars: MagicVar[] = [ + { name: "{randomkey16}", value: "policy.magicVar.16digitsRandomString", example: "a1b2c3d4e5f6g7h8" }, + { name: "{randomkey8}", value: "policy.magicVar.8digitsRandomString", example: "a1b2c3d4" }, + { name: "{timestamp}", value: "policy.magicVar.secondTimestamp", example: "1609459200" }, + { name: "{timestamp_nano}", value: "policy.magicVar.nanoTimestamp", example: "1609459200000000000" }, + { name: "{randomnum2}", value: "policy.magicVar.randomNumber", example: "0-1" }, + { name: "{randomnum3}", value: "policy.magicVar.randomNumber", example: "0-2" }, + { name: "{randomnum4}", value: "policy.magicVar.randomNumber", example: "0-3" }, + { name: "{randomnum8}", value: "policy.magicVar.randomNumber", example: "0-7" }, + { name: "{uid}", value: "policy.magicVar.uid", example: "1" }, + { name: "{datetime}", value: "policy.magicVar.dateAndTime", example: "20220101120000" }, + { name: "{date}", value: "policy.magicVar.date", example: "20220101" }, + { name: "{year}", value: "policy.magicVar.year", example: "2022" }, + { name: "{month}", value: "policy.magicVar.month", example: "01" }, + { name: "{day}", value: "policy.magicVar.day", example: "01" }, + { name: "{hour}", value: "policy.magicVar.hour", example: "12" }, + { name: "{minute}", value: "policy.magicVar.minute", example: "00" }, + { name: "{second}", value: "policy.magicVar.second", example: "00" }, +]; + +export const pathMagicVars: MagicVar[] = [ + ...commonMagicVars, + { name: "{path}", value: "policy.magicVar.path", example: "/path/to/" } +]; + +export const fileMagicVars: MagicVar[] = [ + ...commonMagicVars, + { name: "{originname}", value: "policy.magicVar.originalFileName", example: "example.jpg" }, + { name: "{ext}", value: "policy.magicVar.extension", example: ".jpg" }, + { name: "{originname_without_ext}", value: "policy.magicVar.originFileNameNoext", example: "example" }, + { name: "{uuid}", value: "policy.magicVar.uuidV4", example: "550e8400-e29b-41d4-a716-446655440000" }, +]; \ No newline at end of file diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx new file mode 100644 index 0000000..d6873e6 --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicyForm.tsx @@ -0,0 +1,40 @@ +import { Alert, Box, Link, Stack } from "@mui/material"; +import { useContext } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { + BasicInfoSection, + DownloadSection, + MediaMetadataSection, + StorageAndUploadSection, + ThumbnailsSection, +} from "./FormSections"; +import { StoragePolicySettingContext } from "./StoragePolicySettingWrapper"; + +const StoragePolicyForm = () => { + const { t } = useTranslation("dashboard"); + const { formRef, values } = useContext(StoragePolicySettingContext); + + return ( + e.preventDefault()}> + {!values.edges?.groups?.length && ( + + ]} + /> + + )} + + + + + + + + + ); +}; + +export default StoragePolicyForm; diff --git a/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicySettingWrapper.tsx b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicySettingWrapper.tsx new file mode 100644 index 0000000..abc032f --- /dev/null +++ b/src/component/Admin/StoragePolicy/EditStoragePolicy/StoragePolicySettingWrapper.tsx @@ -0,0 +1,143 @@ +import { Box } from "@mui/material"; +import * as React from "react"; +import { createContext, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getStoragePolicyDetail, upsertStoragePolicy } from "../../../../api/api.ts"; +import { StoragePolicy } from "../../../../api/dashboard.ts"; +import { PolicyType } from "../../../../api/explorer.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import { SavingFloat } from "../../Settings/SettingWrapper.tsx"; +import { SharePointDriverPending } from "./FormSections/BasicInfoSection.tsx"; + +export interface StoragePolicySettingWrapperProps { + policyID: number; + children: React.ReactNode; + onPolicyChange: (policy: StoragePolicy) => void; +} + +export interface StoragePolicySettingContextProps { + values: StoragePolicy; + setPolicy: (f: (p: StoragePolicy) => StoragePolicy) => void; + formRef?: React.RefObject; +} + +const defaultPolicy: StoragePolicy = { + id: 0, + type: PolicyType.local, + name: "", + edges: {}, +}; + +export const StoragePolicySettingContext = createContext({ + values: { ...defaultPolicy }, + setPolicy: () => {}, +}); + +const StoragePolicySettingWrapper = ({ policyID, children, onPolicyChange }: StoragePolicySettingWrapperProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ + ...defaultPolicy, + }); + const [modifiedValues, setModifiedValues] = useState({ + ...defaultPolicy, + }); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + useEffect(() => { + setLoading(true); + dispatch(getStoragePolicyDetail(policyID)) + .then((res) => { + setValues(res); + setModifiedValues(res); + onPolicyChange(res); + }) + .finally(() => { + setLoading(false); + }); + }, [policyID]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + setSubmitting(true); + dispatch( + upsertStoragePolicy({ + policy: { ...modifiedValues, edges: {} }, + }), + ) + .then((res) => { + setValues(res); + setModifiedValues(res); + onPolicyChange(res); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + {children} + + + )} + + + + + ); +}; + +export default StoragePolicySettingWrapper; diff --git a/src/component/Admin/StoragePolicy/OauthCallback.tsx b/src/component/Admin/StoragePolicy/OauthCallback.tsx new file mode 100644 index 0000000..0ef5482 --- /dev/null +++ b/src/component/Admin/StoragePolicy/OauthCallback.tsx @@ -0,0 +1,56 @@ +import { Box, Container } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { finishOauthCallback } from "../../../api/api"; +import { useAppDispatch } from "../../../redux/hooks"; +import { useQuery } from "../../../util"; +import FacebookCircularProgress from "../../Common/CircularProgress"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +const OauthCallback = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const navigate = useNavigate(); + + const query = useQuery(); + + useEffect(() => { + const code = query.get("code"); + const state = query.get("state"); + if (code && state) { + dispatch(finishOauthCallback({ code, state })).finally(() => { + navigate(`/admin/policy/${state}`); + }); + } else { + enqueueSnackbar(t("policy.oauthCallbackFailed"), { + variant: "error", + action: DefaultCloseAction, + }); + } + }, []); + + return ( + + + + + + + + + + ); +}; + +export default OauthCallback; diff --git a/src/component/Admin/StoragePolicy/SelectProvider.tsx b/src/component/Admin/StoragePolicy/SelectProvider.tsx new file mode 100644 index 0000000..7099eb6 --- /dev/null +++ b/src/component/Admin/StoragePolicy/SelectProvider.tsx @@ -0,0 +1,53 @@ +import { Card, CardActionArea, CardContent, CardMedia, DialogContent, Grid2, styled, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { PolicyType } from "../../../api/explorer"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; +import { PolicyPropsMap } from "./StoragePolicySetting"; + +export interface SelectProviderProps { + open: boolean; + onClose: () => void; + onSelect: (provider: PolicyType) => void; +} + +const StyledCard = styled(Card)(({ theme }) => ({ + display: "flex", + boxShadow: "none", + border: `1px solid ${theme.palette.divider}`, +})); + +const SelectProvider = ({ open, onClose, onSelect }: SelectProviderProps) => { + const { t } = useTranslation("dashboard"); + return ( + + + + {Object.values(PolicyType).map((type) => ( + + + onSelect(type)}> + + + + {t(PolicyPropsMap[type].name)} + + + + + + ))} + + + + ); +}; + +export default SelectProvider; diff --git a/src/component/Admin/StoragePolicy/StoragePolicyCard.tsx b/src/component/Admin/StoragePolicy/StoragePolicyCard.tsx new file mode 100644 index 0000000..63e9cab --- /dev/null +++ b/src/component/Admin/StoragePolicy/StoragePolicyCard.tsx @@ -0,0 +1,178 @@ +import { Box, Divider, IconButton, Link, Skeleton, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { deleteStoragePolicy, getStoragePolicyDetail } from "../../../api/api"; +import { StoragePolicy } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { sizeToString } from "../../../util"; +import { NoWrapBox, SquareChip } from "../../Common/StyledComponents"; +import Delete from "../../Icons/Delete"; +import Info from "../../Icons/Info"; +import { BorderedCardClickableBaImg } from "../Common/AdminCard"; +import { PolicyPropsMap } from "./StoragePolicySetting"; + +export interface StoragePolicyCardProps { + policy?: StoragePolicy; + onRefresh?: () => void; + loading?: boolean; +} + +const StoragePolicyCard = ({ policy, onRefresh, loading }: StoragePolicyCardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [detail, setDetail] = useState(undefined); + const [deleteLoading, setDeleteLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const navigate = useNavigate(); + + const loadDetail = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (policy) { + setDetailLoading(true); + dispatch(getStoragePolicyDetail(policy.id, true)).then((res) => { + setDetail(res); + setDetailLoading(false); + }); + } + }, + [policy, dispatch], + ); + + const handleDelete = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch(confirmOperation(t("policy.deletePolicyConfirmation", { name: policy?.name ?? "" }))).then(() => { + setDeleteLoading(true); + dispatch(deleteStoragePolicy(policy?.id ?? 0)) + .then(() => { + onRefresh?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + }); + }, + [policy, dispatch, onRefresh], + ); + + // If loading is true, render a skeleton placeholder + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } + + return ( + + navigate(`/admin/policy/${policy?.id}`)} + img={policy ? PolicyPropsMap[policy.type].img : undefined} + > + + + {policy?.name} + + {policy && ( + + {t(PolicyPropsMap[policy.type].name)} + + )} + + + {policy?.edges.groups?.map((group) => ( + + ))} + {!policy?.edges.groups && ( + + + {t("policy.noGroupBinded")} + + )} + + + + + {detailLoading ? ( + + ) : detail ? ( + t("policy.policySummary", { + count: detail.entities_count ?? 0, + size: sizeToString(detail.entities_size ?? 0), + }) + ) : ( + + {t("policy.loadSummary")} + + )} + + + + + + + + + ); +}; + +export default StoragePolicyCard; diff --git a/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx b/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx new file mode 100644 index 0000000..97a83b7 --- /dev/null +++ b/src/component/Admin/StoragePolicy/StoragePolicySetting.tsx @@ -0,0 +1,451 @@ +import { Box, Container, Link, ListItemText, Stack, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { SelectChangeEvent } from "@mui/material/Select"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getStoragePolicyList } from "../../../api/api"; +import { StoragePolicy } from "../../../api/dashboard"; +import { PolicyType } from "../../../api/explorer"; +import { useAppDispatch } from "../../../redux/hooks"; +import { DenseSelect, SecondaryButton } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import Add from "../../Icons/Add"; +import ArrowSync from "../../Icons/ArrowSync"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import { BorderedCardClickable } from "../Common/AdminCard"; +import { Code } from "../Common/Code"; +import TablePagination from "../Common/TablePagination"; +import AddWizardDialog, { AddWizardProps } from "./AddWizardDialog"; +import SelectProvider from "./SelectProvider"; +import StoragePolicyCard from "./StoragePolicyCard"; +import CosWizard from "./Wizards/COS/CosWizard"; +import LocalWizard from "./Wizards/Local/LocalWizard"; +import ObsWizard from "./Wizards/OBS/ObsWizard"; +import OneDriveWizard from "./Wizards/OneDrive/OneDriveWizard"; +import OssWizard from "./Wizards/OSS/OssWizard"; +import QiniuWizard from "./Wizards/Qiniu/QiniuWizard"; +import RemoteWizard from "./Wizards/Remote/RemoteWizard"; +import S3Wizard from "./Wizards/S3/S3Wizard"; +import UpyunWizard from "./Wizards/Upyun/UpyunWizard"; + +export const PageQuery = "page"; +export const PageSizeQuery = "page_size"; +export const OrderByQuery = "order_by"; +export const OrderDirectionQuery = "order_direction"; +export const PolicyTypeQuery = "policy_type"; + +export interface PolicyProps { + name: string; + img: string; + wizardSize?: "sm" | "md" | "lg"; + wizard?: (props: AddWizardProps) => React.ReactNode; + chunkSizeDes?: string; + chunkSizeMin?: number; + chunkSizeMax?: number; + nativeThumbDes?: string; + nativeThumbDoc?: string; + nativeExtractorName?: string; + nativeExtractorDoc?: string; + nativeExtractorDes?: string; + nativeExtractorDesDoc?: string; + bucketName?: string; + bucketNameDes?: React.ReactNode; + bucketType?: string; + endpointName?: string; + endpointDes?: React.ReactNode; + akName?: string; + skName?: string; + credentialDes?: React.ReactNode; + corsExposedHeaders?: string[]; + endpointNotEnforcePrefix?: boolean; +} + +export const PolicyPropsMap: Record = { + [PolicyType.local]: { + name: "policy.local", + img: "/static/img/local.png", + wizardSize: "sm", + wizard: LocalWizard, + chunkSizeDes: "policy.chunkSizeDes", + }, + [PolicyType.remote]: { + name: "policy.remote", + img: "/static/img/remote.png", + wizardSize: "sm", + wizard: RemoteWizard, + nativeThumbDes: "policy.nativeThumbNailsGeneralRemote", + nativeExtractorName: "policy.mediaExtractorNative", + nativeExtractorDes: "policy.nativeMediaMetaExtsRemote", + chunkSizeDes: "policy.chunkSizeDes", + }, + [PolicyType.s3]: { + name: "policy.s3", + img: "/static/img/s3.png", + wizardSize: "sm", + wizard: S3Wizard, + bucketName: "policy.bucketName", + bucketType: "policy.bucketType", + endpointName: "policy.policyEndpoint", + endpointDes: ]} />, + akName: "Access Key", + skName: "Secret Key", + chunkSizeMin: 5 * 1024 * 1024, //5MB + chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB + chunkSizeDes: "policy.chunkSizeDesS3", + }, + [PolicyType.cos]: { + name: "policy.cos", + img: "/static/img/cos.png", + wizardSize: "sm", + wizard: CosWizard, + bucketName: "policy.cosObsBucketName", + bucketNameDes: ( + , ]} + /> + ), + bucketType: "policy.accessType", + endpointName: "policy.accessDomain", + endpointDes: , ]} />, + chunkSizeMin: 1024 * 1024, //1MB + chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB + chunkSizeDes: "policy.chunkSizeDesQiniuCos", + nativeThumbDes: "policy.nativeThumbNailsGeneralCos", + nativeThumbDoc: "https://cloud.tencent.com/document/product/436/113312", + nativeExtractorName: "policy.mediaExtractorCos", + nativeExtractorDes: "policy.nativeMediaMetaExtCos", + nativeExtractorDesDoc: "https://console.cloud.tencent.com/ci", + nativeExtractorDoc: "https://console.cloud.tencent.com/ci", + akName: "SecretId", + skName: "SecretKey", + corsExposedHeaders: ["ETag"], + credentialDes: ( + , + , + , + ]} + /> + ), + }, + [PolicyType.oss]: { + name: "policy.oss", + img: "/static/img/oss.png", + wizardSize: "sm", + wizard: OssWizard, + chunkSizeMin: 100 * 1024, //100KB + chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB + chunkSizeDes: "policy.chunkSizeDesOssObs", + nativeThumbDes: "policy.nativeThumbNailsGeneralOss", + nativeThumbDoc: "https://help.aliyun.com/zh/oss/user-guide/overview-17/", + nativeExtractorName: "policy.mediaExtractorOss", + nativeExtractorDes: "policy.nativeMediaMetaExtOss", + nativeExtractorDoc: "https://help.aliyun.com/zh/oss/user-guide/quick-start-2326698", + nativeExtractorDesDoc: "https://help.aliyun.com/zh/oss/user-guide/quick-start-2326698", + bucketName: "policy.bucketName", + bucketNameDes: ( + , , ]} + /> + ), + bucketType: "policy.bucketType", + akName: "AccessKey ID", + skName: "AccessKey Secret", + credentialDes: ( + , + , + , + ]} + /> + ), + }, + [PolicyType.obs]: { + name: "policy.obs", + img: "/static/img/obs.png", + wizardSize: "sm", + wizard: ObsWizard, + bucketName: "policy.cosObsBucketName", + bucketNameDes: ( + , + , + , + , + ]} + /> + ), + bucketType: "policy.bucketPolicy", + endpointName: "policy.policyEndpoint", + endpointDes: , ]} />, + endpointNotEnforcePrefix: true, + akName: "Access Key Id", + skName: "Secret Access Key", + credentialDes: ( + , + , + , + , + ]} + /> + ), + corsExposedHeaders: ["ETag"], + chunkSizeMin: 100 * 1024, //100KB + chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB + chunkSizeDes: "policy.chunkSizeDesOssObs", + nativeThumbDes: "policy.nativeThumbNailsGeneralObs", + nativeThumbDoc: "https://support.huaweicloud.com/intl/zh-cn/usermanual-obs/obs_01_0001.html", + nativeExtractorName: "policy.mediaExtractorObs", + nativeExtractorDes: "policy.nativeMediaMetaExtObs", + nativeExtractorDoc: "https://support.huaweicloud.com/intl/zh-cn/usermanual-obs/obs_01_0410.html", + nativeExtractorDesDoc: "https://support.huaweicloud.com/intl/zh-cn/usermanual-obs/obs_01_0410.html", + }, + [PolicyType.qiniu]: { + name: "policy.qiniu", + img: "/static/img/qiniu.png", + wizardSize: "sm", + wizard: QiniuWizard, + bucketName: "policy.qiniuBucketName", + bucketNameDes: ( + ]} + /> + ), + bucketType: "policy.aclType", + akName: "AK", + skName: "SK", + credentialDes: , + nativeThumbDes: "policy.nativeThumbNailsGeneralQiniu", + nativeThumbDoc: "https://developer.qiniu.com/dora/api/basic-processing-images-imageview2", + nativeExtractorName: "policy.mediaExtractorQiniu", + nativeExtractorDes: "policy.nativeMediaMetaExtQiniu", + nativeExtractorDoc: "https://www.qiniu.com/products/dora", + chunkSizeMin: 1024 * 1024, //1 MB + chunkSizeMax: 1024 * 1024 * 1024, //1GB + chunkSizeDes: "policy.chunkSizeDesQiniuCos", + }, + [PolicyType.upyun]: { + name: "policy.upyun", + img: "/static/img/upyun.png", + wizardSize: "sm", + wizard: UpyunWizard, + bucketName: "policy.storageServiceName", + bucketNameDes: ( + ]} + /> + ), + bucketType: "policy.tokenStatus", + akName: "policy.operatorName", + skName: "policy.operatorPassword", + nativeThumbDes: "policy.nativeThumbNailsGeneralUpyun", + nativeThumbDoc: "https://help.upyun.com/knowledge-base/image/", + nativeExtractorName: "policy.mediaExtractorUpyun", + nativeExtractorDes: "policy.nativeMediaMetaExtUpyun", + nativeExtractorDesDoc: "https://help.upyun.com/knowledge-base/image/#e58583e695b0e68daee88eb7e58f96", + nativeExtractorDoc: "https://help.upyun.com/knowledge-base/image/", + }, + [PolicyType.onedrive]: { + name: "policy.onedrive", + img: "/static/img/onedrive.png", + wizardSize: "sm", + wizard: OneDriveWizard, + chunkSizeMin: 5 * 1024 * 1024, //5MB + chunkSizeMax: 5 * 1024 * 1024 * 1024, //5GB + chunkSizeDes: "policy.chunkSizeDesOd", + bucketName: "policy.storageServiceName", + bucketNameDes: ( + ]} + /> + ), + bucketType: "policy.tokenStatus", + }, +}; + +const StoragePolicySetting = () => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [policies, setPolicies] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "11", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [policyType, setPolicyType] = useQueryState(PolicyTypeQuery, { + defaultValue: " ", + }); + const [count, setCount] = useState(0); + const [selectProviderOpen, setSelectProviderOpen] = useState(false); + const [newPolicyType, setNewPolicyType] = useState(null); + const [createNewOpen, setCreateNewOpen] = useState(false); + + const [open, setOpen] = useState(false); + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 11; + + useEffect(() => { + fetchPolicies(); + }, [page, pageSize, orderBy, orderDirection, policyType]); + + const fetchPolicies = () => { + setLoading(true); + dispatch( + getStoragePolicyList({ + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: { + policy_type: policyType == " " ? "" : policyType, + }, + }), + ) + .then((res) => { + setPolicies(res.policies); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const onAddNewPolcyWithType = useCallback((type: PolicyType) => { + setNewPolicyType(type); + setSelectProviderOpen(false); + setCreateNewOpen(true); + }, []); + + const onPolicyTypeChange = useCallback((e: SelectChangeEvent) => { + setPolicyType(e.target.value as PolicyType); + setPage("1"); + }, []); + + return ( + + setSelectProviderOpen(false)} + onSelect={onAddNewPolcyWithType} + /> + {newPolicyType && ( + setCreateNewOpen(false)} type={newPolicyType} /> + )} + + + + }> + {t("node.refresh")} + + ( + + {v == " " ? t("policy.all") : t(PolicyPropsMap[v as PolicyType].name)} + + )} + > + + + {t("policy.all")} + + + {Object.values(PolicyType).map((type) => ( + + + {t(PolicyPropsMap[type].name)} + + + ))} + + + + + setSelectProviderOpen(true)} + sx={{ + height: "100%", + borderStyle: "dashed", + display: "flex", + alignItems: "center", + gap: 1, + justifyContent: "center", + color: (t) => t.palette.text.secondary, + }} + > + + {t("policy.newStoragePolicy")} + + + {!loading && policies.map((p) => )} + {loading && + policies.length > 0 && + policies.map((p) => )} + {loading && + policies.length === 0 && + Array.from(Array(5)).map((_, index) => )} + + {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} + + + ); +}; + +export default StoragePolicySetting; diff --git a/src/component/Admin/StoragePolicy/TrafficDiagram.tsx b/src/component/Admin/StoragePolicy/TrafficDiagram.tsx new file mode 100644 index 0000000..b205f49 --- /dev/null +++ b/src/component/Admin/StoragePolicy/TrafficDiagram.tsx @@ -0,0 +1,222 @@ +import { Box, ListItemText, useMediaQuery, useTheme } from "@mui/material"; +import { forwardRef, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseSelect, NoWrapTypography } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import ArrowLeft from "../../Icons/ArrowLeft"; +import GlobeFilled from "../../Icons/GlobeFilled"; +import Home from "../../Icons/Home"; +import Person from "../../Icons/Person"; +import Storage from "../../Icons/Storage"; +import WindowApps from "../../Icons/WindowApps"; +import { BorderedCard } from "../Common/AdminCard"; + +export interface TrafficDiagramProps { + variant: "upload" | "download"; + proxyed?: boolean; + cdn?: boolean; + storageNodeTitle?: string; + internalEndpoint?: boolean; + proxyNodeTitle?: string; +} + +enum Source { + web = "web", + dav = "dav", + web_edit = "web_edit", + wopi = "wopi", +} + +enum Node { + cloudreve = "cloudreve", + proxy = "proxy", + storage_node = "storage_node", + storage_node_internal = "storage_node_internal", + user = "user", + arrow = "arrow", + wopi = "wopi", +} + +interface NodeIconProps extends TrafficDiagramProps { + type: Node; + size?: number; +} + +const NodeIcon = forwardRef(({ type, size = 40, storageNodeTitle, proxyNodeTitle, variant }: NodeIconProps, ref) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const icon = useMemo(() => { + switch (type) { + case Node.cloudreve: + return t.palette.action.active }} />; + case Node.proxy: + return t.palette.action.active }} />; + case Node.storage_node: + case Node.storage_node_internal: + return t.palette.action.active }} />; + case Node.user: + return t.palette.action.active }} />; + case Node.wopi: + return t.palette.action.active }} />; + case Node.arrow: + return ( + t.palette.action.disabled, + transform: + variant == "upload" + ? isMobile + ? "rotate(-90deg)" + : "rotate(180deg)" + : isMobile + ? "rotate(90deg)" + : "rotate(0deg)", + }} + /> + ); + } + }, [type, isMobile]); + + const title = useMemo(() => { + switch (type) { + case Node.cloudreve: + return "Cloudreve"; + case Node.proxy: + return proxyNodeTitle ?? t("policy.customProxy"); + case Node.storage_node: + return storageNodeTitle ?? t("policy.storageNode"); + case Node.storage_node_internal: + return t("policy.storageNodeInternal"); + case Node.user: + return t("nav.users"); + case Node.wopi: + return t("settings.wopiViewer"); + } + }, [type]); + + return ( + + {icon} + {title && ( + + {title} + + )} + + ); +}); + +export const TrafficDiagram = ({ + variant, + cdn, + proxyed, + internalEndpoint, + storageNodeTitle, + proxyNodeTitle, +}: TrafficDiagramProps) => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [source, setSource] = useState(Source.web); + const nodes = useMemo(() => { + const res: Node[] = []; + if (source == Source.wopi) { + res.push(Node.wopi); + } else { + res.push(Node.user); + } + if (variant == "upload") { + if (proxyed || source == Source.dav || source == Source.web_edit) { + res.push(Node.cloudreve); + } + } else { + if (proxyed || source == Source.wopi) { + res.push(Node.cloudreve); + } + + if (cdn) { + res.push(Node.proxy); + } + } + + if (variant == "upload" && internalEndpoint && (source == Source.dav || source == Source.web_edit || proxyed)) { + res.push(Node.storage_node_internal); + } else if (variant == "download" && internalEndpoint && (source == Source.wopi || proxyed) && !cdn) { + res.push(Node.storage_node_internal); + } else { + res.push(Node.storage_node); + } + + // join arrow between existing nodes + const joinedNodes: Node[] = []; + for (const node of res) { + joinedNodes.push(node); + joinedNodes.push(Node.arrow); + } + return joinedNodes.slice(0, -1); + }, [proxyed, source, cdn, variant, internalEndpoint]); + return ( + + setSource(e.target.value as Source)}> + + + {t("policy.sourceWeb")} + + + + + {t("policy.sourceDav")} + + + {variant == "upload" && ( + + + {t("policy.sourceWebEdit")} + + + )} + {variant == "download" && ( + + + {t("settings.wopiViewer")} + + + )} + + + {nodes.map((node) => ( + + ))} + + + ); +}; diff --git a/src/component/Admin/StoragePolicy/Wizards/COS/CosWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/COS/CosWizard.tsx new file mode 100644 index 0000000..574bf00 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/COS/CosWizard.tsx @@ -0,0 +1,171 @@ +import { Button, Collapse, FormControl, Link, Stack } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { createStoragePolicyCors } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; +import BucketCorsTable from "../../EditStoragePolicy/BucketCorsTable"; +const CosWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const [corsAdded, setCorsAdded] = useState(false); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.cos, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + thumb_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp"], + media_meta_exts: ["jpg", "jpeg", "png", "bmp", "webp", "tiff", "avif", "heif"], + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const hamdleCreateCors = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + setLoading(true); + dispatch(createStoragePolicyCors({ policy })) + .then(() => { + enqueueSnackbar(t("policy.corsPolicyAdded"), { variant: "success", action: DefaultCloseAction }); + setCorsAdded(true); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + , ]} + /> + + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, server: e.target.value })} + variant={"outlined"} + /> + + , ]} /> + + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + , + , + , + ]} + /> + + + + + + + {t("policy.ossCORSDes")} + + + + setCorsAdded(true)}> + {t("policy.addedManually")} + + + + + + + + + ); +}; + +export default CosWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/Local/LocalWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/Local/LocalWizard.tsx new file mode 100644 index 0000000..d211ecc --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/Local/LocalWizard.tsx @@ -0,0 +1,52 @@ +import { Button } from "@mui/material"; +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; + +const LocalWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + name: "", + type: PolicyType.local, + file_name_rule: "{uuid}_{originname}", + settings: { + chunk_size: 25 << 20, + pre_allocate: true, + }, + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + +
+ ); +}; + +export default LocalWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/OBS/ObsWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/OBS/ObsWizard.tsx new file mode 100644 index 0000000..c1d5ea3 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/OBS/ObsWizard.tsx @@ -0,0 +1,179 @@ +import { Button, Collapse, FormControl, Link, Stack } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { createStoragePolicyCors } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; +import BucketCorsTable from "../../EditStoragePolicy/BucketCorsTable"; + +const ObsWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const [corsAdded, setCorsAdded] = useState(false); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.obs, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + thumb_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "tiff"], + media_meta_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "tiff"], + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const hamdleCreateCors = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + setLoading(true); + dispatch(createStoragePolicyCors({ policy })) + .then(() => { + enqueueSnackbar(t("policy.corsPolicyAdded"), { variant: "success", action: DefaultCloseAction }); + setCorsAdded(true); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + , + , + , + , + ]} + /> + + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, server: e.target.value })} + variant={"outlined"} + /> + + , ]} /> + {t("policy.obsEndpointCnameHint")} + + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + , + , + , + ]} + /> + + + + + + + {t("policy.ossCORSDes")} + + + + setCorsAdded(true)}> + {t("policy.addedManually")} + + + + + + + + + ); +}; + +export default ObsWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/OSS/OssWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/OSS/OssWizard.tsx new file mode 100644 index 0000000..d41891b --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/OSS/OssWizard.tsx @@ -0,0 +1,168 @@ +import { Button, Collapse, FormControl, Link, Stack } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { createStoragePolicyCors } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; +import BucketCorsTable from "../../EditStoragePolicy/BucketCorsTable"; +const OssWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const [corsAdded, setCorsAdded] = useState(false); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.oss, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + thumb_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "heic", "tiff", "avif"], + media_meta_exts: ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "heic", "heif"], + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const hamdleCreateCors = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + setLoading(true); + dispatch(createStoragePolicyCors({ policy })) + .then(() => { + enqueueSnackbar(t("policy.corsPolicyAdded"), { variant: "success", action: DefaultCloseAction }); + setCorsAdded(true); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + , , ]} + /> + + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, server: e.target.value })} + /> + + , , ]} /> + {t("policy.ossEndpointDesInternalHint")} + + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + , + , + , + ]} + /> + + + + + + + {t("policy.ossCORSDes")} + + + + setCorsAdded(true)}> + {t("policy.addedManually")} + + + + + + + + + ); +}; + +export default OssWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/OneDrive/GraphEndpointSelection.tsx b/src/component/Admin/StoragePolicy/Wizards/OneDrive/GraphEndpointSelection.tsx new file mode 100644 index 0000000..f417cea --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/OneDrive/GraphEndpointSelection.tsx @@ -0,0 +1,61 @@ +import { Box, OutlinedSelectProps, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { DenseSelect } from "../../../../Common/StyledComponents"; +import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu"; + +export interface GraphEndpointSelectionProps extends OutlinedSelectProps { + value: string; + onChange: (value: string) => void; +} + +interface GraphEndpoint { + name: string; + endpoint: string; +} + +const graphEndpoints: GraphEndpoint[] = [ + { name: "policy.multiTenant", endpoint: "https://graph.microsoft.com/v1.0" }, + { name: "policy.gallatin", endpoint: "https://microsoftgraph.chinacloudapi.cn/v1.0" }, +]; + +const GraphEndpointSelection = ({ value, onChange, ...rest }: GraphEndpointSelectionProps) => { + const { t } = useTranslation("dashboard"); + + return ( + onChange(e.target.value as string)} + MenuProps={{ + PaperProps: { sx: { maxWidth: 230 } }, + MenuListProps: { + sx: { + "& .MuiMenuItem-root": { + whiteSpace: "normal", + }, + }, + }, + }} + {...rest} + > + {graphEndpoints.map((endpoint) => ( + + + + {t(endpoint.name)} + + + {endpoint.endpoint} + + + + ))} + + ); +}; + +export default GraphEndpointSelection; diff --git a/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveAuthDialog.tsx b/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveAuthDialog.tsx new file mode 100644 index 0000000..94670f6 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveAuthDialog.tsx @@ -0,0 +1,104 @@ +import { Alert, Box, Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getPolicyOauthRedirectUrl } from "../../../../../api/api"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import DraggableDialog from "../../../../Dialogs/DraggableDialog"; +import { Code } from "../../../Common/Code"; +import { NoMarginHelperText } from "../../../Settings/Settings"; + +interface OneDriveAuthDialogProps { + open: boolean; + onClose: () => void; + onConfirm: (appId: string, appSecret: string) => void; + initialAppId?: string; + initialAppSecret?: string; +} + +const OneDriveAuthDialog = ({ + open, + onClose, + onConfirm, + initialAppId = "", + initialAppSecret = "", +}: OneDriveAuthDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const [redirectUrl, setRedirectUrl] = useState(""); + const [appId, setAppId] = useState(initialAppId); + const [appSecret, setAppSecret] = useState(initialAppSecret); + const isHttps = window.location.protocol === "https:"; + + useEffect(() => { + if (open) { + setLoading(true); + dispatch(getPolicyOauthRedirectUrl()).then((res) => { + setRedirectUrl(res); + setLoading(false); + }); + } + }, [open, dispatch]); + + const handleConfirm = () => { + onConfirm(appId, appSecret); + onClose(); + }; + + return ( + + + + + {t("policy.authorizeOneDriveDes")} + + + + + {t("policy.redirectUrl")} + + + {redirectUrl} + + + {t("policy.redirectUrlDes")} + + + + + + {t("policy.aadAppID")} + + setAppId(e.target.value)} /> + + , ]} /> + + + + + + {t("policy.aadAppSecret")} + + setAppSecret(e.target.value)} /> + + , , ]} /> + + + {!isHttps && {t("policy.httpsRequired")}} + + + + + + + + ); +}; + +export default OneDriveAuthDialog; diff --git a/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveWizard.tsx new file mode 100644 index 0000000..73963cd --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/OneDrive/OneDriveWizard.tsx @@ -0,0 +1,138 @@ +import { Box, Button, Link, Stack, Typography } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getPolicyOauthRedirectUrl } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import GraphEndpointSelection from "./GraphEndpointSelection"; + +const wwGraph = "https://graph.microsoft.com/v1.0"; + +const OneDriveWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [redirectUrl, setRedirectUrl] = useState("Loading..."); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.onedrive, + server: wwGraph, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 50 << 20, + thumb_support_all_exts: true, + media_meta_generator_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + useEffect(() => { + dispatch(getPolicyOauthRedirectUrl()).then((res) => { + setRedirectUrl(res); + }); + }, []); + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, server: value })} + /> + {t("policy.aadAccountCloudDes")} + + + + , + , + ]} + /> + + + , , , , , , ]} + /> + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + , ]} /> + + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + , , ]} /> + + + + + {t("policy.grantAccessLater")} + + + + + + + + ); +}; + +export default OneDriveWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/Qiniu/QiniuWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/Qiniu/QiniuWizard.tsx new file mode 100644 index 0000000..3648c20 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/Qiniu/QiniuWizard.tsx @@ -0,0 +1,146 @@ +import { Button, FormControl, Link, Stack } from "@mui/material"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; +const QiniuWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.qiniu, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + thumb_exts: ["psd", "jpeg", "png", "gif", "webp", "tiff", "bmp", "avif", "heic"], + thumb_max_size: 20 << 20, + media_meta_exts: [ + "avi", + "mp4", + "mkv", + "mov", + "webm", + "opus", + "flv", + "hls", + "ts", + "dash", + "mp3", + "aac", + "flac", + "wav", + "amr", + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "webp", + "tiff", + "heic", + "heif", + ], + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + custom_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + ]} + /> + + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, settings: { ...policy.settings, proxy_server: e.target.value } })} + variant={"outlined"} + /> + {t("policy.bucketDomainDes")} + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + {t("policy.qiniuCredentialDes")} + + + + +
+ ); +}; + +export default QiniuWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/Remote/RemoteWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/Remote/RemoteWizard.tsx new file mode 100644 index 0000000..4e70c63 --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/Remote/RemoteWizard.tsx @@ -0,0 +1,130 @@ +import { Button, Link, Stack } from "@mui/material"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import NodeSelectionInput from "../../../Common/NodeSelectionInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +const RemoteWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.remote, + dir_name_rule: "data/uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + pre_allocate: true, + thumb_exts: ["jpg", "png", "gif", "jpeg", "mp3", "m4a", "ogg", "flag"], + thumb_generator_proxy: true, + media_meta_exts: [ + "mp3", + "m4a", + "ogg", + "flac", + "jpg", + "jpeg", + "png", + "heic", + "heif", + "tiff", + "avif", + "3fr", + "ari", + "arw", + "bay", + "braw", + "crw", + "cr2", + "cr3", + "cap", + "data", + "dcs", + "dcr", + "dng", + "drf", + "eip", + "erf", + "fff", + "gpr", + "iiq", + "k25", + "kdc", + "mdc", + "mef", + "mos", + "mrw", + "nef", + "nrw", + "obm", + "orf", + "pef", + "ptx", + "pxn", + "r3d", + "raf", + "raw", + "rwl", + "rw2", + "rwz", + "sr2", + "srf", + "srw", + "tif", + "x3f", + ], + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, node_id: value })} + /> + + ]} + /> + + + + +
+ ); +}; + +export default RemoteWizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/S3/S3Wizard.tsx b/src/component/Admin/StoragePolicy/Wizards/S3/S3Wizard.tsx new file mode 100644 index 0000000..391ba7d --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/S3/S3Wizard.tsx @@ -0,0 +1,187 @@ +import { Button, Checkbox, Collapse, FormControl, FormControlLabel, Stack } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { createStoragePolicyCors } from "../../../../../api/api"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, SecondaryButton } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; +import BucketCorsTable from "../../EditStoragePolicy/BucketCorsTable"; + +const S3Wizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const [corsAdded, setCorsAdded] = useState(false); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.s3, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + chunk_size: 25 << 20, + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const hamdleCreateCors = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + setLoading(true); + dispatch(createStoragePolicyCors({ policy })) + .then(() => { + enqueueSnackbar(t("policy.corsPolicyAdded"), { variant: "success", action: DefaultCloseAction }); + setCorsAdded(true); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + + + setPolicy({ ...policy, is_private: value })} + /> + {t("policy.bucketTypeDes")} + + + + setPolicy({ ...policy, server: e.target.value })} + variant={"outlined"} + /> + + ]} /> + + + setPolicy({ + ...policy, + settings: { ...policy.settings, s3_path_style: e.target.checked ? true : undefined }, + }) + } + /> + } + label={t("policy.usePathEndpoint")} + /> + + ]} /> + + + + setPolicy({ ...policy, settings: { ...policy.settings, region: e.target.value } })} + /> + + ]} /> + + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + + + + + {t("policy.ossCORSDes")} + + + + setCorsAdded(true)}> + {t("policy.addedManually")} + + + + + + + +
+ ); +}; + +export default S3Wizard; diff --git a/src/component/Admin/StoragePolicy/Wizards/Upyun/UpyunWizard.tsx b/src/component/Admin/StoragePolicy/Wizards/Upyun/UpyunWizard.tsx new file mode 100644 index 0000000..bfd6f8a --- /dev/null +++ b/src/component/Admin/StoragePolicy/Wizards/Upyun/UpyunWizard.tsx @@ -0,0 +1,137 @@ +import { Button, Collapse, FormControl, Link, Stack } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { StoragePolicy } from "../../../../../api/dashboard"; +import { PolicyType } from "../../../../../api/explorer"; +import { useAppDispatch } from "../../../../../redux/hooks"; +import { DenseFilledTextField } from "../../../../Common/StyledComponents"; +import SettingForm from "../../../../Pages/Setting/SettingForm"; +import { Code } from "../../../Common/Code"; +import { EndpointInput } from "../../../Common/EndpointInput"; +import { NoMarginHelperText } from "../../../Settings/Settings"; +import { AddWizardProps } from "../../AddWizardDialog"; +import BucketACLInput from "../../EditStoragePolicy/BucketACLInput"; + +const UpyunWizard = ({ onSubmit }: AddWizardProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const [corsAdded, setCorsAdded] = useState(false); + const formRef = useRef(null); + const [policy, setPolicy] = useState({ + id: 0, + node_id: 0, + name: "", + type: PolicyType.upyun, + is_private: true, + dir_name_rule: "uploads/{uid}/{path}", + settings: { + thumb_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"], + media_meta_exts: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"], + media_meta_generator_proxy: true, + thumb_generator_proxy: true, + custom_proxy: true, + }, + file_name_rule: "{uuid}_{originname}", + edges: {}, + }); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + onSubmit(policy); + }; + + return ( +
+ + + setPolicy({ ...policy, name: e.target.value })} + /> + {t("policy.policyName")} + + + setPolicy({ ...policy, bucket_name: e.target.value })} + /> + + ]} + /> + + + + + setPolicy({ ...policy, is_private: value })} + /> + + , ]} /> + + + + + + setPolicy({ ...policy, settings: { ...policy.settings, token: e.target.value } })} + /> + {t("policy.upyunTokenSecretDes")} + + + + setPolicy({ ...policy, settings: { ...policy.settings, proxy_server: e.target.value } })} + variant={"outlined"} + /> + {t("policy.bucketCDNDes")} + + + + setPolicy({ ...policy, access_key: e.target.value })} + /> + setPolicy({ ...policy, secret_key: e.target.value })} + /> + + + + + + ); +}; + +export default UpyunWizard; diff --git a/src/component/Admin/Task/Aria2Helper.js b/src/component/Admin/Task/Aria2Helper.js deleted file mode 100644 index 6c2d0c9..0000000 --- a/src/component/Admin/Task/Aria2Helper.js +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Link, -} from "@material-ui/core"; -import { Link as RouterLink } from "react-router-dom"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({})); - -export default function Aria2Helper(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "task" }); - const { t: tCommon } = useTranslation("common"); - const classes = useStyles(); - - return ( - - - {t("howToConfigAria2")} - - - - {t("aria2Des")} -
    -
  • - , - ]} - /> -
  • -
  • - , - ]} - /> -
  • -
- , - ]} - /> -
-
- - - -
- ); -} diff --git a/src/component/Admin/Task/Download.js b/src/component/Admin/Task/Download.js deleted file mode 100644 index bb2e5c6..0000000 --- a/src/component/Admin/Task/Download.js +++ /dev/null @@ -1,444 +0,0 @@ -import { lighten } from "@material-ui/core"; -import Button from "@material-ui/core/Button"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import Link from "@material-ui/core/Link"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Toolbar from "@material-ui/core/Toolbar"; -import Tooltip from "@material-ui/core/Tooltip"; -import Typography from "@material-ui/core/Typography"; -import { Delete } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { sizeToString } from "../../../utils"; -import ShareFilter from "../Dialogs/ShareFilter"; -import { formatLocalTime } from "../../../utils/datetime"; -import Aria2Helper from "./Aria2Helper"; -import HelpIcon from "@material-ui/icons/Help"; -import { Link as RouterLink } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, - highlight: - theme.palette.type === "light" - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, -})); - -export default function Download() { - const { t } = useTranslation("dashboard", { keyPrefix: "task" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [downloads, setDownloads] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [filter, setFilter] = useState({}); - const [users, setUsers] = useState({}); - const [search, setSearch] = useState({}); - const [orderBy, setOrderBy] = useState(["id", "desc"]); - const [filterDialog, setFilterDialog] = useState(false); - const [selected, setSelected] = useState([]); - const [loading, setLoading] = useState(false); - const [helperOpen, setHelperOpen] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/download/list", { - page: page, - page_size: pageSize, - order_by: orderBy.join(" "), - conditions: filter, - searches: search, - }) - .then((response) => { - setUsers(response.data.users); - setDownloads(response.data.items); - setTotal(response.data.total); - setSelected([]); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, orderBy, filter, search]); - - const deletePolicy = (id) => { - setLoading(true); - API.post("/admin/download/delete", { id: [id] }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("taskDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteBatch = () => { - setLoading(true); - API.post("/admin/download/delete", { id: selected }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("taskDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = downloads.map((n) => n.ID); - setSelected(newSelecteds); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - selected.slice(0, selectedIndex), - selected.slice(selectedIndex + 1) - ); - } - - setSelected(newSelected); - }; - - const isSelected = (id) => selected.indexOf(id) !== -1; - - return ( -
- setHelperOpen(false)} - /> - setFilterDialog(false)} - setSearch={setSearch} - setFilter={setFilter} - /> -
- -
- -
-
- - - {selected.length > 0 && ( - - - {tDashboard("user.selectedObjects", { - num: selected.length, - })} - - - - - - - - )} - - - - - - 0 && - selected.length < downloads.length - } - checked={ - downloads.length > 0 && - selected.length === downloads.length - } - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all desserts", - }} - /> - - - - setOrderBy([ - "id", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - # - {orderBy[0] === "id" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {t("srcURL")} - - - {tDashboard("user.status")} - - - {t("node")} - - - - setOrderBy([ - "total_size", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {tDashboard("file.size")} - {orderBy[0] === "total_size" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {t("createdBy")} - - - {tDashboard("file.createdAt")} - - - {tDashboard("policy.actions")} - - - - - {downloads.map((row) => ( - - - - handleClick(event, row.ID) - } - checked={isSelected(row.ID)} - /> - - {row.ID} - - {row.Source} - - - {row.Status === 0 && t("ready")} - {row.Status === 1 && t("downloading")} - {row.Status === 2 && t("paused")} - {row.Status === 3 && t("error")} - {row.Status === 4 && t("finished")} - {row.Status === 5 && t("canceled")} - {row.Status === 6 && t("unknown")} - {row.Status === 7 && t("seeding")} - - - {row.NodeID <= 1 && ( - - {tDashboard("node.master")} - - )} - {row.NodeID > 1 && ( - - {tDashboard("node.slave")}# - {row.NodeID} - - )} - - - {sizeToString(row.TotalSize)} - - - - {users[row.UserID] - ? users[row.UserID].Nick - : tDashboard( - "file.unknownUploader" - )} - - - - {formatLocalTime(row.CreatedAt)} - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/Task/Task.js b/src/component/Admin/Task/Task.js deleted file mode 100644 index fa71e2c..0000000 --- a/src/component/Admin/Task/Task.js +++ /dev/null @@ -1,389 +0,0 @@ -import { lighten } from "@material-ui/core"; -import Button from "@material-ui/core/Button"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import Link from "@material-ui/core/Link"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Toolbar from "@material-ui/core/Toolbar"; -import Tooltip from "@material-ui/core/Tooltip"; -import Typography from "@material-ui/core/Typography"; -import { Delete } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../../redux/explorer"; -import { getTaskProgress, getTaskStatus, getTaskType } from "../../../config"; -import API from "../../../middleware/Api"; -import ShareFilter from "../Dialogs/ShareFilter"; -import { formatLocalTime } from "../../../utils/datetime"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, - highlight: - theme.palette.type === "light" - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, -})); - -export default function Task() { - const { t } = useTranslation("dashboard", { keyPrefix: "task" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [tasks, setTasks] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [filter, setFilter] = useState({}); - const [users, setUsers] = useState({}); - const [search, setSearch] = useState({}); - const [orderBy, setOrderBy] = useState(["id", "desc"]); - const [filterDialog, setFilterDialog] = useState(false); - const [selected, setSelected] = useState([]); - const [loading, setLoading] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/task/list", { - page: page, - page_size: pageSize, - order_by: orderBy.join(" "), - conditions: filter, - searches: search, - }) - .then((response) => { - setUsers(response.data.users); - setTasks(response.data.items); - setTotal(response.data.total); - setSelected([]); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, orderBy, filter, search]); - - const deletePolicy = (id) => { - setLoading(true); - API.post("/admin/task/delete", { id: [id] }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("taskDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteBatch = () => { - setLoading(true); - API.post("/admin/task/delete", { id: selected }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("taskDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = tasks.map((n) => n.ID); - setSelected(newSelecteds); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - selected.slice(0, selectedIndex), - selected.slice(selectedIndex + 1) - ); - } - - setSelected(newSelected); - }; - - const getError = (error) => { - if (error === "") { - return "-"; - } - try { - const res = JSON.parse(error); - return res.msg; - } catch (e) { - return t("unknown"); - } - }; - - const isSelected = (id) => selected.indexOf(id) !== -1; - - return ( -
- setFilterDialog(false)} - setSearch={setSearch} - setFilter={setFilter} - /> -
-
- -
-
- - - {selected.length > 0 && ( - - - {tDashboard("user.selectedObjects", { - num: selected.length, - })} - - - - - - - - )} - - - - - - 0 && - selected.length < tasks.length - } - checked={ - tasks.length > 0 && - selected.length === tasks.length - } - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all desserts", - }} - /> - - - - setOrderBy([ - "id", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - # - {orderBy[0] === "id" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {tDashboard("policy.type")} - - - {tDashboard("user.status")} - - - {t("lastProgress")} - - - {t("errorMsg")} - - - {t("createdBy")} - - - {tDashboard("file.createdAt")} - - - {tDashboard("policy.actions")} - - - - - {tasks.map((row) => ( - - - - handleClick(event, row.ID) - } - checked={isSelected(row.ID)} - /> - - {row.ID} - - {getTaskType(row.Type)} - - - {getTaskStatus(row.Status)} - - - {getTaskProgress( - row.Type, - row.Progress - )} - - - {getError(row.Error)} - - - - {users[row.UserID] - ? users[row.UserID].Nick - : t("unknown")} - - - - {formatLocalTime(row.CreatedAt)} - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/Task/TaskContent.tsx b/src/component/Admin/Task/TaskContent.tsx new file mode 100644 index 0000000..48e29bb --- /dev/null +++ b/src/component/Admin/Task/TaskContent.tsx @@ -0,0 +1,97 @@ +import { Link, Typography } from "@mui/material"; +import { memo, useCallback, useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Task } from "../../../api/dashboard"; +import { TaskSummary, TaskType } from "../../../api/workflow"; +import CrUri, { Filesystem } from "../../../util/uri"; +import TaskSummaryTitle from "../../Pages/Tasks/TaskSummaryTitle"; + +const userTaskTypes: string[] = [ + TaskType.relocate, + TaskType.create_archive, + TaskType.extract_archive, + TaskType.remote_download, +]; + +export interface TaskContentProps { + task: Task; + openEntity?: (entityID: number) => void; +} + +const processUrl = (url: string, userHashId: string) => { + const crUrl = new CrUri(url); + if (crUrl.fs() == Filesystem.my && !crUrl.id()) { + crUrl.setUsername(userHashId); + } + return crUrl.toString(); +}; + +export const processTaskContent = (summary: TaskSummary, userHashId: string): TaskSummary => { + if (summary.props?.src) { + summary.props.src = processUrl(summary.props.src, userHashId); + } + if (summary.props?.dst) { + summary.props.dst = processUrl(summary.props.dst, userHashId); + } + if (summary.props?.src_multiple) { + summary.props.src_multiple = summary.props.src_multiple.map((url) => processUrl(url, userHashId)); + } + + return summary; +}; + +export const TaskContent = memo(({ task, openEntity }: TaskContentProps) => { + const { t } = useTranslation("dashboard"); + + if (userTaskTypes.includes(task.type ?? "")) { + const processedSummary = processTaskContent({ ...task.summary } as TaskSummary, task?.user_hash_id ?? ""); + return ( + + + + ); + } + + const entityLinkClick = useCallback( + (entityID: number) => (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (openEntity) { + openEntity(entityID); + } + }, + [openEntity], + ); + + const content = useMemo(() => { + var privateState: any = {}; + try { + privateState = JSON.parse(task.private_state ?? "{}"); + } catch (error) { + console.error(error); + } + switch (task.type) { + case TaskType.upload_sentinel_check: + return t("task.uploadSentinelCheck", { uploadSessionID: privateState?.session?.Props?.UploadSessionID }); + case TaskType.media_metadata: + return ( + ]} + /> + ); + case TaskType.entity_recycle_routine: + return t("task.entityRecycleRoutine"); + case TaskType.explicit_entity_recycle: + return t("task.explicitEntityRecycle", { + blobs: privateState?.entity_ids?.map((id: number) => `#${id}`).join(", "), + }); + default: + return ""; + } + }, [task, t]); + + return {content}; +}); diff --git a/src/component/Admin/Task/TaskDialog/BlobErrors.tsx b/src/component/Admin/Task/TaskDialog/BlobErrors.tsx new file mode 100644 index 0000000..87932a0 --- /dev/null +++ b/src/component/Admin/Task/TaskDialog/BlobErrors.tsx @@ -0,0 +1,63 @@ +import { Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { StyledTableContainerPaper } from "../../../Common/StyledComponents"; +import EntityDialog from "../../Entity/EntityDialog/EntityDialog"; + +export interface BlobErrorsProps { + privateState: any; +} +export const BlobErrors = ({ privateState }: BlobErrorsProps) => { + const { t } = useTranslation("dashboard"); + const [openEntityDialogOpen, setOpenEntityDialogOpen] = useState(false); + const [openTask, setOpenTask] = useState(undefined); + + const openEntityDialog = (entityID: number) => (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setOpenEntityDialogOpen(true); + setOpenTask(entityID); + }; + + return ( + + setOpenEntityDialogOpen(false)} entityID={openTask} /> + + + + {t("task.blobID")} + {t("task.retryIndex")} + {t("common:error")} + + + + {privateState?.errors?.map((error: any, retryIndex: number) => [ + ...error.map((e: any, index: number) => [ + + + + + #{e.id} + + + + + + {retryIndex} + + + + + {e.error} + + + , + ]), + ])} + +
+
+ ); +}; + +export default BlobErrors; diff --git a/src/component/Admin/Task/TaskDialog/TaskDialog.tsx b/src/component/Admin/Task/TaskDialog/TaskDialog.tsx new file mode 100644 index 0000000..96c8c81 --- /dev/null +++ b/src/component/Admin/Task/TaskDialog/TaskDialog.tsx @@ -0,0 +1,84 @@ +import { Box, DialogContent } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getTaskDetail } from "../../../../api/api.ts"; +import { Task } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import TaskForm from "./TaskForm.tsx"; + +export interface TaskDialogProps { + open: boolean; + onClose: () => void; + taskID?: number; +} + +const TaskDialog = ({ open, onClose, taskID }: TaskDialogProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ edges: {}, id: 0 }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!taskID || !open) { + return; + } + setLoading(true); + dispatch(getTaskDetail(taskID)) + .then((res) => { + setValues(res); + }) + .catch(() => { + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [open]); + + return ( + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && } + + + + + + + ); +}; + +export default TaskDialog; diff --git a/src/component/Admin/Task/TaskDialog/TaskForm.tsx b/src/component/Admin/Task/TaskDialog/TaskForm.tsx new file mode 100644 index 0000000..dc86b61 --- /dev/null +++ b/src/component/Admin/Task/TaskDialog/TaskForm.tsx @@ -0,0 +1,282 @@ +import { + Box, + Grid2 as Grid, + Link, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import { lazy, Suspense, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { Task } from "../../../../api/dashboard"; +import { FileType } from "../../../../api/explorer"; +import { TaskStatus, TaskSummary, TaskType } from "../../../../api/workflow"; +import { formatDuration } from "../../../../util/datetime"; +import FacebookCircularProgress from "../../../Common/CircularProgress"; +import { NoWrapTypography, StyledTableContainerPaper } from "../../../Common/StyledComponents"; +import TimeBadge from "../../../Common/TimeBadge"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import FileBadge from "../../../FileManager/FileBadge"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import DownloadFileList from "../../../Pages/Tasks/DownloadFileList"; +import { getTaskStatusText } from "../../../Pages/Tasks/TaskProps"; +import UserDialog from "../../User/UserDialog/UserDialog"; +import { processTaskContent } from "../TaskContent"; +import BlobErrors from "./BlobErrors"; +dayjs.extend(duration); + +const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor")); + +const TaskForm = ({ values }: { values: Task }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation("dashboard"); + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(0); + + const userClicked = (e: React.MouseEvent) => { + e.preventDefault(); + setUserDialogOpen(true); + setUserDialogID(values?.edges?.user?.id ?? 0); + }; + + const processedSummary = useMemo(() => { + return processTaskContent({ ...values.summary } as TaskSummary, values?.user_hash_id ?? ""); + }, [values]); + + const privateState = useMemo((): any => { + let res: any = {}; + if (values.private_state) { + try { + res = JSON.parse(values.private_state); + } catch (error) { + console.error(error); + } + } + return res; + }, [values]); + + return ( + <> + setUserDialogOpen(false)} userID={userDialogID} /> + + + + + {values.id} + + + + + + {t(`task.${values.type}`)} + + + + + + {getTaskStatusText(values?.status ?? TaskStatus.queued, t)} + + + + + + {values?.node?.name ? ( + + {values?.node?.name} + + ) : ( + "-" + )} + + + + + + {values?.edges?.user ? ( + + + + + {values?.edges?.user?.nick} + + + + ) : ( + "-" + )} + + + + + + + + + + + + + + + + + {values?.correlation_id ?? "-"} + + + + + + {formatDuration(dayjs.duration((values?.public_state?.executed_duration ?? 0) / 1e6))} + + + + + + {values?.public_state?.retry_count ?? "-"} + + + + {processedSummary?.props?.src && ( + + + + )} + + {processedSummary?.props?.src_multiple && ( + + + {processedSummary?.props.src_multiple.map((src, index) => ( + + ))} + + + )} + + {processedSummary?.props?.dst && ( + + + + )} + + {values?.public_state?.error && ( + + + {values?.public_state?.error} + + + )} + + {processedSummary?.props?.download && ( + + + + )} + + {values?.public_state?.error_history && ( + + + + + + # + {t("common:error")} + + + + {values?.public_state?.error_history.map((error, index) => ( + + + {index + 1} + + {error} + + ))} + +
+
+
+ )} + + {values.type == TaskType.entity_recycle_routine && privateState?.errors && ( + + + + )} + + + }> + + + +
+
+ + ); +}; + +export default TaskForm; diff --git a/src/component/Admin/Task/TaskFilterPopover.tsx b/src/component/Admin/Task/TaskFilterPopover.tsx new file mode 100644 index 0000000..ac4c30d --- /dev/null +++ b/src/component/Admin/Task/TaskFilterPopover.tsx @@ -0,0 +1,207 @@ +import { Box, Button, ListItemText, Popover, PopoverProps, Stack } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TaskStatus, TaskType } from "../../../api/workflow"; +import { DenseFilledTextField, DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import { getTaskStatusText } from "../../Pages/Tasks/TaskProps"; + +export interface TaskFilterPopoverProps extends PopoverProps { + status: string; + setStatus: (status: string) => void; + user: string; + setUser: (user: string) => void; + correlationID: string; + setCorrelationID: (correlationID: string) => void; + type: string; + setType: (type: string) => void; + clearFilters: () => void; +} + +const TaskFilterPopover = ({ + status, + setStatus, + user, + setUser, + correlationID, + setCorrelationID, + type, + setType, + clearFilters, + onClose, + open, + ...rest +}: TaskFilterPopoverProps) => { + const { t } = useTranslation("dashboard"); + + // Create local state to track changes before applying + const [localStatus, setLocalStatus] = useState(status); + const [localUser, setLocalUser] = useState(user); + const [localCorrelationID, setLocalCorrelationID] = useState(correlationID); + const [localType, setLocalType] = useState(type); + + // Initialize local state when popup opens + useEffect(() => { + if (open) { + setLocalStatus(status); + setLocalUser(user); + setLocalCorrelationID(correlationID); + setLocalType(type); + } + }, [open]); + + // Apply filters and close popover + const handleApplyFilters = () => { + setStatus(localStatus); + setUser(localUser); + setCorrelationID(localCorrelationID); + setType(localType); + onClose?.({}, "backdropClick"); + }; + + // Reset filters and close popover + const handleResetFilters = () => { + setLocalStatus(""); + setLocalUser(""); + setLocalCorrelationID(""); + setLocalType(""); + clearFilters(); + onClose?.({}, "backdropClick"); + }; + + return ( + + + + setLocalUser(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalCorrelationID(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalType(e.target.value === " " ? "" : (e.target.value as string))} + > + {[ + TaskType.create_archive, + TaskType.extract_archive, + TaskType.remote_download, + TaskType.media_metadata, + TaskType.entity_recycle_routine, + TaskType.explicit_entity_recycle, + TaskType.upload_sentinel_check, + ].map((type) => ( + + + + ))} + + {t("user.all")}} + slotProps={{ + primary: { + variant: "body2", + }, + }} + /> + + + + + + setLocalStatus(e.target.value === " " ? "" : (e.target.value as string))} + > + {[ + TaskStatus.queued, + TaskStatus.processing, + TaskStatus.suspending, + TaskStatus.error, + TaskStatus.completed, + ].map((status) => ( + + + + ))} + + {t("user.all")}} + slotProps={{ + primary: { + variant: "body2", + }, + }} + /> + + + + + + + + + + ); +}; + +export default TaskFilterPopover; diff --git a/src/component/Admin/Task/TaskList.tsx b/src/component/Admin/Task/TaskList.tsx new file mode 100644 index 0000000..bb7afcd --- /dev/null +++ b/src/component/Admin/Task/TaskList.tsx @@ -0,0 +1,322 @@ +import { Delete } from "@mui/icons-material"; +import { + Badge, + Box, + Button, + Checkbox, + Container, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { batchDeleteTasks, getTaskList } from "../../../api/api"; +import { AdminListService, Task } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import ArrowSync from "../../Icons/ArrowSync"; +import Filter from "../../Icons/Filter"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import EntityDialog from "../Entity/EntityDialog/EntityDialog"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import UserDialog from "../User/UserDialog/UserDialog"; +import TaskDialog from "./TaskDialog/TaskDialog"; +import TaskFilterPopover from "./TaskFilterPopover"; +import TaskRow from "./TaskRow"; + +export const UserQuery = "user"; +export const TypeQuery = "type"; +export const StatusQuery = "status"; +export const CorrelationIDQuery = "correlation_id"; + +const TaskList = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [tasks, setTasks] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [user, setUser] = useQueryState(UserQuery, { defaultValue: "" }); + const [type, setType] = useQueryState(TypeQuery, { defaultValue: "" }); + const [status, setStatus] = useQueryState(StatusQuery, { defaultValue: "" }); + const [correlationID, setCorrelationID] = useQueryState(CorrelationIDQuery, { defaultValue: "" }); + + const [count, setCount] = useState(0); + const [selected, setSelected] = useState([]); + const filterPopupState = usePopupState({ + variant: "popover", + popupId: "taskFilterPopover", + }); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(undefined); + const [openEntity, setOpenEntity] = useState(undefined); + const [openEntityDialogOpen, setOpenEntityDialogOpen] = useState(false); + const [openTask, setOpenTask] = useState(undefined); + const [openTaskDialogOpen, setOpenTaskDialogOpen] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 10; + + const clearFilters = useCallback(() => { + setUser(""); + setType(""); + setStatus(""); + setCorrelationID(""); + }, [setUser, setType, setStatus, setCorrelationID]); + + useEffect(() => { + fetchTasks(); + }, [page, pageSize, orderBy, orderDirection, user, type, status, correlationID]); + + const fetchTasks = () => { + setLoading(true); + setSelected([]); + + const params: AdminListService = { + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: { + task_status: status, + task_user_id: user, + task_type: type, + task_correlation_id: correlationID, + }, + }; + + dispatch(getTaskList(params)) + .then((res) => { + setTasks(res.tasks); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleDelete = () => { + setDeleteLoading(true); + dispatch(confirmOperation(t("task.confirmBatchDelete", { num: selected.length }))) + .then(() => { + dispatch(batchDeleteTasks({ ids: Array.from(selected) })) + .then(() => { + fetchTasks(); + }) + .finally(() => { + setDeleteLoading(false); + }); + setDeleteLoading(false); + }) + .finally(() => { + setDeleteLoading(false); + }); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = tasks.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleSelect = useCallback( + (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); + } + setSelected(newSelected); + }, + [selected], + ); + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + const hasActiveFilters = useMemo(() => { + return !!(status || user || type); + }, [status, user, type]); + + const handleUserDialogOpen = (id: number) => { + setUserDialogID(id); + setUserDialogOpen(true); + }; + + const handleOpenEntity = (entityID: number) => { + setOpenEntity(entityID); + setOpenEntityDialogOpen(true); + }; + + const handleOpenTask = (taskID: number) => { + setOpenTask(taskID); + setOpenTaskDialogOpen(true); + }; + + return ( + + setUserDialogOpen(false)} userID={userDialogID} /> + setOpenEntityDialogOpen(false)} entityID={openEntity} /> + setOpenTaskDialogOpen(false)} taskID={openTask} /> + + + + + + }> + {t("node.refresh")} + + + + } variant="contained" {...bindTrigger(filterPopupState)}> + {t("user.filter")} + + + + {selected.length > 0 && !isMobile && ( + <> + + + + )} + + {isMobile && selected.length > 0 && ( + + + + )} + + + + + + 0 && selected.length < tasks.length} + checked={tasks.length > 0 && selected.length === tasks.length} + onChange={handleSelectAllClick} + /> + + + + {t("group.#")} + + + {t("task.content")} + {t("task.status")} + {t("file.creator")} + {t("task.node")} + + + {t("file.createdAt")} + + + + + + + {!loading && + tasks.map((task) => ( + + ))} + {loading && + tasks.length > 0 && + tasks.slice(0, 10).map((task) => )} + {loading && + tasks.length === 0 && + Array.from(Array(10)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default TaskList; diff --git a/src/component/Admin/Task/TaskRow.tsx b/src/component/Admin/Task/TaskRow.tsx new file mode 100644 index 0000000..e706365 --- /dev/null +++ b/src/component/Admin/Task/TaskRow.tsx @@ -0,0 +1,192 @@ +import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { batchDeleteTasks } from "../../../api/api"; +import { Task } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { NoWrapTableCell, NoWrapTypography } from "../../Common/StyledComponents"; +import TimeBadge from "../../Common/TimeBadge"; +import UserAvatar from "../../Common/User/UserAvatar"; +import Delete from "../../Icons/Delete"; +import TaskSummaryStatus from "../../Pages/Tasks/TaskSummaryStatus"; +import { TaskContent } from "./TaskContent"; + +export interface TaskRowProps { + task?: Task; + loading?: boolean; + deleting?: boolean; + selected?: boolean; + onDelete?: () => void; + onDetails?: (id: number) => void; + onSelect?: (id: number) => void; + openUserDialog?: (id: number) => void; + openEntity?: (entityID: number) => void; + openTask?: (taskID: number) => void; +} + +const TaskRow = ({ + task, + loading, + deleting, + selected, + onDelete, + onDetails, + onSelect, + openUserDialog, + openEntity, +}: TaskRowProps) => { + const navigate = useNavigate(); + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [deleteLoading, setDeleteLoading] = useState(false); + const [openLoading, setOpenLoading] = useState(false); + const onRowClick = () => { + onDetails?.(task?.id ?? 0); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(confirmOperation(t("task.confirmDelete"))).then(() => { + if (task?.id) { + setDeleteLoading(true); + dispatch(batchDeleteTasks({ ids: [task.id] })) + .then(() => { + onDelete?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + } + }); + }; + + const onSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onSelect?.(task?.id ?? 0); + }; + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const userClicked = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + openUserDialog?.(task?.edges?.user?.id ?? 0); + }; + + return ( + + + + + + {task?.id} + + {task && } + + + + + {task?.edges?.user && ( + + + + + {task?.edges?.user?.nick} + + + + )} + + + + {task?.node?.name ? ( + + {task?.node?.name} + + ) : ( + "-" + )} + + + + + + + + + + + + + + ); +}; + +export default TaskRow; diff --git a/src/component/Admin/User/EditUser.js b/src/component/Admin/User/EditUser.js deleted file mode 100644 index 24fa518..0000000 --- a/src/component/Admin/User/EditUser.js +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import API from "../../../middleware/Api"; -import { useDispatch } from "react-redux"; -import UserForm from "./UserForm"; -import { toggleSnackbar } from "../../../redux/explorer"; - -export default function EditUserPreload() { - const [user, setUser] = useState({}); - - const { id } = useParams(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - setUser({}); - API.get("/admin/user/" + id) - .then((response) => { - // 整型转换 - ["Status", "GroupID"].forEach((v) => { - response.data[v] = response.data[v].toString(); - }); - setUser(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, [id]); - - return
{user.ID !== undefined && }
; -} diff --git a/src/component/Admin/User/NewUserDialog.tsx b/src/component/Admin/User/NewUserDialog.tsx new file mode 100644 index 0000000..4a8020c --- /dev/null +++ b/src/component/Admin/User/NewUserDialog.tsx @@ -0,0 +1,117 @@ +import { DialogContent, Stack } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { upsertUser } from "../../../api/api"; +import { User, UserStatus } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { DenseFilledTextField } from "../../Common/StyledComponents"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import GroupSelectionInput from "../Common/GroupSelectionInput"; + +export interface NewUserDialogProps { + open: boolean; + onClose: () => void; + onCreated: (user: User) => void; +} + +const defaultUser: User = { + edges: {}, + id: 0, + email: "", + nick: "", + password: "", + status: UserStatus.active, + group_users: 2, +}; + +const NewUserDialog = ({ open, onClose, onCreated }: NewUserDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [user, setUser] = useState({ ...defaultUser }); + const formRef = useRef(null); + + useEffect(() => { + if (open) { + setUser({ ...defaultUser }); + } + }, [open]); + + const handleSubmit = () => { + if (!formRef.current?.checkValidity()) { + formRef.current?.reportValidity(); + return; + } + + let newUser = { ...user, nick: user.email.split("@")[0] }; + + setLoading(true); + dispatch(upsertUser({ user: newUser, password: user.password })) + .then((r) => { + onCreated(r); + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + +
+ + + setUser({ ...user, email: e.target.value })} + /> + + + setUser({ ...user, password: e.target.value })} + /> + + + setUser({ ...user, group_users: parseInt(e) })} + fullWidth + /> + + +
+
+
+ ); +}; + +export default NewUserDialog; diff --git a/src/component/Admin/User/User.js b/src/component/Admin/User/User.js deleted file mode 100644 index 9783001..0000000 --- a/src/component/Admin/User/User.js +++ /dev/null @@ -1,541 +0,0 @@ -import { lighten } from "@material-ui/core"; -import Badge from "@material-ui/core/Badge"; -import Button from "@material-ui/core/Button"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import Link from "@material-ui/core/Link"; -import Paper from "@material-ui/core/Paper"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TablePagination from "@material-ui/core/TablePagination"; -import TableRow from "@material-ui/core/TableRow"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import Toolbar from "@material-ui/core/Toolbar"; -import Tooltip from "@material-ui/core/Tooltip"; -import Typography from "@material-ui/core/Typography"; -import { Block, Delete, Edit, FilterList } from "@material-ui/icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { sizeToString } from "../../../utils"; -import UserFilter from "../Dialogs/UserFilter"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - content: { - padding: theme.spacing(2), - }, - container: { - overflowX: "auto", - }, - tableContainer: { - marginTop: 16, - }, - header: { - display: "flex", - justifyContent: "space-between", - }, - headerRight: {}, - highlight: - theme.palette.type === "light" - ? { - color: theme.palette.secondary.main, - backgroundColor: lighten(theme.palette.secondary.light, 0.85), - } - : { - color: theme.palette.text.primary, - backgroundColor: theme.palette.secondary.dark, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, -})); - -export default function Group() { - const { t } = useTranslation("dashboard", { keyPrefix: "user" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [users, setUsers] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [filter, setFilter] = useState({}); - const [search, setSearch] = useState({}); - const [orderBy, setOrderBy] = useState(["id", "desc"]); - const [filterDialog, setFilterDialog] = useState(false); - const [selected, setSelected] = useState([]); - const [loading, setLoading] = useState(false); - - const history = useHistory(); - const theme = useTheme(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = () => { - API.post("/admin/user/list", { - page: page, - page_size: pageSize, - order_by: orderBy.join(" "), - conditions: filter, - searches: search, - }) - .then((response) => { - setUsers(response.data.items); - setTotal(response.data.total); - setSelected([]); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(); - }, [page, pageSize, orderBy, filter, search]); - - const deletePolicy = (id) => { - setLoading(true); - API.post("/admin/user/delete", { id: [id] }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", "用户已删除", "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const deleteBatch = () => { - setLoading(true); - API.post("/admin/user/delete", { id: selected }) - .then(() => { - loadList(); - ToggleSnackbar("top", "right", t("deleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const block = (id) => { - setLoading(true); - API.patch("/admin/user/ban/" + id) - .then((response) => { - setUsers( - users.map((v) => { - if (v.ID === id) { - const newUser = { ...v, Status: response.data }; - return newUser; - } - return v; - }) - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelecteds = users.map((n) => n.ID); - setSelected(newSelecteds); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat( - selected.slice(0, selectedIndex), - selected.slice(selectedIndex + 1) - ); - } - - setSelected(newSelected); - }; - - const isSelected = (id) => selected.indexOf(id) !== -1; - - return ( -
- setFilterDialog(false)} - setSearch={setSearch} - setFilter={setFilter} - /> -
- -
- - setFilterDialog(true)} - > - - - - - - -
-
- - - {selected.length > 0 && ( - - - {t("selectedObjects", { num: selected.length })} - - - - - - - - )} - - - - - - 0 && - selected.length < users.length - } - checked={ - users.length > 0 && - selected.length === users.length - } - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all desserts", - }} - /> - - - - setOrderBy([ - "id", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {tDashboard("node.#")} - {orderBy[0] === "id" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "nick", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("nick")} - {orderBy[0] === "nick" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - setOrderBy([ - "email", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("email")} - {orderBy[0] === "email" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {t("group")} - - - {t("status")} - - - - setOrderBy([ - "storage", - orderBy[1] === "asc" - ? "desc" - : "asc", - ]) - } - > - {t("usedStorage")} - {orderBy[0] === "storage" ? ( - - {orderBy[1] === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - {tDashboard("policy.actions")} - - - - - {users.map((row) => ( - - - - handleClick(event, row.ID) - } - checked={isSelected(row.ID)} - /> - - {row.ID} - {row.Nick} - {row.Email} - - - {row.Group.Name} - - - - {row.Status === 0 && ( - - {t("active")} - - )} - {row.Status === 1 && ( - - {t("notActivated")} - - )} - {row.Status === 2 && ( - - {t("banned")} - - )} - {row.Status === 3 && ( - - {t("bannedBySys")} - - )} - - - {sizeToString(row.Storage)} - - - - - history.push( - "/admin/user/edit/" + - row.ID - ) - } - size={"small"} - > - - - - - block(row.ID)} - size={"small"} - > - - - - - - deletePolicy(row.ID) - } - size={"small"} - > - - - - - - ))} - -
-
- setPage(p + 1)} - onChangeRowsPerPage={(e) => { - setPageSize(e.target.value); - setPage(1); - }} - /> -
-
- ); -} diff --git a/src/component/Admin/User/UserDialog/UserDialog.tsx b/src/component/Admin/User/UserDialog/UserDialog.tsx new file mode 100644 index 0000000..78cb248 --- /dev/null +++ b/src/component/Admin/User/UserDialog/UserDialog.tsx @@ -0,0 +1,174 @@ +import { Box, Button, Collapse, DialogActions, DialogContent } from "@mui/material"; +import * as React from "react"; +import { createContext, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getUserDetail, upsertUser } from "../../../../api/api.ts"; +import { UpsertUserService, User } from "../../../../api/dashboard.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import UserForm from "./UserForm.tsx"; + +export interface UserDialogProps { + open: boolean; + onClose: () => void; + userID?: number; + onUpdated?: (user: User) => void; +} + +export interface UserDialogContextProps { + values: User; + setUser: (f: (p: User) => User) => void; + formRef?: React.RefObject; +} + +const defaultUser: User = { + id: 0, + nick: "", + email: "", + edges: {}, +}; + +export const UserDialogContext = createContext({ + values: { ...defaultUser }, + setUser: () => {}, +}); + +const UserDialog = ({ open, onClose, userID, onUpdated }: UserDialogProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation("dashboard"); + const [values, setValues] = useState({ + ...defaultUser, + }); + const [modifiedValues, setModifiedValues] = useState({ + ...defaultUser, + }); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const formRef = useRef(null); + + const showSaveButton = useMemo(() => { + return JSON.stringify(modifiedValues) !== JSON.stringify(values); + }, [modifiedValues, values]); + + const loadUser = useCallback(() => { + if (!userID) { + return; + } + setLoading(true); + dispatch(getUserDetail(userID)) + .then((res) => { + setValues(res); + setModifiedValues(res); + }) + .catch(() => { + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }, [userID]); + + useEffect(() => { + loadUser(); + }, [userID]); + + const revert = () => { + setModifiedValues(values); + }; + + const submit = () => { + if (formRef.current) { + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + } + + const args: UpsertUserService = { + user: { ...modifiedValues }, + }; + + if (!args.user.two_fa_enabled) { + args.two_fa = "clear"; + } + + if (args.user.password) { + args.password = args.user.password; + } + + setSubmitting(true); + dispatch(upsertUser(args)) + .then((res) => { + setValues(res); + setModifiedValues(res); + onUpdated?.(res); + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + + + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && } + + + + + + + + + + + + + + ); +}; + +export default UserDialog; diff --git a/src/component/Admin/User/UserDialog/UserForm.tsx b/src/component/Admin/User/UserDialog/UserForm.tsx new file mode 100644 index 0000000..dd84baf --- /dev/null +++ b/src/component/Admin/User/UserDialog/UserForm.tsx @@ -0,0 +1,247 @@ +import { OpenInNew } from "@mui/icons-material"; +import { + Box, + Collapse, + Divider, + FormControl, + Grid2 as Grid, + Link, + ListItemText, + SelectChangeEvent, + Stack, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { sendCalibrateUserStorage } from "../../../../api/api"; +import { UserStatus } from "../../../../api/dashboard"; +import { useAppDispatch } from "../../../../redux/hooks"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar"; +import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../Common/StyledComponents"; +import UserAvatar from "../../../Common/User/UserAvatar"; +import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu"; +import Delete from "../../../Icons/Delete"; +import SettingForm from "../../../Pages/Setting/SettingForm"; +import { CapacityBar } from "../../../Pages/Setting/StorageSetting"; +import GroupSelectionInput from "../../Common/GroupSelectionInput"; +import { NoMarginHelperText } from "../../Settings/Settings"; +import { UserDialogContext } from "./UserDialog"; + +const UserForm = ({ reload, setLoading }: { reload: () => void; setLoading: (loading: boolean) => void }) => { + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation("dashboard"); + const { formRef, values, setUser } = useContext(UserDialogContext); + + const removeAvatar = useCallback(() => { + setUser((prev) => ({ ...prev, avatar: undefined })); + }, [setUser]); + + const onEmailChange = useCallback( + (e: React.ChangeEvent) => { + setUser((prev) => ({ ...prev, email: e.target.value })); + }, + [setUser], + ); + + const onNickChange = useCallback( + (e: React.ChangeEvent) => { + setUser((prev) => ({ ...prev, nick: e.target.value })); + }, + [setUser], + ); + + const openUserFiles = useCallback(() => { + window.open(`/home?path=${encodeURIComponent(`cloudreve://${values.hash_id}@my`)}`, "_blank"); + }, [values.id]); + + const onStatusChange = useCallback( + (e: SelectChangeEvent) => { + setUser((prev) => ({ ...prev, status: e.target.value as UserStatus })); + }, + [setUser], + ); + + const onGroupChange = useCallback( + (value: string) => { + setUser((prev) => ({ ...prev, group_users: parseInt(value) })); + }, + [setUser], + ); + + const onPasswordChange = useCallback( + (e: React.ChangeEvent) => { + setUser((prev) => ({ ...prev, password: e.target.value ? e.target.value : undefined })); + }, + [setUser], + ); + + const onReset2FA = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setUser((prev) => ({ ...prev, two_fa_enabled: false })); + }, + [setUser], + ); + + const onCalibrateStorage = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setLoading(true); + dispatch(sendCalibrateUserStorage(values.id)) + .then(() => { + reload(); + enqueueSnackbar({ + message: t("user.calibrateStorageSuccess"), + variant: "success", + action: DefaultCloseAction, + }); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch, values.id], + ); + + return ( + e.preventDefault()}> + + + + + + }> + {t("user.removeAvatar")} + + + + + + {t("user.idValue", { id: values.id, hash_id: values.hash_id })} + + + + + + + + + {t("user.calibrateStorage")} + + + + + }> + {t("user.openUserFiles")} + + + + + + + + + + + + + + + + {Object.values(UserStatus).map((value) => ( + + + {t(`user.status_${value}`)} + + + ))} + + + + + + + + + + + + + + {}} + emptyText={t("user.noOriginUserGroup")} + emptyValue={" "} + fullWidth + /> + {t("user.originUserGroupDes")} + + + + {t("user.groupExpiredDes")} + + + + + + + {values.two_fa_enabled ? t("user.2FAEnabled") : t("user.notEnabled")} + {values.two_fa_enabled && ( + + {t("user.reset2Fa")} + + )} + + + + + + + ); +}; + +export default UserForm; diff --git a/src/component/Admin/User/UserFilterPopover.tsx b/src/component/Admin/User/UserFilterPopover.tsx new file mode 100644 index 0000000..9eef54e --- /dev/null +++ b/src/component/Admin/User/UserFilterPopover.tsx @@ -0,0 +1,160 @@ +import { Box, Button, FormControl, ListItemText, Popover, PopoverProps, Stack } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { UserStatus } from "../../../api/dashboard"; +import { DenseFilledTextField, DenseSelect } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import GroupSelectionInput from "../Common/GroupSelectionInput"; + +export interface UserFilterPopoverProps extends PopoverProps { + email: string; + setEmail: (email: string) => void; + nick: string; + setNick: (nick: string) => void; + group: string; + setGroup: (group: string) => void; + status: string; + setStatus: (status: string) => void; + clearFilters: () => void; +} + +const UserFilterPopover = ({ + email, + setEmail, + nick, + setNick, + group, + setGroup, + status, + setStatus, + clearFilters, + onClose, + open, + ...rest +}: UserFilterPopoverProps) => { + const { t } = useTranslation("dashboard"); + + // Create local state to track changes before applying + const [localEmail, setLocalEmail] = useState(email); + const [localNick, setLocalNick] = useState(nick); + const [localGroup, setLocalGroup] = useState(group); + const [localStatus, setLocalStatus] = useState(status); + + // Initialize local state when popup opens + useEffect(() => { + if (open) { + setLocalEmail(email); + setLocalNick(nick); + setLocalGroup(group); + setLocalStatus(status); + } + }, [open]); + + // Apply filters and close popover + const handleApplyFilters = () => { + setEmail(localEmail); + setNick(localNick); + setGroup(localGroup == " " ? "" : localGroup); + setStatus(localStatus == " " ? "" : localStatus); + onClose?.({}, "backdropClick"); + }; + + // Reset filters and close popover + const handleResetFilters = () => { + setLocalEmail(""); + setLocalNick(""); + setLocalGroup(""); + setLocalStatus(""); + clearFilters(); + onClose?.({}, "backdropClick"); + }; + + return ( + + + + setLocalEmail(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + setLocalNick(e.target.value)} + placeholder={t("user.emptyNoFilter")} + size="small" + /> + + + + + + + + + setLocalStatus(e.target.value as string)} + > + + + {t("user.all")} + + + {Object.values(UserStatus).map((value) => ( + + {t(`user.status_${value}`)} + + ))} + + + + + + + + + + + ); +}; + +export default UserFilterPopover; diff --git a/src/component/Admin/User/UserForm.js b/src/component/Admin/User/UserForm.js deleted file mode 100644 index edca1b2..0000000 --- a/src/component/Admin/User/UserForm.js +++ /dev/null @@ -1,254 +0,0 @@ -import Button from "@material-ui/core/Button"; -import FormControl from "@material-ui/core/FormControl"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import Input from "@material-ui/core/Input"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import { makeStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; -import React, { useCallback, useEffect,useMemo, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { toggleSnackbar } from "../../../redux/explorer"; -import API from "../../../middleware/Api"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - root: { - [theme.breakpoints.up("md")]: { - marginLeft: 100, - }, - marginBottom: 40, - }, - form: { - maxWidth: 400, - marginTop: 20, - marginBottom: 20, - }, - formContainer: { - [theme.breakpoints.up("md")]: { - padding: "0px 24px 0 24px", - }, - }, -})); -export default function UserForm(props) { - const { t } = useTranslation("dashboard", { keyPrefix: "user" }); - const { t: tDashboard } = useTranslation("dashboard"); - const classes = useStyles(); - const [loading, setLoading] = useState(false); - const [user, setUser] = useState( - props.user - ? props.user - : { - ID: 0, - Email: "", - Nick: "", - Password: "", // 为空时只读 - Status: "0", // 转换类型 - GroupID: "2", // 转换类型 - TwoFactor: "", - } - ); - const [groups, setGroups] = useState([]); - - const history = useHistory(); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - API.get("/admin/groups") - .then((response) => { - setGroups(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }, []); - - const handleChange = (name) => (event) => { - setUser({ - ...user, - [name]: event.target.value, - }); - }; - - const submit = (e) => { - e.preventDefault(); - const userCopy = { ...user }; - - // 整型转换 - ["Status", "GroupID", "Score"].forEach((v) => { - userCopy[v] = parseInt(userCopy[v]); - }); - - setLoading(true); - API.post("/admin/user", { - user: userCopy, - password: userCopy.Password, - }) - .then(() => { - history.push("/admin/user"); - ToggleSnackbar( - "top", - "right", - props.user ? t("saved") : t("added"), - "success" - ); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const groupSelections = useMemo( - () => - groups.map((v) => { - if (v.ID === 3) { - return null; - } - return ( - - {v.Name} - - ); - }), - [groups] - ); - - return ( -
-
-
- - {user.ID === 0 && t("new")} - {user.ID !== 0 && t("editUser", { nick: user.Nick })} - - -
-
- - - {t("email")} - - - -
- -
- - - {t("nick")} - - - -
- -
- - - {t("password")} - - - - {user.ID !== 0 && t("passwordDes")} - - -
- -
- - - {t("group")} - - - - {t("groupDes")} - - -
- -
- - - {t("status")} - - - -
- -
- - - {t("2FASecret")} - - - - - {t("2FASecretDes")} - -
-
-
-
- -
-
-
- ); -} diff --git a/src/component/Admin/User/UserRow.tsx b/src/component/Admin/User/UserRow.tsx new file mode 100644 index 0000000..4d60e96 --- /dev/null +++ b/src/component/Admin/User/UserRow.tsx @@ -0,0 +1,176 @@ +import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow, Tooltip } from "@mui/material"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { batchDeleteUser } from "../../../api/api"; +import { User, UserStatus } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { sizeToString } from "../../../util"; +import { NoWrapTableCell, NoWrapTypography, SquareChip } from "../../Common/StyledComponents"; +import UserAvatar from "../../Common/User/UserAvatar"; +import Delete from "../../Icons/Delete"; +import PersonPasskey from "../../Icons/PersonPasskey"; + +export interface UserRowProps { + user?: User; + loading?: boolean; + deleting?: boolean; + selected?: boolean; + onDelete?: () => void; + onDetails?: (id: number) => void; + onSelect?: (id: number) => void; +} + +const UserRow = ({ user, loading, deleting, selected, onDelete, onDetails, onSelect }: UserRowProps) => { + const navigate = useNavigate(); + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [deleteLoading, setDeleteLoading] = useState(false); + + const onRowClick = () => { + onDetails?.(user?.id ?? 0); + }; + + const onDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(confirmOperation(t("user.confirmDelete", { user: user?.email }))).then(() => { + if (user?.id) { + setDeleteLoading(true); + dispatch(batchDeleteUser({ ids: [user.id] })) + .then(() => { + onDelete?.(); + }) + .finally(() => { + setDeleteLoading(false); + }); + } + }); + }; + + const onSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onSelect?.(user?.id ?? 0); + }; + + const userProps = useMemo(() => { + const res = { + passkey: false, + twoFa: false, + }; + + if (user?.edges?.passkey) { + res.passkey = true; + } + + if (user?.two_fa_enabled) { + res.twoFa = true; + } + + return res; + }, [user]); + + if (loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const stopPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + + + + + + {user?.id} + + + + + {user?.nick} + + {user?.status == UserStatus.inactive && } + {user?.status == UserStatus.sys_banned && ( + + )} + {user?.status == UserStatus.manual_banned && ( + + )} + {user?.two_fa_enabled && ( + + + + + + )} + + + + + {user?.email} + + + + + {user?.edges?.group?.name} + + + + {sizeToString(user?.storage ?? 0)} + + + + + + + ); +}; + +export default UserRow; diff --git a/src/component/Admin/User/UserSetting.tsx b/src/component/Admin/User/UserSetting.tsx new file mode 100644 index 0000000..7f3a14d --- /dev/null +++ b/src/component/Admin/User/UserSetting.tsx @@ -0,0 +1,319 @@ +import { Delete } from "@mui/icons-material"; +import { + Badge, + Box, + Button, + Checkbox, + Container, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useQueryState } from "nuqs"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { batchDeleteUser, getUserList } from "../../../api/api"; +import { User } from "../../../api/dashboard"; +import { useAppDispatch } from "../../../redux/hooks"; +import { confirmOperation } from "../../../redux/thunks/dialog"; +import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; +import Add from "../../Icons/Add"; +import ArrowSync from "../../Icons/ArrowSync"; +import Filter from "../../Icons/Filter"; +import PageContainer from "../../Pages/PageContainer"; +import PageHeader from "../../Pages/PageHeader"; +import TablePagination from "../Common/TablePagination"; +import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; +import NewUserDialog from "./NewUserDialog"; +import UserDialog from "./UserDialog/UserDialog"; +import UserFilterPopover from "./UserFilterPopover"; +import UserRow from "./UserRow"; +export const EmailQuery = "email"; +export const NickQuery = "nick"; +export const GroupQuery = "group"; +export const StatusQuery = "status"; + +const UserSetting = () => { + const { t } = useTranslation("dashboard"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(true); + const [users, setUsers] = useState([]); + const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState(PageSizeQuery, { + defaultValue: "10", + }); + const [orderBy, setOrderBy] = useQueryState(OrderByQuery, { + defaultValue: "", + }); + const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" }); + const [email, setEmail] = useQueryState(EmailQuery, { defaultValue: "" }); + const [nick, setNick] = useQueryState(NickQuery, { defaultValue: "" }); + const [group, setGroup] = useQueryState(GroupQuery, { defaultValue: "" }); + const [status, setStatus] = useQueryState(StatusQuery, { defaultValue: "" }); + const [count, setCount] = useState(0); + const [selected, setSelected] = useState([]); + const [createNewOpen, setCreateNewOpen] = useState(false); + const filterPopupState = usePopupState({ + variant: "popover", + popupId: "userFilterPopover", + }); + + const [userDialogOpen, setUserDialogOpen] = useState(false); + const [userDialogID, setUserDialogID] = useState(undefined); + const [deleteLoading, setDeleteLoading] = useState(false); + + const pageInt = parseInt(page) ?? 1; + const pageSizeInt = parseInt(pageSize) ?? 11; + + const clearFilters = useCallback(() => { + setEmail(""); + setNick(""); + setGroup(""); + setStatus(""); + }, [setEmail, setNick, setGroup, setStatus]); + + useEffect(() => { + fetchUsers(); + }, [page, pageSize, orderBy, orderDirection, email, nick, group, status]); + + const fetchUsers = () => { + setLoading(true); + setSelected([]); + dispatch( + getUserList({ + page: pageInt, + page_size: pageSizeInt, + order_by: orderBy ?? "", + order_direction: orderDirection ?? "desc", + conditions: { + user_email: email, + user_nick: nick, + user_group: group, + user_status: status, + }, + }), + ) + .then((res) => { + setUsers(res.users); + setPageSize(res.pagination.page_size.toString()); + setCount(res.pagination.total_items ?? 0); + setLoading(false); + }) + .finally(() => { + setLoading(false); + }); + }; + + const handleDelete = () => { + setDeleteLoading(true); + dispatch(confirmOperation(t("user.confirmBatchDelete", { num: selected.length }))) + .then(() => { + dispatch(batchDeleteUser({ ids: Array.from(selected) })) + .then(() => { + fetchUsers(); + }) + .finally(() => { + setDeleteLoading(false); + }); + }) + .finally(() => { + setDeleteLoading(false); + }); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = users.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleSelect = useCallback( + (id: number) => { + const selectedIndex = selected.indexOf(id); + let newSelected: readonly number[] = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); + } + setSelected(newSelected); + }, + [selected], + ); + + const orderById = orderBy === "id" || orderBy === ""; + const direction = orderDirection as "asc" | "desc"; + const onSortClick = (field: string) => () => { + const alreadySorted = orderBy === field || (field === "id" && orderById); + setOrderBy(field); + setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc"); + }; + + const hasActiveFilters = useMemo(() => { + return !!(email || nick || group || status); + }, [email, nick, group, status]); + + const handleUserDialogOpen = (id: number) => { + setUserDialogID(id); + setUserDialogOpen(true); + }; + + return ( + + setCreateNewOpen(false)} + onCreated={(user) => { + setUserDialogID(user.id); + setUserDialogOpen(true); + }} + /> + setUserDialogOpen(false)} + userID={userDialogID} + onUpdated={() => fetchUsers()} + /> + + + + + + + + }> + {t("node.refresh")} + + + + } variant="contained" {...bindTrigger(filterPopupState)}> + {t("user.filter")} + + + + {selected.length > 0 && !isMobile && ( + <> + + + + )} + + {isMobile && selected.length > 0 && ( + + + + )} + + + + + + 0 && selected.length < users.length} + checked={users.length > 0 && selected.length === users.length} + onChange={handleSelectAllClick} + /> + + + + {t("group.#")} + + + + + {t("user.nick")} + + + + + {t("user.email")} + + + {t("user.group")} + + + {t("user.usedStorage")} + + + + + + + {!loading && + users.map((user) => ( + + ))} + {loading && + users.length > 0 && + users.slice(0, 10).map((user) => )} + {loading && + users.length === 0 && + Array.from(Array(5)).map((_, index) => )} + +
+
+ {count > 0 && ( + + setPageSize(value.toString())} + onChange={(_, value) => setPage(value.toString())} + /> + + )} +
+
+ ); +}; + +export default UserSetting; diff --git a/src/component/Common/AutoHeight.tsx b/src/component/Common/AutoHeight.tsx new file mode 100644 index 0000000..b155270 --- /dev/null +++ b/src/component/Common/AutoHeight.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useRef, useState } from "react"; +import AnimateHeight, { Height } from "react-animate-height"; +import { useTheme } from "@mui/material"; + +// @ts-ignore +const AutoHeight = ({ children, ...props }) => { + const [height, setHeight] = useState("auto"); + const contentDiv = useRef(null); + const theme = useTheme(); + + useEffect(() => { + const element = contentDiv.current as HTMLDivElement; + + const resizeObserver = new ResizeObserver(() => { + setHeight(element.clientHeight); + }); + + resizeObserver.observe(element); + + return () => resizeObserver.disconnect(); + }, [contentDiv]); + + return ( + + {children} + + ); +}; + +export default AutoHeight; diff --git a/src/component/Common/BorderLinearProgress.tsx b/src/component/Common/BorderLinearProgress.tsx new file mode 100644 index 0000000..c5686c3 --- /dev/null +++ b/src/component/Common/BorderLinearProgress.tsx @@ -0,0 +1,17 @@ +import { LinearProgress, linearProgressClasses } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({ + height: 8, + borderRadius: 5, + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: + theme.palette.grey[theme.palette.mode === "light" ? 200 : 800], + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 5, + backgroundColor: theme.palette.mode === "light" ? "#1a90ff" : "#308fe8", + }, +})); + +export default BorderLinearProgress; diff --git a/src/component/Common/Captcha/Captcha.tsx b/src/component/Common/Captcha/Captcha.tsx new file mode 100644 index 0000000..c0ea5a3 --- /dev/null +++ b/src/component/Common/Captcha/Captcha.tsx @@ -0,0 +1,35 @@ +import { useAppSelector } from "../../../redux/hooks.ts"; +import { CaptchaType } from "../../../api/site.ts"; +import DefaultCaptcha from "./DefaultCaptcha.tsx"; +import ReCaptchaV2 from "./ReCaptchaV2.tsx"; +import TurnstileCaptcha from "./TurnstileCaptcha.tsx"; + +export interface CaptchaProps { + onStateChange: (state: CaptchaParams) => void; + generation: number; + [x: string]: any; +} + +export interface CaptchaParams { + [x: string]: any; +} + +export const Captcha = (props: CaptchaProps) => { + const captchaType = useAppSelector( + (state) => state.siteConfig.basic.config.captcha_type, + ); + + // const recaptcha = useRecaptcha(setCaptchaLoading); + // const tcaptcha = useTCaptcha(setCaptchaLoading); + + switch (captchaType) { + case CaptchaType.RECAPTCHA: + return ; + case CaptchaType.TURNSTILE: + return ; + // case "tcaptcha": + // return { ...tcaptcha, captchaRefreshRef, captchaLoading }; + default: + return ; + } +}; diff --git a/src/component/Common/Captcha/DefaultCaptcha.tsx b/src/component/Common/Captcha/DefaultCaptcha.tsx new file mode 100644 index 0000000..c1d4d6d --- /dev/null +++ b/src/component/Common/Captcha/DefaultCaptcha.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getCaptcha } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { Box, InputAdornment, Skeleton, TextField } from "@mui/material"; +import { CaptchaParams } from "./Captcha.tsx"; + +export interface DefaultCaptchaProps { + onStateChange: (state: CaptchaParams) => void; + generation: number; +} + +const DefaultCaptcha = ({ + onStateChange, + generation, + ...rest +}: DefaultCaptchaProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [captcha, setCaptcha] = useState(""); + const [sessionId, setSessionID] = useState(""); + const [captchaData, setCaptchaData] = useState(); + + const refreshCaptcha = async () => { + setCaptchaData(undefined); + const captchaResponse = await dispatch(getCaptcha()); + setCaptchaData(captchaResponse.image); + setSessionID(captchaResponse.ticket); + }; + + useEffect(() => { + refreshCaptcha(); + }, [generation]); + + useEffect(() => { + onStateChange({ captcha, ticket: sessionId }); + }, [captcha, sessionId]); + + return ( + setCaptcha(e.target.value)} + value={captcha} + autoComplete={"true"} + {...rest} + slotProps={{ + input: { + endAdornment: ( + + + {!captchaData && ( + `${theme.shape.borderRadius}px`, + }} + variant="rounded" + width={192} + height={48} + /> + )} + {captchaData && ( + `${theme.shape.borderRadius}px`, + height: 48, + }} + src={captchaData} + alt="captcha" + onClick={refreshCaptcha} + /> + )} + + + ), + }, + + htmlInput: { + name: "captcha", + id: "captcha", + } + }} /> + ); +}; + +export default DefaultCaptcha; diff --git a/src/component/Common/Captcha/ReCaptchaV2.tsx b/src/component/Common/Captcha/ReCaptchaV2.tsx new file mode 100644 index 0000000..f3373c9 --- /dev/null +++ b/src/component/Common/Captcha/ReCaptchaV2.tsx @@ -0,0 +1,66 @@ +import { useEffect, useRef } from "react"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { CaptchaParams } from "./Captcha.tsx"; +import ReCAPTCHA from "react-google-recaptcha"; +import { Box, useTheme } from "@mui/material"; + +export interface ReCaptchaV2Props { + onStateChange: (state: CaptchaParams) => void; + generation: number; +} + +declare global { + interface Window { + subTitle: string; + recaptchaOptions: { + useRecaptchaNet: boolean; + }; + } +} + +window.recaptchaOptions = { + useRecaptchaNet: true, +}; + +const ReCaptchaV2 = ({ + onStateChange, + generation, + ...rest +}: ReCaptchaV2Props) => { + const theme = useTheme(); + + const captchaRef = useRef(); + const reCaptchaKey = useAppSelector( + (state) => state.siteConfig.basic.config.captcha_ReCaptchaKey, + ); + + const refreshCaptcha = async () => { + captchaRef.current?.reset(); + }; + + useEffect(() => { + refreshCaptcha(); + }, [generation]); + + const onCompleted = () => { + const recaptchaValue = captchaRef.current?.getValue(); + if (recaptchaValue) { + onStateChange({ captcha: recaptchaValue }); + } + }; + + return ( + + + + ); +}; + +export default ReCaptchaV2; diff --git a/src/component/Common/Captcha/TurnstileCaptcha.tsx b/src/component/Common/Captcha/TurnstileCaptcha.tsx new file mode 100644 index 0000000..5d6778a --- /dev/null +++ b/src/component/Common/Captcha/TurnstileCaptcha.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef } from "react"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { CaptchaParams } from "./Captcha.tsx"; +import { Box, useTheme } from "@mui/material"; +import { Turnstile } from "@marsidev/react-turnstile"; +import i18next from "i18next"; + +export interface TurnstileProps { + onStateChange: (state: CaptchaParams) => void; + generation: number; +} + +const TurnstileCaptcha = ({ + onStateChange, + generation, + ...rest +}: TurnstileProps) => { + const theme = useTheme(); + + const captchaRef = useRef(); + const turnstileKey = useAppSelector( + (state) => state.siteConfig.basic.config.turnstile_site_id, + ); + + const refreshCaptcha = async () => { + captchaRef.current?.reset(); + }; + + useEffect(() => { + refreshCaptcha(); + }, [generation]); + + const onCompleted = (t: string) => { + onStateChange({ ticket: t }); + }; + + return ( + + {turnstileKey && ( + + )} + + ); +}; + +export default TurnstileCaptcha; diff --git a/src/component/Common/CircularProgress.tsx b/src/component/Common/CircularProgress.tsx new file mode 100644 index 0000000..7a58413 --- /dev/null +++ b/src/component/Common/CircularProgress.tsx @@ -0,0 +1,48 @@ +import { + Box, + CircularProgress, + circularProgressClasses, + CircularProgressProps, +} from "@mui/material"; +import { forwardRef } from "react"; + +export interface FacebookCircularProgressProps extends CircularProgressProps { + bgColor?: string; + fgColor?: string; +} + +const FacebookCircularProgress = forwardRef( + ({ sx, bgColor, fgColor, ...rest }: FacebookCircularProgressProps, ref) => { + return ( + + + bgColor ?? + theme.palette.grey[theme.palette.mode === "light" ? 200 : 800], + }} + size={40} + thickness={4} + {...rest} + value={100} + /> + fgColor ?? theme.palette.primary.main, + position: "absolute", + left: 0, + [`& .${circularProgressClasses.circle}`]: { + strokeLinecap: "round", + }, + }} + size={40} + thickness={4} + {...rest} + /> + + ); + }, +); + +export default FacebookCircularProgress; diff --git a/src/component/Common/ErrorBoundary.tsx b/src/component/Common/ErrorBoundary.tsx new file mode 100644 index 0000000..ff8a5dd --- /dev/null +++ b/src/component/Common/ErrorBoundary.tsx @@ -0,0 +1,30 @@ +import { useRouteError } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +function ErrorBoundary() { + let error = useRouteError(); + const { t } = useTranslation(); + console.log(error); + // Uncaught ReferenceError: path is not defined + return ( +
+

:(

+

{t("common:renderError")}

+ {!!error && ( +
+ {t("common:errorDetails")} +
+            {error.toString()}
+          
+ {error.stack && ( +
+              {error.stack}
+            
+ )} +
+ )} +
+ ); +} + +export default ErrorBoundary; diff --git a/src/component/Common/FadeTransition.css b/src/component/Common/FadeTransition.css new file mode 100644 index 0000000..cfbd612 --- /dev/null +++ b/src/component/Common/FadeTransition.css @@ -0,0 +1,17 @@ +.fade-enter { + opacity: 0; +} +.fade-enter-active { + opacity: 1; +} +.fade-exit { + opacity: 1; +} +.fade-exit-active { + opacity: 0; +} +.fade-enter-active, +.fade-exit-active { + transition: opacity 150ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/src/component/Common/Form/FileDisplayForm.tsx b/src/component/Common/Form/FileDisplayForm.tsx new file mode 100644 index 0000000..a2e8f22 --- /dev/null +++ b/src/component/Common/Form/FileDisplayForm.tsx @@ -0,0 +1,22 @@ +import FileBadge from "../../FileManager/FileBadge.tsx"; +import { FileResponse } from "../../../api/explorer.ts"; +import { StyledTextField } from "./PathSelectorForm.tsx"; + +export interface FileDisplayFormProps { + file: FileResponse; + label: string; +} + +export const FileDisplayForm = ({ file, label }: FileDisplayFormProps) => { + return ( + , + }} + label={label} + fullWidth + /> + ); +}; diff --git a/src/component/Common/Form/OutlineIconTextField.tsx b/src/component/Common/Form/OutlineIconTextField.tsx new file mode 100644 index 0000000..b0b23c1 --- /dev/null +++ b/src/component/Common/Form/OutlineIconTextField.tsx @@ -0,0 +1,31 @@ +import { + InputAdornment, + TextField, + TextFieldProps, + useMediaQuery, + useTheme, +} from "@mui/material"; + +export interface OutlineIconTextFieldProps extends TextFieldProps<"outlined"> { + icon: React.ReactNode; +} + +export const OutlineIconTextField = ({ + icon, + ...rest +}: OutlineIconTextFieldProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + return ( + {icon} + ), + ...rest.InputProps, + } + }} /> + ); +}; diff --git a/src/component/Common/Form/PathSelectorForm.tsx b/src/component/Common/Form/PathSelectorForm.tsx new file mode 100644 index 0000000..546b36c --- /dev/null +++ b/src/component/Common/Form/PathSelectorForm.tsx @@ -0,0 +1,48 @@ +import { styled, TextField, TextFieldProps } from "@mui/material"; +import { useCallback } from "react"; +import { FileType } from "../../../api/explorer.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { selectPath } from "../../../redux/thunks/dialog.ts"; +import FileBadge from "../../FileManager/FileBadge.tsx"; + +export interface PathSelectorFormProps { + path: string; + label?: string; + onChange: (path: string) => void; + variant?: string; + textFieldProps?: TextFieldProps; + allowedFs?: string[]; +} + +export const StyledTextField = styled(TextField)({ + "& .MuiInputBase-root": { + paddingLeft: "8px", + cursor: "pointer", + }, + "& .MuiOutlinedInput-input": { + paddingLeft: "8px", + cursor: "pointer", + }, +}); + +export const PathSelectorForm = ({ path, onChange, label, variant, textFieldProps }: PathSelectorFormProps) => { + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(selectPath(variant ?? "saveTo", path)).then((path) => { + onChange(path); + }); + }, [dispatch]); + return ( + , + }} + label={label} + fullWidth + {...textFieldProps} + /> + ); +}; diff --git a/src/component/Common/ICPFooter.js b/src/component/Common/ICPFooter.js deleted file mode 100644 index d0390b4..0000000 --- a/src/component/Common/ICPFooter.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Link, makeStyles } from "@material-ui/core"; -import React, { useEffect, useState } from "react"; -import { useSelector } from "react-redux"; -import { useLocation } from "react-router"; -import pageHelper from "../../utils/page"; - -const useStyles = makeStyles(() => ({ - icp: { - padding: "8px 24px", - position: "absolute", - bottom: 0, - }, -})); - -export const ICPFooter = () => { - const siteICPId = useSelector((state) => state.siteConfig.siteICPId); - const classes = useStyles(); - const location = useLocation(); - const [show, setShow] = useState(true); - - useEffect(() => { - // 只在分享和登录界面显示 - const isSharePage = pageHelper.isSharePage(location.pathname); - const isLoginPage = pageHelper.isLoginPage(location.pathname); - setShow(siteICPId && (isSharePage || isLoginPage)); - }, [siteICPId, location]); - - if (!show) { - return <>; - } - return ( -
- - {siteICPId} - -
- ); -}; diff --git a/src/component/Common/Logo.tsx b/src/component/Common/Logo.tsx new file mode 100644 index 0000000..c254c7b --- /dev/null +++ b/src/component/Common/Logo.tsx @@ -0,0 +1,47 @@ +import { Box, Skeleton, useTheme } from "@mui/material"; +import React, { useEffect, useRef } from "react"; +import { useAppSelector } from "../../redux/hooks.ts"; + +const Logo = (props: any) => { + const theme = useTheme(); + const imageRef = useRef(); + const [loaded, setLoaded] = React.useState(false); + const { mode } = theme.palette; + const logo = useAppSelector((state) => state.siteConfig.basic.config.logo); + const logo_light = useAppSelector((state) => state.siteConfig.basic.config.logo_light); + useEffect(() => { + setLoaded(logo == logo_light); + }, [mode]); + + useEffect(() => { + if (imageRef.current?.complete) { + setLoaded(true); + } + }, []); + + return ( + <> + {(!logo || !loaded) && } + {logo && ( + setLoaded(true)} + src={mode === "light" ? logo : logo_light} + {...props} + sx={{ + display: loaded ? "block" : "none", + // disable drag + userSelect: "none", + WebkitUserDrag: "none", + MozUserDrag: "none", + msUserDrag: "none", + ...props.sx, + }} + /> + )} + + ); +}; + +export default Logo; diff --git a/src/component/Common/Nothing.tsx b/src/component/Common/Nothing.tsx new file mode 100644 index 0000000..182a91d --- /dev/null +++ b/src/component/Common/Nothing.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Box, Typography } from "@mui/material"; +import PackageOpen from "../Icons/PackageOpen.tsx"; + +export interface NothingProps { + primary: string; + secondary?: string; + top?: number; + size?: number; +} + +export default function Nothing({ + primary, + secondary, + top = 20, + size = 1, +}: NothingProps) { + return ( + theme.palette.action.disabled, + textAlign: "center", + }} + > + + theme.palette.action.disabled, + }} + > + {primary} + + {secondary && ( + theme.palette.action.disabled }} + > + {secondary} + + )} + + ); +} diff --git a/src/component/Common/ResponsiveTabs.tsx b/src/component/Common/ResponsiveTabs.tsx new file mode 100644 index 0000000..a15ef0b --- /dev/null +++ b/src/component/Common/ResponsiveTabs.tsx @@ -0,0 +1,121 @@ +import * as React from "react"; +import { useLayoutEffect, useRef, useState } from "react"; +import { + Box, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { StyledTab, StyledTabs } from "./StyledComponents.tsx"; +import CaretDown from "../Icons/CaretDown.tsx"; +import { useTranslation } from "react-i18next"; +import { + bindMenu, + bindTrigger, + usePopupState, +} from "material-ui-popup-state/hooks"; + +export interface Tab { + label: React.ReactNode; + value: T; + icon?: React.ReactElement; +} + +export interface ResponsiveTabsProps { + tabs: Tab[]; + value: T; + onChange: (event: React.SyntheticEvent, value: T) => void; +} + +const ResponsiveTabs = ({ + tabs, + value, + onChange, +}: ResponsiveTabsProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [hideTabs, setHideTabs] = useState(false); + const tabsRef = useRef(null); + const { t } = useTranslation(); + const moreOptionState = usePopupState({ + variant: "popover", + popupId: "tabMore", + }); + const { onClose, ...menuProps } = bindMenu(moreOptionState); + useLayoutEffect(() => { + const checkOverflow = () => { + if (tabsRef.current?.children[0]?.children[0]) { + setHideTabs((e) => + e + ? true + : (tabsRef.current?.children[0]?.children[0]?.scrollWidth ?? 0) > + (tabsRef.current?.children[0]?.children[0]?.clientWidth ?? 0), + ); + } + }; + + checkOverflow(); + window.addEventListener("resize", checkOverflow); + return () => window.removeEventListener("resize", checkOverflow); + }, []); + + return ( + + + {tabs + .filter((tab) => (isMobile || hideTabs ? tab.value == value : true)) + .map((tab) => ( + + ))} + {(isMobile || hideTabs) && tabs.length > 1 && ( + <> + + {t("application:navbar.showMore")} + + + } + {...bindTrigger(moreOptionState)} + /> + + {tabs + .filter((tab) => tab.value != value) + .map((option, index) => ( + { + onClose(); + onChange(e, option.value); + }} + > + {option.icon && {option.icon}} + {option.label} + + ))} + + + )} + + + ); +}; + +export default ResponsiveTabs; diff --git a/src/component/Common/SizeInput.tsx b/src/component/Common/SizeInput.tsx new file mode 100644 index 0000000..1d1f7a9 --- /dev/null +++ b/src/component/Common/SizeInput.tsx @@ -0,0 +1,186 @@ +import { + FilledInput, + FilledInputProps, + FormControl, + FormHelperText, + InputAdornment, + InputLabel, + MenuItem, + Select, + styled, +} from "@mui/material"; +import { parseInt } from "lodash"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../redux/hooks.ts"; +import { DenseFilledTextField } from "./StyledComponents.tsx"; + +const unitTransform = (v?: number): number[] => { + if (!v || v.toString() === "0") { + return [0, 1024 * 1024]; + } + for (let i = 4; i >= 0; i--) { + const base = Math.pow(1024, i); + if (v % base === 0) { + return [v / base, base]; + } + } + + return [0, 1024 * 1024]; +}; + +export interface SizeInputProps { + onChange: (size: number) => void; + min?: number; + value: number; + required?: boolean; + label?: string; + max?: number; + suffix?: string; + inputProps?: FilledInputProps; + variant?: "filled" | "outlined"; + allowZero?: boolean; +} + +const StyledSelect = styled(Select)(() => ({ + "& .MuiFilledInput-input": { + paddingTop: "5px", + "&:focus": { + backgroundColor: "initial", + }, + }, + minWidth: "70px", + marginTop: "14px", + backgroundColor: "initial", +})); + +const StyleOutlinedSelect = styled(Select)(({ theme }) => ({ + "& .MuiFilledInput-input": { + paddingTop: "5px", + "&:focus": { + backgroundColor: "initial", + }, + }, + minWidth: "70px", + backgroundColor: "initial", + fontSize: theme.typography.body2.fontSize, +})); + +export default function SizeInput({ + onChange, + min, + value, + required, + label, + max, + inputProps, + allowZero = true, + suffix, + variant = "filled", +}: SizeInputProps) { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const [unit, setUnit] = useState(1); + const [val, setVal] = useState(value); + const [err, setError] = useState(""); + + useEffect(() => { + onChange(val * unit); + if ((max && val * unit > max) || (min && val * unit < min)) { + setError(t("common:incorrectSizeInput")); + } else { + setError(""); + } + }, [val, unit, max, min]); + + useEffect(() => { + const res = unitTransform(value); + setUnit(res[1]); + setVal(res[0]); + }, [value]); + + if (variant === "outlined") { + return ( + ) => setVal(parseInt(e.target.value) ?? 0)} + error={err !== ""} + helperText={err} + required={required} + InputProps={{ + endAdornment: ( + + setUnit(e.target.value as number)} + > + + B{suffix && suffix} + + + KB{suffix && suffix} + + + MB{suffix && suffix} + + + GB{suffix && suffix} + + + TB{suffix && suffix} + + + + ), + ...inputProps, + }} + /> + ); + } + + return ( + + {label} + ) => setVal(parseInt(e.target.value) ?? 0)} + required={required} + endAdornment={ + + setUnit(e.target.value as number)} + > + + B{suffix && suffix} + + + KB{suffix && suffix} + + + MB{suffix && suffix} + + + GB{suffix && suffix} + + + TB{suffix && suffix} + + + + } + {...inputProps} + /> + {err !== "" && {err}} + + ); +} diff --git a/src/component/Common/Snackbar.js b/src/component/Common/Snackbar.js deleted file mode 100644 index b5262c3..0000000 --- a/src/component/Common/Snackbar.js +++ /dev/null @@ -1,151 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import classNames from "classnames"; -import ErrorIcon from "@material-ui/icons/Error"; -import InfoIcon from "@material-ui/icons/Info"; -import CloseIcon from "@material-ui/icons/Close"; -import CheckCircleIcon from "@material-ui/icons/CheckCircle"; -import WarningIcon from "@material-ui/icons/Warning"; - -import { - IconButton, - Snackbar, - SnackbarContent, - withStyles, -} from "@material-ui/core"; - -const mapStateToProps = (state) => { - return { - snackbar: state.viewUpdate.snackbar, - }; -}; - -const mapDispatchToProps = () => { - return {}; -}; - -const variantIcon = { - success: CheckCircleIcon, - warning: WarningIcon, - error: ErrorIcon, - info: InfoIcon, -}; - -const styles1 = (theme) => ({ - success: { - backgroundColor: theme.palette.success.main, - }, - error: { - backgroundColor: theme.palette.error.dark, - }, - info: { - backgroundColor: theme.palette.info.main, - }, - warning: { - backgroundColor: theme.palette.warning.main, - }, - icon: { - fontSize: 20, - }, - iconVariant: { - opacity: 0.9, - marginRight: theme.spacing(1), - }, - message: { - display: "flex", - alignItems: "center", - }, -}); - -function MySnackbarContent(props) { - const { classes, className, message, onClose, variant, ...other } = props; - const Icon = variantIcon[variant]; - - return ( - - - {message} - - } - action={[ - - - , - ]} - {...other} - /> - ); -} -MySnackbarContent.propTypes = { - classes: PropTypes.object.isRequired, - className: PropTypes.string, - message: PropTypes.node, - onClose: PropTypes.func, - variant: PropTypes.oneOf(["alert", "success", "warning", "error", "info"]) - .isRequired, -}; - -const MySnackbarContentWrapper = withStyles(styles1)(MySnackbarContent); -const styles = (theme) => ({ - margin: { - margin: theme.spacing(1), - }, -}); -class SnackbarCompoment extends Component { - state = { - open: false, - }; - - UNSAFE_componentWillReceiveProps = (nextProps) => { - if (nextProps.snackbar.toggle !== this.props.snackbar.toggle) { - this.setState({ open: true }); - } - }; - - handleClose = () => { - this.setState({ open: false }); - }; - - render() { - return ( - - - - ); - } -} - -const AlertBar = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(SnackbarCompoment)); - -export default AlertBar; diff --git a/src/component/Common/Snackbar/LoadingSnackbar.tsx b/src/component/Common/Snackbar/LoadingSnackbar.tsx new file mode 100644 index 0000000..261b42d --- /dev/null +++ b/src/component/Common/Snackbar/LoadingSnackbar.tsx @@ -0,0 +1,91 @@ +import { Box } from "@mui/material"; +import MuiSnackbarContent from "@mui/material/SnackbarContent"; +import { CustomContentProps } from "notistack"; +import * as React from "react"; +import { forwardRef, useEffect, useState } from "react"; +import CircularProgress from "../CircularProgress.tsx"; + +declare module "notistack" { + interface VariantOverrides { + loading: { + getProgress?: () => number; + }; + } +} + +interface LoadingSnackbarProps extends CustomContentProps { + getProgress?: () => number; +} + +const LoadingSnackbar = forwardRef((props, ref) => { + const [progress, setProgress] = useState(0); + const { + // You have access to notistack props and options 👇🏼 + message, + action, + id, + getProgress, + // as well as your own custom props 👇🏼 + ...other + } = props; + + useEffect(() => { + var intervalId: NodeJS.Timeout; + if (getProgress) { + intervalId = setInterval(() => { + setProgress(getProgress()); + }, 1000); + } + + return () => { + clearInterval(intervalId); + }; + }, [getProgress]); + + let componentOrFunctionAction: React.ReactNode = undefined; + if (typeof action === "function") { + componentOrFunctionAction = action(id); + } else { + componentOrFunctionAction = action; + } + + return ( + + + + + {message} + {componentOrFunctionAction && ( + + {componentOrFunctionAction} + + )} + + } + /> + ); +}); + +export default LoadingSnackbar; diff --git a/src/component/Common/Snackbar/snackbar.tsx b/src/component/Common/Snackbar/snackbar.tsx new file mode 100644 index 0000000..1f74ea5 --- /dev/null +++ b/src/component/Common/Snackbar/snackbar.tsx @@ -0,0 +1,111 @@ +import { closeSnackbar, SnackbarKey } from "notistack"; +import { Button } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Response } from "../../../api/request.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { useCallback } from "react"; +import { showAggregatedErrorDialog } from "../../../redux/thunks/dialog.ts"; +import { navigateToPath } from "../../../redux/thunks/filemanager.ts"; +import { FileManagerIndex } from "../../FileManager/FileManager.tsx"; +import { setBatchDownloadLogDialog } from "../../../redux/globalStateSlice.ts"; +import { useNavigate } from "react-router-dom"; + +export const DefaultCloseAction = (snackbarId: SnackbarKey | undefined) => { + const { t } = useTranslation(); + return ( + <> + + + ); +}; + +export const ErrorListDetailAction = + (error: Response) => (snackbarId: SnackbarKey | undefined) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const Close = DefaultCloseAction(snackbarId); + + const showDetails = useCallback(() => { + dispatch(showAggregatedErrorDialog(error)); + closeSnackbar(snackbarId); + }, [dispatch, error, snackbarId]); + + return ( + <> + + {Close} + + ); + }; + +export const ViewDstAction = + (dst: string) => (snackbarId: SnackbarKey | undefined) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const Close = DefaultCloseAction(snackbarId); + + const viewDst = useCallback(() => { + dispatch(navigateToPath(FileManagerIndex.main, dst)); + closeSnackbar(snackbarId); + }, [dispatch, snackbarId]); + + return ( + <> + + {Close} + + ); + }; + +export const ViewDownloadLogAction = + (downloadId: string) => (_snackbarId: SnackbarKey | undefined) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const viewLogs = useCallback(() => { + dispatch(setBatchDownloadLogDialog({ open: true, id: downloadId })); + }, [dispatch, downloadId]); + + return ( + <> + + + ); + }; + +export const ViewTaskAction = + (path: string = "/tasks") => + (snackbarId: SnackbarKey | undefined) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const Close = DefaultCloseAction(snackbarId); + + const viewDst = useCallback(() => { + navigate(path); + closeSnackbar(snackbarId); + }, [navigate, snackbarId]); + + return ( + <> + + {Close} + + ); + }; diff --git a/src/component/Common/StyledComponents.tsx b/src/component/Common/StyledComponents.tsx new file mode 100644 index 0000000..435302d --- /dev/null +++ b/src/component/Common/StyledComponents.tsx @@ -0,0 +1,235 @@ +import { LoadingButton } from "@mui/lab"; +import { + alpha, + Autocomplete, + Box, + Button, + ButtonProps, + Checkbox, + Chip, + FormControlLabel, + FormControlLabelProps, + ListItemText, + ListItemTextProps, + Paper, + Select, + styled, + Tab, + TableCell, + Tabs, + TextField, + Typography, +} from "@mui/material"; + +export const DefaultButton = styled(({ variant, ...rest }: ButtonProps) => + } + showActions + hideOk + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + }} + > + + theme.typography.body2.fontSize, + }, + }} + sx={{ pt: 0.5 }} + minRows={10} + maxRows={10} + variant="outlined" + value={logs} + multiline + fullWidth + id="standard-basic" + /> + + + ); +}; +export default BatchDownloadLog; diff --git a/src/component/Dialogs/Confirmation.tsx b/src/component/Dialogs/Confirmation.tsx new file mode 100644 index 0000000..2014b7c --- /dev/null +++ b/src/component/Dialogs/Confirmation.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from "react-i18next"; +import { DialogContent, Stack } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import { useCallback } from "react"; +import DraggableDialog, { + StyledDialogContentText, +} from "./DraggableDialog.tsx"; +import { generalDialogPromisePool } from "../../redux/thunks/dialog.ts"; +import { closeConfirmDialog } from "../../redux/globalStateSlice.ts"; + +const Confirmation = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const open = useAppSelector((state) => state.globalState.confirmDialogOpen); + const message = useAppSelector( + (state) => state.globalState.confirmDialogMessage, + ); + const promiseId = useAppSelector( + (state) => state.globalState.confirmPromiseId, + ); + + const onClose = useCallback(() => { + dispatch(closeConfirmDialog()); + if (promiseId) { + generalDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback(() => { + dispatch(closeConfirmDialog()); + if (promiseId) { + generalDialogPromisePool[promiseId]?.resolve(); + } + }, [promiseId]); + + return ( + + + + {message} + + + + ); +}; +export default Confirmation; diff --git a/src/component/Dialogs/DialogAccordion.tsx b/src/component/Dialogs/DialogAccordion.tsx new file mode 100644 index 0000000..11b63a5 --- /dev/null +++ b/src/component/Dialogs/DialogAccordion.tsx @@ -0,0 +1,92 @@ +import { AccordionDetailsProps, Box, styled } from "@mui/material"; +import MuiAccordion, { AccordionProps } from "@mui/material/Accordion"; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from "@mui/material/AccordionSummary"; +import MuiAccordionDetails from "@mui/material/AccordionDetails"; +import { useState } from "react"; +import { CaretDownIcon } from "../FileManager/TreeView/TreeFile.tsx"; +import { DefaultButton } from "../Common/StyledComponents.tsx"; + +const Accordion = styled((props: AccordionProps) => ( + +))(({ theme, expanded }) => ({ + borderRadius: theme.shape.borderRadius, + backgroundColor: expanded + ? theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)" + : "initial", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&::before": { + display: "none", + }, +})); + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + +))(() => ({ + flexDirection: "row-reverse", + minHeight: 0, + padding: 0, + "& .MuiAccordionSummary-content": { + margin: 0, + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), +})); + +const SummaryButton = styled(DefaultButton)<{ expanded: boolean }>( + ({ theme, expanded }) => ({ + justifyContent: "flex-start", + backgroundColor: expanded + ? "initial" + : theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)", + "&:hover": { + backgroundColor: + theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.09)" + : "rgba(255, 255, 255, 0.13)", + }, + }), +); + +export interface DialogAccordionProps { + children?: React.ReactNode; + defaultExpanded?: boolean; + title: string; + accordionDetailProps?: AccordionDetailsProps; +} + +const DialogAccordion = (props: DialogAccordionProps) => { + const [expanded, setExpanded] = useState(!!props.defaultExpanded); + const handleChange = (_event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded); + }; + return ( + + + + } + > + {props.title} + + + + {props.children} + + + + ); +}; + +export default DialogAccordion; diff --git a/src/component/Dialogs/DraggableDialog.tsx b/src/component/Dialogs/DraggableDialog.tsx new file mode 100644 index 0000000..f561de0 --- /dev/null +++ b/src/component/Dialogs/DraggableDialog.tsx @@ -0,0 +1,117 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContentText, + DialogProps, + DialogTitle, + IconButton, + Paper, + PaperProps, + Stack, + styled, + useMediaQuery, +} from "@mui/material"; + +import { LoadingButton } from "@mui/lab"; +import { useCallback } from "react"; +import Draggable from "react-draggable"; +import { useTranslation } from "react-i18next"; +import Dismiss from "../Icons/Dismiss.tsx"; + +function PaperComponent(props: PaperProps) { + return ( + + + + ); +} + +export const StyledDialogActions = styled(DialogActions)<{ + denseAction?: boolean; +}>(({ theme, denseAction }) => ({ + padding: `${theme.spacing(denseAction ? 0.5 : 2)} ${theme.spacing(3)}`, + justifyContent: "space-between", +})); + +export const StyledDialogContentText = styled(DialogContentText)(({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + wordBreak: "break-all", +})); + +export const StyledDialogTitle = styled(DialogTitle)<{ moveable?: boolean }>(({ moveable }) => ({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + cursor: moveable ? "move" : "initial", +})); + +export interface DraggableDialogProps { + dialogProps: DialogProps; + children?: React.ReactNode; + secondaryAction?: React.ReactNode; + showActions?: boolean; + showCancel?: boolean; + hideOk?: boolean; + okText?: string; + cancelText?: string; + title?: string | React.ReactNode; + onAccept?: () => void; + loading?: boolean; + disabled?: boolean; + denseAction?: boolean; + secondaryFullWidth?: boolean; +} + +const DraggableDialog = (props: DraggableDialogProps) => { + const { t } = useTranslation(); + const isTouch = useMediaQuery("(pointer: coarse)"); + const onClose = useCallback(() => { + props.dialogProps.onClose && props.dialogProps.onClose({}, "backdropClick"); + }, [props.dialogProps.onClose]); + return ( + + {props.title != undefined && ( + + + {props.title} + + + + + + )} + {props.children} + {props.showActions && ( + + {props.secondaryAction} + + {props.showCancel && ( + + )} + {!props.hideOk && ( + + {t(props.okText ?? "common:ok")} + + )} + + + )} + + ); +}; + +export default DraggableDialog; diff --git a/src/component/Dialogs/GlobalDialogs.tsx b/src/component/Dialogs/GlobalDialogs.tsx new file mode 100644 index 0000000..1a53e91 --- /dev/null +++ b/src/component/Dialogs/GlobalDialogs.tsx @@ -0,0 +1,20 @@ +import { useAppSelector } from "../../redux/hooks.ts"; +import PinToSidebar from "../FileManager/Dialogs/PinToSidebar.tsx"; +import BatchDownloadLog from "./BatchDownloadLog.tsx"; +import Confirmation from "./Confirmation.tsx"; +import SelectOption from "./SelectOption.tsx"; + +const GlobalDialogs = () => { + const selectOptionOpen = useAppSelector((state) => state.globalState.selectOptionDialogOpen); + const batchDownloadLogOpen = useAppSelector((state) => state.globalState.batchDownloadLogDialogOpen); + return ( + <> + + + {batchDownloadLogOpen != undefined && } + {selectOptionOpen != undefined && } + + ); +}; + +export default GlobalDialogs; diff --git a/src/component/Dialogs/SelectOption.tsx b/src/component/Dialogs/SelectOption.tsx new file mode 100644 index 0000000..3b036bc --- /dev/null +++ b/src/component/Dialogs/SelectOption.tsx @@ -0,0 +1,77 @@ +import { useTranslation } from "react-i18next"; +import { + DialogContent, + List, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import React, { useCallback } from "react"; +import DraggableDialog from "./DraggableDialog.tsx"; +import { selectOptionDialogPromisePool } from "../../redux/thunks/dialog.ts"; +import { closeSelectOptionDialog } from "../../redux/globalStateSlice.ts"; + +const SelectOption = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const open = useAppSelector( + (state) => state.globalState.selectOptionDialogOpen, + ); + const title = useAppSelector((state) => state.globalState.selectOptionTitle); + const promiseId = useAppSelector( + (state) => state.globalState.selectOptionPromiseId, + ); + const options = useAppSelector( + (state) => state.globalState.selectOptionDialogOptions, + ); + + const onClose = useCallback(() => { + dispatch(closeSelectOptionDialog()); + if (promiseId) { + selectOptionDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback( + (v: any) => { + dispatch(closeSelectOptionDialog()); + if (promiseId) { + selectOptionDialogPromisePool[promiseId]?.resolve(v); + } + }, + [promiseId], + ); + + return ( + + + + {options?.map((o) => ( + onAccept(o.value)}> + + + ))} + + + + ); +}; +export default SelectOption; diff --git a/src/component/Download/Download.js b/src/component/Download/Download.js deleted file mode 100644 index 9d84960..0000000 --- a/src/component/Download/Download.js +++ /dev/null @@ -1,229 +0,0 @@ -import { Button, IconButton, Typography, withStyles } from "@material-ui/core"; -import RefreshIcon from "@material-ui/icons/Refresh"; -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { toggleSnackbar } from "../../redux/explorer"; -import API from "../../middleware/Api"; -import DownloadingCard from "./DownloadingCard"; -import FinishedCard from "./FinishedCard"; -import RemoteDownloadButton from "../Dial/Aria2"; -import Auth from "../../middleware/Auth"; -import Nothing from "../Placeholder/Nothing"; -import { withTranslation } from "react-i18next"; - -const styles = (theme) => ({ - actions: { - display: "flex", - }, - title: { - marginTop: "20px", - }, - layout: { - width: "auto", - marginTop: "30px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 700, - marginLeft: "auto", - marginRight: "auto", - }, - }, - shareTitle: { - maxWidth: "200px", - }, - avatarFile: { - backgroundColor: theme.palette.primary.light, - }, - avatarFolder: { - backgroundColor: theme.palette.secondary.light, - }, - gird: { - marginTop: "30px", - }, - hide: { - display: "none", - }, - loadingAnimation: { - borderRadius: "6px 6px 0 0", - }, - shareFix: { - marginLeft: "20px", - }, - loadMore: { - textAlign: "center", - marginTop: "20px", - marginBottom: "20px", - }, - margin: { - marginTop: theme.spacing(2), - }, -}); -const mapStateToProps = () => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -class DownloadComponent extends Component { - page = 0; - interval = 0; - previousDownloading = -1; - - state = { - downloading: [], - loading: false, - finishedList: [], - continue: true, - }; - - componentDidMount = () => { - this.loadDownloading(); - }; - - componentWillUnmount() { - clearTimeout(this.interval); - } - - loadDownloading = () => { - this.setState({ - loading: true, - }); - API.get("/aria2/downloading") - .then((response) => { - this.setState({ - downloading: response.data, - loading: false, - }); - // 设定自动更新 - clearTimeout(this.interval); - if (response.data.length > 0) { - this.interval = setTimeout( - this.loadDownloading, - 1000 * - response.data.reduce(function (prev, current) { - return prev.interval < current.interval - ? prev - : current; - }).interval - ); - } - - // 下载中条目变更时刷新已完成列表 - if (response.data.length !== this.previousDownloading) { - this.page = 0; - this.setState({ - finishedList: [], - continue: true, - }); - this.loadMore(); - } - this.previousDownloading = response.data.length; - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - loadMore = () => { - this.setState({ - loading: true, - }); - API.get("/aria2/finished?page=" + ++this.page) - .then((response) => { - this.setState({ - finishedList: [ - ...this.state.finishedList, - ...response.data, - ], - loading: false, - continue: response.data.length >= 10, - }); - }) - .catch(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("download.failedToLoad"), - "error" - ); - this.setState({ - loading: false, - }); - }); - }; - - render() { - const { classes, t } = this.props; - const user = Auth.GetUser(); - - return ( -
- {user.group.allowRemoteDownload && } - - {t("download.active")} - - - - - {this.state.downloading.length === 0 && ( - - )} - {this.state.downloading.map((value, k) => ( - - ))} - - {t("download.finished")} - -
- {this.state.finishedList.length === 0 && ( - - )} - {this.state.finishedList.map((value, k) => { - if (value.files) { - return ; - } - return null; - })} - -
-
- ); - } -} - -const Download = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withTranslation()(DownloadComponent))); - -export default Download; diff --git a/src/component/Download/DownloadingCard.js b/src/component/Download/DownloadingCard.js deleted file mode 100644 index db04320..0000000 --- a/src/component/Download/DownloadingCard.js +++ /dev/null @@ -1,761 +0,0 @@ -import { - Card, - CardContent, - darken, - IconButton, - lighten, - LinearProgress, - makeStyles, - Typography, - useTheme, -} from "@material-ui/core"; -import Badge from "@material-ui/core/Badge"; -import Button from "@material-ui/core/Button"; -import Divider from "@material-ui/core/Divider"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import Grid from "@material-ui/core/Grid"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableRow from "@material-ui/core/TableRow"; -import TableCell from "@material-ui/core/TableCell"; -import Tooltip from "@material-ui/core/Tooltip"; -import { ExpandMore, HighlightOff } from "@material-ui/icons"; -import PermMediaIcon from "@material-ui/icons/PermMedia"; -import classNames from "classnames"; -import React, { useCallback, useEffect, useMemo } from "react"; -import { useDispatch } from "react-redux"; -import TimeAgo from "timeago-react"; -import { toggleSnackbar } from "../../redux/explorer"; -import API from "../../middleware/Api"; -import { hex2bin, sizeToString } from "../../utils"; -import TypeIcon from "../FileManager/TypeIcon"; -import SelectFileDialog from "../Modals/SelectFile"; -import { useHistory } from "react-router"; -import { TableVirtuoso } from "react-virtuoso"; -import { useTranslation } from "react-i18next"; - -const ExpansionPanel = withStyles({ - root: { - maxWidth: "100%", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": {}, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles({ - root: { - minHeight: 0, - padding: 0, - - "&$expanded": { - minHeight: 56, - }, - }, - content: { - maxWidth: "100%", - margin: 0, - display: "flex", - "&$expanded": { - margin: "0", - }, - }, - expanded: {}, -})(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - display: "block", - padding: theme.spacing(0), - }, -}))(MuiExpansionPanelDetails); - -const useStyles = makeStyles((theme) => ({ - card: { - marginTop: "20px", - justifyContent: "space-between", - }, - iconContainer: { - width: "90px", - height: "96px", - padding: " 35px 29px 29px 29px", - paddingLeft: "35px", - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - content: { - width: "100%", - minWidth: 0, - [theme.breakpoints.up("sm")]: { - borderInlineStart: "1px " + theme.palette.divider + " solid", - }, - }, - contentSide: { - minWidth: 0, - paddingTop: "24px", - paddingRight: "28px", - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - iconBig: { - fontSize: "30px", - }, - iconMultiple: { - fontSize: "30px", - color: "#607D8B", - }, - progress: { - marginTop: 8, - marginBottom: 4, - }, - expand: { - transition: ".15s transform ease-in-out", - }, - expanded: { - transform: "rotate(180deg)", - }, - subFile: { - width: "100%", - minWidth: 300, - wordBreak: "break-all", - }, - subFileName: { - display: "flex", - }, - subFileIcon: { - marginRight: "20px", - }, - subFileSize: { - minWidth: 120, - }, - subFilePercent: { - minWidth: 105, - }, - scroll: { - overflow: "auto", - maxHeight: "300px", - }, - action: { - padding: theme.spacing(2), - textAlign: "right", - }, - actionButton: { - marginLeft: theme.spacing(1), - }, - info: { - padding: theme.spacing(2), - }, - infoTitle: { - fontWeight: 700, - textAlign: "left", - }, - infoValue: { - color: theme.palette.text.secondary, - textAlign: "left", - paddingLeft:theme.spacing(1), - }, - bitmap: { - width: "100%", - height: "50px", - backgroundColor: theme.palette.background.default, - }, -})); - -export default function DownloadingCard(props) { - const { t } = useTranslation("application", { keyPrefix: "download" }); - const { t: tGlobal } = useTranslation(); - const canvasRef = React.createRef(); - const classes = useStyles(); - const theme = useTheme(); - const history = useHistory(); - - const [expanded, setExpanded] = React.useState(""); - const [task, setTask] = React.useState(props.task); - const [loading, setLoading] = React.useState(false); - const [selectDialogOpen, setSelectDialogOpen] = React.useState(false); - const [selectFileOption, setSelectFileOption] = React.useState([]); - - const handleChange = (panel) => (event, newExpanded) => { - setExpanded(newExpanded ? panel : false); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - setTask(props.task); - }, [props.task]); - - useEffect(() => { - if (task.info.bitfield === "") { - return; - } - let result = ""; - task.info.bitfield.match(/.{1,2}/g).forEach((str) => { - result += hex2bin(str); - }); - const canvas = canvasRef.current; - const context = canvas.getContext("2d"); - context.clearRect(0, 0, canvas.width, canvas.height); - context.strokeStyle = theme.palette.primary.main; - for (let i = 0; i < canvas.width; i++) { - let bit = - result[ - Math.round(((i + 1) / canvas.width) * task.info.numPieces) - ]; - bit = bit ? bit : result.slice(-1); - if (bit === "1") { - context.beginPath(); - context.moveTo(i, 0); - context.lineTo(i, canvas.height); - context.stroke(); - } - } - // eslint-disable-next-line - }, [task.info.bitfield, task.info.numPieces, theme]); - - const getPercent = (completed, total) => { - if (total === 0) { - return 0; - } - return (completed / total) * 100; - }; - - const activeFiles = useCallback(() => { - return task.info.files.filter((v) => v.selected === "true"); - }, [task.info.files]); - - const deleteFile = (index) => { - setLoading(true); - const current = activeFiles(); - const newIndex = []; - const newFiles = []; - // eslint-disable-next-line - current.map((v) => { - if (v.index !== index && v.selected) { - newIndex.push(parseInt(v.index)); - newFiles.push({ - ...v, - selected: "true", - }); - } else { - newFiles.push({ - ...v, - selected: "false", - }); - } - }); - API.put("/aria2/select/" + task.info.gid, { - indexes: newIndex, - }) - .then(() => { - setTask({ - ...task, - info: { - ...task.info, - files: newFiles, - }, - }); - ToggleSnackbar("top", "right", t("taskFileDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const getDownloadName = useCallback(() => { - if (task.info.bittorrent.info.name !== "") { - return task.info.bittorrent.info.name; - } - return task.name === "." ? t("unknownTaskName") : task.name; - }, [task]); - - const getIcon = useCallback(() => { - if (task.info.bittorrent.mode === "multi") { - return ( - - - - ); - } else { - return ( - - ); - } - // eslint-disable-next-line - }, [task, classes]); - - const cancel = () => { - setLoading(true); - API.delete("/aria2/task/" + task.info.gid) - .then(() => { - ToggleSnackbar("top", "right", t("taskCanceled"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const changeSelectedFile = (fileIndex) => { - setLoading(true); - API.put("/aria2/select/" + task.info.gid, { - indexes: fileIndex, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - t("operationSubmitted"), - "success" - ); - setSelectDialogOpen(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const subFileList = useMemo(() => { - const processStyle = (value) => ({ - background: - "linear-gradient(to right, " + - (theme.palette.type === - "dark" - ? darken( - theme.palette - .primary - .main, - 0.4 - ) - : lighten( - theme.palette - .primary - .main, - 0.85 - )) + - " 0%," + - (theme.palette.type === - "dark" - ? darken( - theme.palette - .primary - .main, - 0.4 - ) - : lighten( - theme.palette - .primary - .main, - 0.85 - )) + - " " + - getPercent( - value.completedLength, - value.length - ).toFixed(0) + - "%," + - theme.palette.background - .paper + - " " + - getPercent( - value.completedLength, - value.length - ).toFixed(0) + - "%," + - theme.palette.background - .paper + - " 100%)", - }); - - const subFileCell = (value) => ( - <> - - - - {value.path} - - - - - {" "} - {sizeToString( - value.length - )} - - - - - {getPercent( - value.completedLength, - value.length - ).toFixed(2)} - % - - - - - - deleteFile( - value.index - ) - } - disabled={loading} - size={"small"} - > - - - - - - ); - - return activeFiles().length > 5 ? ( - , - // eslint-disable-next-line react/display-name - TableRow: (props) => { - const index = props["data-index"]; - const value = activeFiles()[index]; - return ( - - ); - }, - }} - data={activeFiles()} - itemContent={(index, value) => ( - subFileCell(value) - )} - /> - ) : ( -
-
- - {activeFiles().map((value) => { - return ( - - {subFileCell(value)} - - ); - })} - -
-
- ); - }, [ - classes, - theme, - activeFiles, - ]); - - return ( - - setSelectDialogOpen(false)} - modalsLoading={loading} - files={selectFileOption} - onSubmit={changeSelectedFile} - /> - - -
{getIcon()}
- - - - {getDownloadName()} - - - - - {task.total > 0 && ( - - {getPercent( - task.downloaded, - task.total - ).toFixed(2)} - % -{" "} - {task.downloaded === 0 - ? "0Bytes" - : sizeToString(task.downloaded)} - / - {task.total === 0 - ? "0Bytes" - : sizeToString(task.total)}{" "} - -{" "} - {task.speed === "0" - ? "0B/s" - : sizeToString(task.speed) + "/s"} - - )} - {task.total === 0 && - } - - - - - - - -
- - - {task.info.bittorrent.mode === "multi" && subFileList} -
- - {task.info.bittorrent.mode === "multi" && ( - - )} - -
- -
- {task.info.bitfield !== "" && ( - - )} - - - - - {t("updatedAt")} - - - - - - - - {t("uploaded")} - - - {sizeToString(task.info.uploadLength)} - - - - - {t("uploadSpeed")} - - - {sizeToString(task.info.uploadSpeed)} / s - - - {task.info.bittorrent.mode !== "" && ( - <> - - - {t("InfoHash")} - - - {task.info.infoHash} - - - - - {t("seederCount")} - - - {task.info.numSeeders} - - - - - {t("seeding")} - - - {task.info.seeder === "true" - ? t("isSeeding") - : t("notSeeding")} - - - - )} - - - {t("chunkSize")} - - - {sizeToString(task.info.pieceLength)} - - - - - {t("chunkNumbers")} - - - {task.info.numPieces} - - - {props.task.node && - - {t("downloadNode")} - - - {props.task.node} - - } - -
-
-
-
- ); -} diff --git a/src/component/Download/FinishedCard.js b/src/component/Download/FinishedCard.js deleted file mode 100644 index 75f876a..0000000 --- a/src/component/Download/FinishedCard.js +++ /dev/null @@ -1,489 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { - Card, - CardContent, - IconButton, - makeStyles, - Typography, - useTheme, -} from "@material-ui/core"; -import { sizeToString } from "../../utils"; -import PermMediaIcon from "@material-ui/icons/PermMedia"; -import TypeIcon from "../FileManager/TypeIcon"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import withStyles from "@material-ui/core/styles/withStyles"; -import Divider from "@material-ui/core/Divider"; -import { ExpandMore } from "@material-ui/icons"; -import classNames from "classnames"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableRow from "@material-ui/core/TableRow"; -import TableCell from "@material-ui/core/TableCell"; -import Badge from "@material-ui/core/Badge"; -import Tooltip from "@material-ui/core/Tooltip"; -import Button from "@material-ui/core/Button"; -import Grid from "@material-ui/core/Grid"; -import API from "../../middleware/Api"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { formatLocalTime } from "../../utils/datetime"; -import { toggleSnackbar } from "../../redux/explorer"; -import { TableVirtuoso } from "react-virtuoso"; -import { useTranslation } from "react-i18next"; - -const ExpansionPanel = withStyles({ - root: { - maxWidth: "100%", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": {}, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles({ - root: { - minHeight: 0, - padding: 0, - - "&$expanded": { - minHeight: 56, - }, - }, - content: { - maxWidth: "100%", - margin: 0, - display: "flex", - "&$expanded": { - margin: "0", - }, - }, - expanded: {}, -})(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - display: "block", - padding: theme.spacing(0), - }, -}))(MuiExpansionPanelDetails); - -const useStyles = makeStyles((theme) => ({ - card: { - marginTop: "20px", - justifyContent: "space-between", - }, - iconContainer: { - width: "90px", - height: "96px", - padding: " 35px 29px 29px 29px", - paddingLeft: "35px", - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - content: { - width: "100%", - minWidth: 0, - [theme.breakpoints.up("sm")]: { - borderInlineStart: "1px " + theme.palette.divider + " solid", - }, - textAlign: "left", - }, - contentSide: { - minWidth: 0, - paddingTop: "24px", - paddingRight: "28px", - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - iconBig: { - fontSize: "30px", - }, - iconMultiple: { - fontSize: "30px", - color: "#607D8B", - }, - progress: { - marginTop: 8, - marginBottom: 4, - }, - expand: { - transition: ".15s transform ease-in-out", - }, - expanded: { - transform: "rotate(180deg)", - }, - subFile: { - width: "100%", - minWidth: 300, - wordBreak: "break-all", - }, - subFileName: { - display: "flex", - }, - subFileIcon: { - marginRight: "20px", - }, - subFileSize: { - minWidth: 115, - }, - subFilePercent: { - minWidth: 100, - }, - scroll: { - overflow: "auto", - maxHeight: "300px", - }, - action: { - padding: theme.spacing(2), - textAlign: "right", - }, - actionButton: { - marginLeft: theme.spacing(1), - }, - info: { - padding: theme.spacing(2), - }, - infoTitle: { - fontWeight: 700, - textAlign: "left", - }, - infoValue: { - color: theme.palette.text.secondary, - textAlign: "left", - paddingLeft: theme.spacing(1), - }, -})); - -export default function FinishedCard(props) { - const { t } = useTranslation("application", { keyPrefix: "download" }); - const classes = useStyles(); - const theme = useTheme(); - const history = useHistory(); - - const [expanded, setExpanded] = React.useState(false); - const [loading, setLoading] = React.useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const handleChange = () => (event, newExpanded) => { - setExpanded(!!newExpanded); - }; - - const getPercent = (completed, total) => { - if (total === 0) { - return 0; - } - return (completed / total) * 100; - }; - - const cancel = () => { - setLoading(true); - API.delete("/aria2/task/" + props.task.gid) - .then(() => { - ToggleSnackbar("top", "right", t("taskDeleted"), "success"); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - window.location.reload(); - }); - }; - - const getDownloadName = useCallback(() => { - return props.task.name === "." ? t("unknownTaskName") : props.task.name; - }, [props.task.name]); - - const activeFiles = useCallback(() => { - return props.task.files.filter((v) => v.selected === "true"); - }, [props.task.files]); - - const getIcon = useCallback(() => { - if (props.task.files.length > 1) { - return ( - - - - ); - } else { - return ( - - ); - } - }, [props.task, classes]); - - const getTaskError = (error) => { - try { - const res = JSON.parse(error); - return res.msg + ":" + res.error; - } catch (e) { - return t("transferFailed"); - } - }; - - const subFileList = useMemo(() => { - const subFileCell = (value) => ( - <> - - - - {value.path} - - - - - {" "} - {sizeToString(value.length)} - - - - - {getPercent( - value.completedLength, - value.length - ).toFixed(2)} - % - - - - ); - - return activeFiles().length > 5 ? ( - , - }} - data={activeFiles()} - itemContent={(index, value) => subFileCell(value)} - /> - ) : ( -
-
- - {activeFiles().map((value) => { - return ( - - {subFileCell(value)} - - ); - })} - -
-
- ); - }, [classes, activeFiles]); - - return ( - - - -
{getIcon()}
- - - - {getDownloadName()} - - - {props.task.status === 3 && ( - - - {t("downloadFailed", { - msg: props.task.error, - })} - - - )} - {props.task.status === 5 && ( - - {t("canceledStatus")} - {props.task.error !== "" && ( - ({props.task.error}) - )} - - )} - {props.task.status === 4 && - props.task.task_status === 4 && ( - - {t("finishedStatus")} - - )} - {props.task.status === 4 && - props.task.task_status === 0 && ( - - {t("pending")} - - )} - {props.task.status === 4 && - props.task.task_status === 1 && ( - - {t("transferring")} - - )} - {props.task.status === 4 && - props.task.task_status === 2 && ( - - - {getTaskError(props.task.task_error)} - - - )} - - - - - - -
- - - {props.task.files.length > 1 && subFileList} -
- - -
- -
- - - - {t("createdAt")} - - - {formatLocalTime(props.task.create)} - - - - - {t("updatedAt")} - - - {formatLocalTime(props.task.update)} - - - {props.task.node && ( - - - {t("downloadNode")} - - - {props.task.node} - - - )} - -
-
-
-
- ); -} diff --git a/src/component/FileManager/ContextMenu.js b/src/component/FileManager/ContextMenu.js deleted file mode 100644 index 2a7b3fa..0000000 --- a/src/component/FileManager/ContextMenu.js +++ /dev/null @@ -1,733 +0,0 @@ -import { - Divider, - ListItemIcon, - MenuItem, - Typography, - withStyles, -} from "@material-ui/core"; -import Menu from "@material-ui/core/Menu"; -import { Archive, InfoOutlined, Unarchive } from "@material-ui/icons"; -import RenameIcon from "@material-ui/icons/BorderColor"; -import DownloadIcon from "@material-ui/icons/CloudDownload"; -import UploadIcon from "@material-ui/icons/CloudUpload"; -import NewFolderIcon from "@material-ui/icons/CreateNewFolder"; -import DeleteIcon from "@material-ui/icons/Delete"; -import FileCopyIcon from "@material-ui/icons/FileCopy"; -import OpenFolderIcon from "@material-ui/icons/FolderOpen"; -import MoveIcon from "@material-ui/icons/Input"; -import LinkIcon from "@material-ui/icons/InsertLink"; -import OpenIcon from "@material-ui/icons/OpenInNew"; -import ShareIcon from "@material-ui/icons/Share"; -import { - FolderDownload, - FolderUpload, - MagnetOn, - FilePlus, -} from "mdi-material-ui"; -import PropTypes from "prop-types"; -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import { isCompressFile, isPreviewable, isTorrent } from "../../config"; -import Auth from "../../middleware/Auth"; -import pathHelper from "../../utils/page"; -import RefreshIcon from "@material-ui/icons/Refresh"; -import { - batchGetSource, - openParentFolder, - openPreview, - openTorrentDownload, - setSelectedTarget, - startBatchDownload, - startDirectoryDownload, - startDownload, - toggleObjectInfoSidebar, -} from "../../redux/explorer/action"; -import { - changeContextMenu, - navigateTo, - openCompressDialog, - openCopyDialog, - openCreateFileDialog, - openCreateFolderDialog, - openDecompressDialog, - openLoadingDialog, - openMoveDialog, - openMusicDialog, - openRemoteDownloadDialog, - openRemoveDialog, - openRenameDialog, - openShareDialog, - refreshFileList, - setNavigatorLoadingStatus, - showImgPreivew, - toggleSnackbar, -} from "../../redux/explorer"; -import { pathJoin } from "../Uploader/core/utils"; -import { - openFileSelector, - openFolderSelector, -} from "../../redux/viewUpdate/action"; -import { withTranslation } from "react-i18next"; - -const styles = () => ({ - propover: {}, - divider: { - marginTop: 4, - marginBottom: 4, - }, -}); - -const StyledListItemIcon = withStyles({ - root: { - minWidth: 38, - }, -})(ListItemIcon); - -const mapStateToProps = (state) => { - return { - menuType: state.viewUpdate.contextType, - menuOpen: state.viewUpdate.contextOpen, - isMultiple: state.explorer.selectProps.isMultiple, - withFolder: state.explorer.selectProps.withFolder, - withFile: state.explorer.selectProps.withFile, - withSourceEnabled: state.explorer.selectProps.withSourceEnabled, - path: state.navigator.path, - selected: state.explorer.selected, - search: state.explorer.search, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - changeContextMenu: (type, open) => { - dispatch(changeContextMenu(type, open)); - }, - setNavigatorLoadingStatus: (status) => { - dispatch(setNavigatorLoadingStatus(status)); - }, - setSelectedTarget: (targets) => { - dispatch(setSelectedTarget(targets)); - }, - navigateTo: (path) => { - dispatch(navigateTo(path)); - }, - openCreateFolderDialog: () => { - dispatch(openCreateFolderDialog()); - }, - openCreateFileDialog: () => { - dispatch(openCreateFileDialog()); - }, - openRenameDialog: () => { - dispatch(openRenameDialog()); - }, - openMoveDialog: () => { - dispatch(openMoveDialog()); - }, - openRemoveDialog: () => { - dispatch(openRemoveDialog()); - }, - openShareDialog: () => { - dispatch(openShareDialog()); - }, - showImgPreivew: (first) => { - dispatch(showImgPreivew(first)); - }, - openMusicDialog: () => { - dispatch(openMusicDialog()); - }, - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - openRemoteDownloadDialog: () => { - dispatch(openRemoteDownloadDialog()); - }, - openTorrentDownloadDialog: () => { - dispatch(openTorrentDownload()); - }, - openCopyDialog: () => { - dispatch(openCopyDialog()); - }, - openLoadingDialog: (text) => { - dispatch(openLoadingDialog(text)); - }, - openDecompressDialog: () => { - dispatch(openDecompressDialog()); - }, - openCompressDialog: () => { - dispatch(openCompressDialog()); - }, - refreshFileList: () => { - dispatch(refreshFileList()); - }, - openPreview: (share) => { - dispatch(openPreview(share)); - }, - toggleObjectInfoSidebar: (open) => { - dispatch(toggleObjectInfoSidebar(open)); - }, - startBatchDownload: (share) => { - dispatch(startBatchDownload(share)); - }, - openFileSelector: () => { - dispatch(openFileSelector()); - }, - openFolderSelector: () => { - dispatch(openFolderSelector()); - }, - startDownload: (share, file) => { - dispatch(startDownload(share, file)); - }, - batchGetSource: () => { - dispatch(batchGetSource()); - }, - startDirectoryDownload: (share) => { - dispatch(startDirectoryDownload(share)); - }, - openParentFolder: () => { - dispatch(openParentFolder()); - }, - }; -}; - -class ContextMenuCompoment extends Component { - X = 0; - Y = 0; - - state = {}; - - componentDidMount = () => { - window.document.addEventListener("mousemove", this.setPoint); - }; - - setPoint = (e) => { - this.Y = e.clientY; - this.X = e.clientX; - }; - - openArchiveDownload = () => { - this.props.startBatchDownload(this.props.share); - }; - - openDirectoryDownload = () => { - this.props.startDirectoryDownload(this.props.share); - }; - - openDownload = () => { - this.props.startDownload(this.props.share, this.props.selected[0]); - }; - - enterFolder = () => { - this.props.navigateTo( - pathJoin([this.props.path, this.props.selected[0].name]) - ); - }; - - // 暂时只对空白处右键菜单使用这个函数,疑似有bug会导致的一个菜单被默认选中。 - // 相关issue: https://github.com/mui-org/material-ui/issues/23747 - renderMenuItems = (items) => { - const res = []; - let key = 0; - - ["top", "center", "bottom"].forEach((position) => { - let visibleCount = 0; - items[position].forEach((item) => { - if (item.condition) { - res.push( - - {item.icon} - - {item.text} - - - ); - key++; - visibleCount++; - } - }); - if (visibleCount > 0 && position != "bottom") { - res.push( - - ); - key++; - } - }); - - return res; - }; - - render() { - const { classes, t } = this.props; - const user = Auth.GetUser(); - const isHomePage = pathHelper.isHomePage(this.props.location.pathname); - const emptyMenuList = { - top: [ - { - condition: true, - onClick: () => { - this.props.refreshFileList(); - this.props.changeContextMenu( - this.props.menuType, - false - ); - }, - icon: , - text: "刷新", - }, - ], - center: [ - { - condition: true, - onClick: () => this.props.openFileSelector(), - icon: , - text: "上传文件", - }, - { - condition: true, - onClick: () => this.props.openFolderSelector(), - icon: , - text: "上传目录", - }, - { - condition: user.group.allowRemoteDownload, - onClick: () => this.props.openRemoteDownloadDialog(), - icon: , - text: "离线下载", - }, - ], - bottom: [ - { - condition: true, - onClick: () => this.props.openCreateFolderDialog(), - icon: , - text: "创建文件夹", - }, - { - condition: true, - onClick: () => this.props.openCreateFileDialog(), - icon: , - text: "创建文件", - }, - ], - }; - - return ( -
- - this.props.changeContextMenu(this.props.menuType, false) - } - anchorReference="anchorPosition" - anchorPosition={{ top: this.Y, left: this.X }} - anchorOrigin={{ - vertical: "top", - horizontal: "left", - }} - transformOrigin={{ - vertical: "top", - horizontal: "left", - }} - > - {this.props.menuType === "empty" && ( -
- { - this.props.refreshFileList(); - this.props.changeContextMenu( - this.props.menuType, - false - ); - }} - > - - - - - {t("fileManager.refresh")} - - - - this.props.openFileSelector()} - > - - - - - {t("fileManager.uploadFiles")} - - - this.props.openFolderSelector()} - > - - - - - {t("fileManager.uploadFolder")} - - - {user.group.allowRemoteDownload && ( - - this.props.openRemoteDownloadDialog() - } - > - - - - - {t("fileManager.newRemoteDownloads")} - - - )} - - - - this.props.openCreateFolderDialog() - } - > - - - - - {t("fileManager.newFolder")} - - - - this.props.openCreateFileDialog() - } - > - - - - - {t("fileManager.newFile")} - - -
- )} - {this.props.menuType !== "empty" && ( -
- {!this.props.isMultiple && this.props.withFolder && ( -
- - - - - - {t("fileManager.enter")} - - - {isHomePage && ( - - )} -
- )} - {!this.props.isMultiple && - this.props.withFile && - (!this.props.share || - this.props.share.preview) && - isPreviewable(this.props.selected[0].name) && ( -
- - this.props.openPreview() - } - > - - - - - {t("fileManager.open")} - - -
- )} - - {this.props.search && !this.props.isMultiple && ( -
- - this.props.openParentFolder() - } - > - - - - - {t("fileManager.openParentFolder")} - - -
- )} - - {!this.props.isMultiple && this.props.withFile && ( -
- - this.openDownload(this.props.share) - } - > - - - - - {t("fileManager.download")} - - - {isHomePage && ( - - )} -
- )} - - {(this.props.isMultiple || this.props.withFolder) && - window.showDirectoryPicker && - window.isSecureContext && ( - - this.openDirectoryDownload() - } - > - - - - - {t("fileManager.download")} - - - )} - - {(this.props.isMultiple || - this.props.withFolder) && ( - this.openArchiveDownload()} - > - - - - - {t("fileManager.batchDownload")} - - - )} - - {isHomePage && - user.group.sourceBatch > 0 && - this.props.withSourceEnabled && ( - - this.props.batchGetSource() - } - > - - - - - {this.props.isMultiple || - (this.props.withFolder && - !this.props.withFile) - ? t( - "fileManager.getSourceLinkInBatch" - ) - : t( - "fileManager.getSourceLink" - )} - - - )} - - {!this.props.isMultiple && - isHomePage && - user.group.allowRemoteDownload && - this.props.withFile && - isTorrent(this.props.selected[0].name) && ( - - this.props.openTorrentDownloadDialog() - } - > - - - - - {t( - "fileManager.createRemoteDownloadForTorrent" - )} - - - )} - {!this.props.isMultiple && - isHomePage && - user.group.compress && - this.props.withFile && - isCompressFile(this.props.selected[0].name) && ( - - this.props.openDecompressDialog() - } - > - - - - - {t("fileManager.decompress")} - - - )} - - {isHomePage && user.group.compress && ( - - this.props.openCompressDialog() - } - > - - - - - {t("fileManager.compress")} - - - )} - - {!this.props.isMultiple && isHomePage && ( - this.props.openShareDialog()} - > - - - - - {t("fileManager.createShareLink")} - - - )} - - {!this.props.isMultiple && isHomePage && ( - - this.props.toggleObjectInfoSidebar(true) - } - > - - - - - {t("fileManager.viewDetails")} - - - )} - - {!this.props.isMultiple && isHomePage && ( - - )} - - {!this.props.isMultiple && isHomePage && ( -
- - this.props.openRenameDialog() - } - > - - - - - {t("fileManager.rename")} - - - {!this.props.search && ( - - this.props.openCopyDialog() - } - > - - - - - {t("fileManager.copy")} - - - )} -
- )} - {isHomePage && ( -
- {!this.props.search && ( - - this.props.openMoveDialog() - } - > - - - - - {t("fileManager.move")} - - - )} - - - - this.props.openRemoveDialog() - } - > - - - - - {t("fileManager.delete")} - - -
- )} -
- )} -
-
- ); - } -} - -ContextMenuCompoment.propTypes = { - classes: PropTypes.object.isRequired, - menuType: PropTypes.string.isRequired, -}; - -const ContextMenu = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(ContextMenuCompoment)))); - -export default ContextMenu; diff --git a/src/component/FileManager/ContextMenu/CascadingMenu.tsx b/src/component/FileManager/ContextMenu/CascadingMenu.tsx new file mode 100644 index 0000000..2f31808 --- /dev/null +++ b/src/component/FileManager/ContextMenu/CascadingMenu.tsx @@ -0,0 +1,117 @@ +import { Box, ListItemIcon, ListItemText, Menu, MenuItemProps, styled, useMediaQuery, useTheme } from "@mui/material"; +import { bindFocus, bindHover } from "material-ui-popup-state"; +import { bindMenu, bindTrigger, PopupState, usePopupState } from "material-ui-popup-state/hooks"; +import { createContext, useCallback, useContext, useMemo } from "react"; +import CaretRight from "../../Icons/CaretRight.tsx"; +import { SquareMenuItem } from "./ContextMenu.tsx"; +import HoverMenu from "./HoverMenu.tsx"; + +export const CascadingContext = createContext<{ + parentPopupState?: PopupState; + rootPopupState?: PopupState; +}>({}); + +export const SquareHoverMenu = styled(HoverMenu)(() => ({ + "& .MuiPaper-root": { + minWidth: "200px", + }, +})); + +export const SquareMenu = styled(Menu)(() => ({ + "& .MuiPaper-root": { + minWidth: "200px", + }, +})); + +export interface CascadingMenuItem { + onClick?: (event: React.MouseEvent) => void; + [key: string]: any; +} + +export function CascadingMenuItem({ onClick, ...props }: CascadingMenuItem) { + const { rootPopupState } = useContext(CascadingContext); + if (!rootPopupState) throw new Error("must be used inside a CascadingMenu"); + const handleClick = useCallback( + (event: React.MouseEvent) => { + rootPopupState.close(); + if (onClick) onClick(event); + }, + [rootPopupState, onClick], + ); + + return ; +} + +export interface CascadingMenuProps { + popupState: PopupState; + isMobile?: boolean; + [key: string]: any; +} + +export function CascadingMenu({ popupState, isMobile, ...props }: CascadingMenuProps) { + const { rootPopupState } = useContext(CascadingContext); + const context = useMemo( + () => ({ + rootPopupState: rootPopupState || popupState, + parentPopupState: popupState, + }), + [rootPopupState, popupState], + ); + + const MenuComponent = isMobile ? SquareMenu : SquareHoverMenu; + + return ( + + + + ); +} + +export interface CascadingSubmenu { + title: string; + icon?: JSX.Element; + popupId: string; + menuItemProps?: MenuItemProps; + [key: string]: any; +} + +export function CascadingSubmenu({ title, popupId, menuItemProps, icon, ...props }: CascadingSubmenu) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { parentPopupState } = useContext(CascadingContext); + const popupState = usePopupState({ + popupId, + variant: "popover", + parentPopupState, + }); + return ( + <> + + {icon && {icon}} + + + + + + + + ); +} diff --git a/src/component/FileManager/ContextMenu/ContextMenu.tsx b/src/component/FileManager/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..ba87167 --- /dev/null +++ b/src/component/FileManager/ContextMenu/ContextMenu.tsx @@ -0,0 +1,507 @@ +import { + Box, + Divider, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + styled, + Typography, + useTheme, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { closeContextMenu } from "../../../redux/fileManagerSlice.ts"; +import useActionDisplayOpt from "./useActionDisplayOpt.ts"; +import { useCallback, useMemo } from "react"; +import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; +import { useTranslation } from "react-i18next"; +import { + batchGetDirectLinks, + createNew, + deleteFile, + dialogBasedMoveCopy, + enterFolder, + extractArchive, + goToParent, + goToSharedLink, + newRemoteDownload, + openShareDialog, + openSidebar, + renameFile, + restoreFile, +} from "../../../redux/thunks/file.ts"; +import RenameOutlined from "../../Icons/RenameOutlined.tsx"; +import BinFullOutlined from "../../Icons/BinFullOutlined.tsx"; +import { CascadingSubmenu } from "./CascadingMenu.tsx"; +import HistoryOutlined from "../../Icons/HistoryOutlined.tsx"; +import CopyOutlined from "../../Icons/CopyOutlined.tsx"; +import Tag from "../../Icons/Tag.tsx"; +import TagMenuItems from "./TagMenuItems.tsx"; +import Download from "../../Icons/Download.tsx"; +import ShareOutlined from "../../Icons/ShareOutlined.tsx"; +import FolderLink from "../../Icons/FolderLink.tsx"; +import { downloadFiles } from "../../../redux/thunks/download.ts"; +import Info from "../../Icons/Info.tsx"; +import WrenchSettings from "../../Icons/WrenchSettings.tsx"; +import Open from "../../Icons/Open.tsx"; +import { openViewers } from "../../../redux/thunks/viewer.ts"; +import AppFolder from "../../Icons/AppFolder.tsx"; +import OrganizeMenuItems from "./OrganizeMenuItems.tsx"; +import MoreMenuItems from "./MoreMenuItems.tsx"; +import OpenWithMenuItems from "./OpenWithMenuItems.tsx"; +import { + refreshFileList, + uploadClicked, + uploadFromClipboard, +} from "../../../redux/thunks/filemanager.ts"; +import ArrowSync from "../../Icons/ArrowSync.tsx"; +import FolderAdd from "../../Icons/FolderAdd.tsx"; +import { CreateNewDialogType } from "../../../redux/globalStateSlice.ts"; +import FileAdd from "../../Icons/FileAdd.tsx"; +import NewFileTemplateMenuItems from "./NewFileTemplateMenuItems.tsx"; +import Upload from "../../Icons/Upload.tsx"; +import FolderArrowUp from "../../Icons/FolderArrowUp.tsx"; +import { SelectType } from "../../Uploader/core"; +import Clipboard from "../../Icons/Clipboard.tsx"; +import ArchiveArrow from "../../Icons/ArchiveArrow.tsx"; +import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx"; +import Enter from "../../Icons/Enter.tsx"; +import FolderOutlined from "../../Icons/FolderOutlined.tsx"; +import LinkOutlined from "../../Icons/LinkOutlined.tsx"; + +export const SquareMenu = styled(Menu)(() => ({ + "& .MuiPaper-root": { + minWidth: "200px", + }, +})); + +export const SquareMenuItem = styled(MenuItem)<{ hoverColor?: string }>( + ({ theme, hoverColor }) => ({ + "&:hover .MuiListItemIcon-root": { + color: hoverColor ?? theme.palette.primary.main, + }, + }), +); + +export const DenseDivider = styled(Divider)(() => ({ + margin: "4px 0 !important", +})); + +export const EmptyMenu = () => { + const { t } = useTranslation(); + return ( + + + + {t("fileManager.noActionsCanBeDone")} + + + ); +}; + +export interface ContextMenuProps { + fmIndex: number; +} + +const ContextMenu = ({ fmIndex = 0 }: ContextMenuProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const contextMenuOpen = useAppSelector( + (state) => state.fileManager[fmIndex].contextMenuOpen, + ); + const contextMenuType = useAppSelector( + (state) => state.fileManager[fmIndex].contextMenuType, + ); + const contextMenuPos = useAppSelector( + (state) => state.fileManager[fmIndex].contextMenuPos, + ); + const selected = useAppSelector( + (state) => state.fileManager[fmIndex].selected, + ); + const targetOverwrite = useAppSelector( + (state) => state.fileManager[fmIndex].contextMenuTargets, + ); + + const targets = useMemo(() => { + const targetsMap = targetOverwrite ?? selected; + return Object.keys(targetsMap).map((key) => targetsMap[key]); + }, [targetOverwrite, selected]); + + const parent = useAppSelector( + (state) => state.fileManager[fmIndex].list?.parent, + ); + + const displayOpt = useActionDisplayOpt( + targets, + contextMenuType, + parent, + fmIndex, + ); + const onClose = useCallback(() => { + dispatch(closeContextMenu({ index: fmIndex, value: undefined })); + }, [dispatch]); + + const showOpenWith = displayOpt.showOpenWith && displayOpt.showOpenWith(); + let part1 = + displayOpt.showOpen || + showOpenWith || + displayOpt.showEnter || + displayOpt.showDownload || + displayOpt.showRemoteDownload || + displayOpt.showTorrentRemoteDownload || + displayOpt.showExtractArchive || + displayOpt.showUpload; + let part2 = + displayOpt.showCreateFolder || + displayOpt.showCreateFile || + displayOpt.showShare || + displayOpt.showRename || + displayOpt.showCopy || + displayOpt.showDirectLink; + let part3 = + displayOpt.showTags || + displayOpt.showOrganize || + displayOpt.showMore || + displayOpt.showNewFileFromTemplate; + let part4 = + displayOpt.showInfo || + displayOpt.showGoToParent || + displayOpt.showGoToSharedLink; + let part5 = + displayOpt.showRestore || displayOpt.showDelete || displayOpt.showRefresh; + const showDivider1 = part1 && part2; + const showDivider2 = part2 && part3; + const showDivider3 = part3 && part4; + const showDivider4 = part4 && part5; + + const part1Elements = part1 ? ( + <> + {displayOpt.showUpload && ( + dispatch(uploadClicked(0, SelectType.File))} + > + + + + + {t("application:fileManager.uploadFiles")} + + + )} + {displayOpt.showEnter && ( + dispatch(enterFolder(0, targets[0]))}> + + + + {t("application:fileManager.enter")} + + )} + {displayOpt.showUpload && ( + dispatch(uploadClicked(0, SelectType.Directory))} + > + + + + + {t("application:fileManager.uploadFolder")} + + + )} + {displayOpt.showUpload && ( + dispatch(uploadFromClipboard(0))}> + + + + + {t("application:uploader.uploadFromClipboard")} + + + )} + {displayOpt.showRemoteDownload && ( + dispatch(newRemoteDownload(0))}> + + + + + {t("application:fileManager.newRemoteDownloads")} + + + )} + {displayOpt.showOpen && ( + dispatch(openViewers(fmIndex, targets[0]))} + > + + + + {t("application:fileManager.open")} + + )} + {showOpenWith && ( + } + > + + + )} + {displayOpt.showDownload && ( + dispatch(downloadFiles(fmIndex, targets))} + > + + + + {t("application:fileManager.download")} + + )} + {displayOpt.showExtractArchive && ( + dispatch(extractArchive(fmIndex, targets[0]))} + > + + + + + {t("application:fileManager.extractArchive")} + + + )} + {displayOpt.showTorrentRemoteDownload && ( + dispatch(newRemoteDownload(fmIndex, targets[0]))} + > + + + + + {t("application:fileManager.createRemoteDownloadForTorrent")} + + + )} + + ) : undefined; + + const part2Elements = part2 ? ( + <> + {displayOpt.showCreateFolder && ( + + dispatch(createNew(fmIndex, CreateNewDialogType.folder)) + } + > + + + + {t("application:fileManager.newFolder")} + + )} + {displayOpt.showCreateFile && ( + dispatch(createNew(fmIndex, CreateNewDialogType.file))} + > + + + + {t("application:fileManager.newFile")} + + )} + {displayOpt.showShare && ( + dispatch(openShareDialog(fmIndex, targets[0]))} + > + + + + {t("application:fileManager.share")} + + )} + {displayOpt.showRename && ( + dispatch(renameFile(fmIndex, targets[0]))} + > + + + + {t("application:fileManager.rename")} + + )} + {displayOpt.showCopy && ( + dispatch(dialogBasedMoveCopy(fmIndex, targets, true))} + > + + + + {t("application:fileManager.copy")} + + )} + {displayOpt.showDirectLink && ( + dispatch(batchGetDirectLinks(fmIndex, targets))} + > + + + + + {t("application:fileManager.getSourceLink")} + + + )} + + ) : undefined; + + const part3Elements = part3 ? ( + <> + {displayOpt.showTags && ( + } + > + + + )} + {displayOpt.showOrganize && ( + } + > + + + )} + {displayOpt.showMore && ( + } + > + + + )} + {displayOpt.showNewFileFromTemplate && ( + + )} + + ) : undefined; + + const part4Elements = part4 ? ( + <> + {displayOpt.showGoToSharedLink && ( + dispatch(goToSharedLink(fmIndex, targets[0]))} + > + + + + + {t("application:fileManager.goToSharedLink")} + + + )} + {displayOpt.showGoToParent && ( + dispatch(goToParent(0, targets[0]))}> + + + + + {t("application:fileManager.openParentFolder")} + + + )} + {displayOpt.showInfo && ( + dispatch(openSidebar(fmIndex, targets[0]))} + > + + + + + {t("application:fileManager.viewDetails")} + + + )} + + ) : undefined; + + const part5Elements = part5 ? ( + <> + {displayOpt.showRestore && ( + dispatch(restoreFile(fmIndex, targets))}> + + + + {t("application:fileManager.restore")} + + )} + {displayOpt.showDelete && ( + dispatch(deleteFile(fmIndex, targets))} + > + + + + {t("application:fileManager.delete")} + + )} + {displayOpt.showRefresh && ( + dispatch(refreshFileList(fmIndex))}> + + + + {t("application:fileManager.refresh")} + + )} + + ) : undefined; + + const allParts = [ + part1Elements, + part2Elements, + part3Elements, + part4Elements, + part5Elements, + ].filter((p) => p != undefined); + + return ( + { + e.preventDefault(); + }, + }, + }} + > + {allParts.map((part, index) => ( + <> + {part} + {index < allParts.length - 1 && } + + ))} + {allParts.length == 0 && } + + ); +}; + +export default ContextMenu; diff --git a/src/component/FileManager/ContextMenu/HoverMenu.tsx b/src/component/FileManager/ContextMenu/HoverMenu.tsx new file mode 100644 index 0000000..d6a6665 --- /dev/null +++ b/src/component/FileManager/ContextMenu/HoverMenu.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { Menu, type MenuProps } from "@mui/material"; + +const HoverMenu: React.ComponentType = React.forwardRef( + function HoverMenu(props: MenuProps, ref): any { + return ( + + ); + }, +); + +export default HoverMenu; diff --git a/src/component/FileManager/ContextMenu/MoreMenuItems.tsx b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx new file mode 100644 index 0000000..8609ca0 --- /dev/null +++ b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx @@ -0,0 +1,93 @@ +import { useCallback, useContext } from "react"; +import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { closeContextMenu } from "../../../redux/fileManagerSlice.ts"; +import { + setCreateArchiveDialog, + setManageShareDialog, + setVersionControlDialog, +} from "../../../redux/globalStateSlice.ts"; +import { ListItemIcon, ListItemText } from "@mui/material"; +import HistoryOutlined from "../../Icons/HistoryOutlined.tsx"; +import LinkSetting from "../../Icons/LinkSetting.tsx"; +import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx"; +import Archive from "../../Icons/Archive.tsx"; + +const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { + const { rootPopupState } = useContext(CascadingContext); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback( + (f: () => any) => () => { + f(); + if (rootPopupState) { + rootPopupState.close(); + } + dispatch( + closeContextMenu({ + index: 0, + value: undefined, + }), + ); + }, + [dispatch, targets], + ); + return ( + <> + {displayOpt.showVersionControl && ( + + dispatch( + setVersionControlDialog({ + open: true, + file: targets[0], + }), + ), + )} + > + + + + {t("application:fileManager.manageVersions")} + + )} + {displayOpt.showManageShares && ( + + dispatch( + setManageShareDialog({ + open: true, + file: targets[0], + }), + ), + )} + > + + + + {t("application:fileManager.manageShares")} + + )} + {displayOpt.showCreateArchive && ( + + dispatch( + setCreateArchiveDialog({ + open: true, + files: targets, + }), + ), + )} + > + + + + {t("application:fileManager.createArchive")} + + )} + + ); +}; + +export default MoreMenuItems; diff --git a/src/component/FileManager/ContextMenu/NewFileTemplateMenuItems.tsx b/src/component/FileManager/ContextMenu/NewFileTemplateMenuItems.tsx new file mode 100644 index 0000000..5b2bcb5 --- /dev/null +++ b/src/component/FileManager/ContextMenu/NewFileTemplateMenuItems.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx"; +import { ViewersByID } from "../../../redux/siteConfigSlice.ts"; +import { ListItemIcon, ListItemText } from "@mui/material"; +import { ViewerIcon } from "../Dialogs/OpenWith.tsx"; +import { SquareMenuItem } from "./ContextMenu.tsx"; +import { + CascadingContext, + CascadingMenuItem, + CascadingSubmenu, +} from "./CascadingMenu.tsx"; +import { NewFileTemplate, Viewer } from "../../../api/explorer.ts"; +import { createNew } from "../../../redux/thunks/file.ts"; +import { CreateNewDialogType } from "../../../redux/globalStateSlice.ts"; + +interface MultiTemplatesMenuItemsProps { + viewer: Viewer; +} + +const MultiTemplatesMenuItems = ({ viewer }: MultiTemplatesMenuItemsProps) => { + const { rootPopupState } = useContext(CascadingContext); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback( + (f: NewFileTemplate) => () => { + if (rootPopupState) { + rootPopupState.close(); + } + + dispatch(createNew(0, CreateNewDialogType.file, viewer, f)); + }, + [dispatch], + ); + + return ( + <> + {viewer.templates?.map((template) => ( + + + {t("fileManager.newDocumentType", { + display_name: t(template.display_name), + ext: template.ext, + })} + + + ))} + + ); +}; + +const NewFileTemplateMenuItems = (props: SubMenuItemsProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onClick = useCallback( + (viewer: Viewer, template: NewFileTemplate) => () => { + dispatch(createNew(0, CreateNewDialogType.file, viewer, template)); + }, + [dispatch], + ); + + return ( + <> + {Object.values(ViewersByID) + .filter((viewer) => viewer.templates) + .map((viewer) => { + if (!viewer.templates) { + return null; + } + + if (viewer.templates.length == 1) { + return ( + + + + + + {t("fileManager.newDocumentType", { + display_name: t(viewer.templates[0].display_name), + ext: viewer.templates[0].ext, + })} + + + ); + } else { + return ( + } + > + + + ); + } + })} + + ); +}; + +export default NewFileTemplateMenuItems; diff --git a/src/component/FileManager/ContextMenu/OpenWithMenuItems.tsx b/src/component/FileManager/ContextMenu/OpenWithMenuItems.tsx new file mode 100644 index 0000000..886d54d --- /dev/null +++ b/src/component/FileManager/ContextMenu/OpenWithMenuItems.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useContext, useMemo } from "react"; +import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { closeContextMenu } from "../../../redux/fileManagerSlice.ts"; +import { ListItemIcon, ListItemText } from "@mui/material"; +import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx"; +import { fileExtension } from "../../../util"; +import { Viewers } from "../../../redux/siteConfigSlice.ts"; +import { Viewer } from "../../../api/explorer.ts"; +import { ViewerIcon } from "../Dialogs/OpenWith.tsx"; +import { openViewer, openViewers } from "../../../redux/thunks/viewer.ts"; + +const OpenWithMenuItems = ({ targets }: SubMenuItemsProps) => { + const { rootPopupState } = useContext(CascadingContext); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback( + (v: Viewer) => () => { + dispatch(openViewer(targets[0], v, targets[0].size)); + if (rootPopupState) { + rootPopupState.close(); + } + dispatch( + closeContextMenu({ + index: 0, + value: undefined, + }), + ); + }, + [dispatch, targets], + ); + + const openSelector = useCallback(() => { + dispatch(openViewers(0, targets[0], targets[0].size, undefined, true)); + }, [targets]); + + const viewers = useMemo(() => { + if (targets.length == 0) { + return []; + } + + const firstFileSuffix = fileExtension(targets[0].name); + return Viewers[firstFileSuffix ?? ""]; + }, [targets]); + + return ( + <> + {viewers.map((viewer) => ( + + + + + {t(viewer.display_name)} + + ))} + + + {t("fileManager.selectApplications")} + + + ); +}; + +export default OpenWithMenuItems; diff --git a/src/component/FileManager/ContextMenu/OrganizeMenuItems.tsx b/src/component/FileManager/ContextMenu/OrganizeMenuItems.tsx new file mode 100644 index 0000000..b88fa84 --- /dev/null +++ b/src/component/FileManager/ContextMenu/OrganizeMenuItems.tsx @@ -0,0 +1,118 @@ +import { DisplayOption } from "./useActionDisplayOpt.ts"; +import { FileResponse } from "../../../api/explorer.ts"; +import { useCallback, useContext } from "react"; +import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { closeContextMenu } from "../../../redux/fileManagerSlice.ts"; +import { + applyIconColor, + dialogBasedMoveCopy, +} from "../../../redux/thunks/file.ts"; +import { ListItemIcon, ListItemText } from "@mui/material"; +import FolderArrowRightOutlined from "../../Icons/FolderArrowRightOutlined.tsx"; +import { + setChangeIconDialog, + setPinFileDialog, +} from "../../../redux/globalStateSlice.ts"; +import { getFileLinkedUri } from "../../../util"; +import PinOutlined from "../../Icons/PinOutlined.tsx"; +import EmojiEdit from "../../Icons/EmojiEdit.tsx"; +import FolderColorQuickAction from "../FileInfo/FolderColorQuickAction.tsx"; +import { DenseDivider } from "./ContextMenu.tsx"; + +export interface SubMenuItemsProps { + displayOpt: DisplayOption; + targets: FileResponse[]; +} +const OrganizeMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { + const { rootPopupState } = useContext(CascadingContext); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback( + (f: () => any) => () => { + f(); + if (rootPopupState) { + rootPopupState.close(); + } + dispatch( + closeContextMenu({ + index: 0, + value: undefined, + }), + ); + }, + [dispatch, targets], + ); + const showDivider = + (displayOpt.showMove || displayOpt.showPin || displayOpt.showChangeIcon) && + displayOpt.showChangeFolderColor; + return ( + <> + {displayOpt.showMove && ( + + dispatch(dialogBasedMoveCopy(0, targets, false)), + )} + > + + + + {t("application:fileManager.move")} + + )} + {displayOpt.showPin && ( + + dispatch( + setPinFileDialog({ + open: true, + uri: getFileLinkedUri(targets[0]), + }), + ), + )} + > + + + + {t("application:fileManager.pin")} + + )} + {displayOpt.showChangeIcon && ( + + dispatch( + setChangeIconDialog({ + open: true, + file: targets, + }), + ), + )} + > + + + + + {t("application:fileManager.customizeIcon")} + + + )} + {showDivider && } + {displayOpt.showChangeFolderColor && ( + + onClick(() => dispatch(applyIconColor(0, targets, color, true)))() + } + sx={{ + maxWidth: "204px", + margin: (theme) => `0 ${theme.spacing(0.5)}`, + padding: (theme) => `${theme.spacing(0.5)} ${theme.spacing(1)}`, + }} + /> + )} + + ); +}; + +export default OrganizeMenuItems; diff --git a/src/component/FileManager/ContextMenu/TagMenuItems.tsx b/src/component/FileManager/ContextMenu/TagMenuItems.tsx new file mode 100644 index 0000000..38875f6 --- /dev/null +++ b/src/component/FileManager/ContextMenu/TagMenuItems.tsx @@ -0,0 +1,149 @@ +import { getUniqueTagsFromFiles, Tag as TagItem } from "../Dialogs/Tags.tsx"; +import { FileResponse, Metadata } from "../../../api/explorer.ts"; +import React, { useCallback, useContext, useState } from "react"; +import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { closeContextMenu } from "../../../redux/fileManagerSlice.ts"; +import { setTagsDialog } from "../../../redux/globalStateSlice.ts"; +import { ListItemIcon, ListItemText } from "@mui/material"; +import Tags from "../../Icons/Tags.tsx"; +import { + DenseDivider, + SquareMenuItem, + SubMenuItemsProps, +} from "./ContextMenu.tsx"; +import SessionManager, { UserSettings } from "../../../session"; +import { UsedTags } from "../../../session/utils.ts"; +import Checkmark from "../../Icons/Checkmark.tsx"; +import FileTag from "../Explorer/FileTag.tsx"; +import { patchFileMetadata } from "../../../redux/thunks/file.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; + +interface TagOption extends TagItem { + selected?: boolean; +} + +const getTagOptions = (targets: FileResponse[]): TagOption[] => { + const tags: { + [key: string]: TagOption; + } = {}; + getUniqueTagsFromFiles(targets).forEach((tag) => { + tags[tag.key] = { ...tag, selected: true }; + }); + + const existing = SessionManager.get(UserSettings.UsedTags) as UsedTags; + if (existing) { + Object.keys(existing).forEach((key) => { + if (!tags[key]) { + tags[key] = { key, color: existing[key] ?? undefined, selected: false }; + } + }); + } + + return Object.values(tags); +}; + +const TagMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => { + const { rootPopupState } = useContext(CascadingContext); + const [tags, setTags] = useState(getTagOptions(targets)); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const onClick = useCallback( + (f: () => any) => () => { + f(); + if (rootPopupState) { + rootPopupState.close(); + } + dispatch( + closeContextMenu({ + index: 0, + value: undefined, + }), + ); + }, + [dispatch, targets], + ); + + const onTagChange = useCallback( + async (tag: TagOption, selected: boolean) => { + setTags((tags) => + tags.map((t) => { + if (t.key == tag.key) { + return { ...t, selected }; + } + return t; + }), + ); + try { + await dispatch( + patchFileMetadata(FileManagerIndex.main, targets, [ + { + key: Metadata.tag_prefix + tag.key, + value: tag.color, + remove: !selected, + }, + ]), + ); + } catch (e) { + return; + } + }, + [targets, setTags], + ); + + return ( + <> + + dispatch( + setTagsDialog({ + open: true, + file: targets, + }), + ), + )} + > + + + + {t("application:modals.manageTags")} + + {tags.length > 0 && } + {tags.map((tag) => ( + onTagChange(tag, !tag.selected)} + > + {tag.selected && ( + <> + + + + + + + + )} + {!tag.selected && ( + + + + )} + + ))} + + ); +}; + +export default TagMenuItems; diff --git a/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts new file mode 100644 index 0000000..4c70a4a --- /dev/null +++ b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts @@ -0,0 +1,293 @@ +import { useMemo } from "react"; +import { FileResponse, FileType, Metadata, NavigatorCapability } from "../../../api/explorer.ts"; +import { GroupPermission } from "../../../api/user.ts"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import { ContextMenuTypes } from "../../../redux/fileManagerSlice.ts"; +import { Viewers, ViewersByID } from "../../../redux/siteConfigSlice.ts"; +import { ExpandedViewerSetting } from "../../../redux/thunks/viewer.ts"; +import SessionManager from "../../../session"; +import { fileExtension } from "../../../util"; +import Boolset from "../../../util/boolset.ts"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; + +const supportedArchiveTypes = ["zip", "gz", "xz", "tar", "rar"]; + +export const canManageVersion = (file: FileResponse, bs: Boolset) => { + return ( + file.type == FileType.file && + (!file.metadata || !file.metadata[Metadata.share_redirect]) && + bs.enabled(NavigatorCapability.version_control) + ); +}; + +export const canShowInfo = (cap: Boolset) => { + return cap.enabled(NavigatorCapability.info); +}; + +export const canUpdate = (opt: DisplayOption) => { + return !!( + opt.allUpdatable && + opt.hasFile && + opt.orCapability?.enabled(NavigatorCapability.upload_file) && + opt.allUpdatable + ); +}; + +export interface DisplayOption { + allReadable: boolean; + allUpdatable: boolean; + + hasReadable?: boolean; + hasUpdatable?: boolean; + + hasTrashFile?: boolean; + hasFile?: boolean; + hasFolder?: boolean; + hasOwned?: boolean; + + showEnter?: boolean; + showOpen?: boolean; + showOpenWith?: () => boolean; + showDownload?: boolean; + showGoToSharedLink?: boolean; + showExtractArchive?: boolean; + showTorrentRemoteDownload?: boolean; + showGoToParent?: boolean; + + showDelete?: boolean; + showRestore?: boolean; + showRename?: boolean; + showPin?: boolean; + showOrganize?: boolean; + showCopy?: boolean; + showShare?: boolean; + showInfo?: boolean; + showDirectLink?: boolean; + + showMove?: boolean; + showTags?: boolean; + showChangeFolderColor?: boolean; + showChangeIcon?: boolean; + + showMore?: boolean; + showVersionControl?: boolean; + showManageShares?: boolean; + showCreateArchive?: boolean; + + andCapability?: Boolset; + orCapability?: Boolset; + + showCreateFolder?: boolean; + showCreateFile?: boolean; + showRefresh?: boolean; + showNewFileFromTemplate?: boolean; + showUpload?: boolean; + showRemoteDownload?: boolean; +} + +const capabilityMap: { [key: string]: Boolset } = {}; + +export const getActionOpt = ( + targets: FileResponse[], + viewerSetting?: ExpandedViewerSetting, + type?: string, + parent?: FileResponse, + fmIndex: number = 0, +): DisplayOption => { + const currentUser = SessionManager.currentLoginOrNull(); + const currentUserAnonymous = SessionManager.currentUser(); + const groupBs = SessionManager.currentUserGroupPermission(); + const display: DisplayOption = { + allReadable: true, + allUpdatable: true, + }; + if (type == ContextMenuTypes.empty || type == ContextMenuTypes.new) { + display.showRefresh = type == ContextMenuTypes.empty; + display.showRemoteDownload = groupBs.enabled(GroupPermission.remote_download) && !!currentUser; + + if (!parent || parent.type != FileType.folder) { + display.showRemoteDownload = display.showRemoteDownload && type == ContextMenuTypes.new; + return display; + } + + const parentCap = new Boolset(parent.capability); + display.showCreateFolder = parentCap.enabled(NavigatorCapability.create_file) && parent.owned; + display.showCreateFile = display.showCreateFolder && fmIndex == FileManagerIndex.main; + display.showUpload = display.showCreateFile; + if (display.showCreateFile) { + const allViewers = Object.entries(ViewersByID); + for (let i = 0; i < allViewers.length; i++) { + if (allViewers[i][1] && allViewers[i][1].templates) { + display.showNewFileFromTemplate = true; + break; + } + } + } + + return display; + } + + if (type == ContextMenuTypes.searchResult) { + display.showGoToParent = true; + } + + const parentUrl = new CrUri(targets?.[0]?.path ?? defaultPath); + targets.forEach((target) => { + let readable = true; + let updatable = target.owned && parentUrl.fs() != Filesystem.share; + + if (display.allReadable && !readable) { + display.allReadable = false; + } + if (display.allUpdatable && !updatable) { + display.allUpdatable = false; + } + + if (!display.hasReadable && readable) { + display.hasReadable = true; + } + if (!display.hasUpdatable && updatable) { + display.hasUpdatable = true; + } + + if (target.metadata && target.metadata[Metadata.restore_uri]) { + display.hasTrashFile = true; + } + + if (target.type == FileType.file) { + display.hasFile = true; + } + + if (target.type == FileType.folder) { + display.hasFolder = true; + } + + if (target.owned) { + display.hasOwned = true; + } + + if (target.capability) { + let bs = capabilityMap[target.capability]; + if (!bs) { + bs = new Boolset(target.capability); + capabilityMap[target.capability] = bs; + } + + if (!display.andCapability) { + display.andCapability = bs; + } + + display.andCapability = display.andCapability.and(bs); + + if (!display.orCapability) { + display.orCapability = bs; + } + display.orCapability = display.orCapability.or(bs); + } + }); + + const firstFileSuffix = fileExtension(targets[0]?.name ?? ""); + display.showPin = !display.hasTrashFile && targets.length == 1 && display.hasFolder; + display.showDelete = + display.hasUpdatable && + display.orCapability && + (display.orCapability.enabled(NavigatorCapability.soft_delete) || + display.orCapability.enabled(NavigatorCapability.delete_file)); + display.showRestore = display.andCapability?.enabled(NavigatorCapability.restore); + display.showRename = + targets.length == 1 && + display.allUpdatable && + display.orCapability && + display.orCapability.enabled(NavigatorCapability.rename_file); + display.showCopy = display.hasUpdatable && !!display.orCapability; + display.showShare = + targets.length == 1 && + !!currentUser && + groupBs.enabled(GroupPermission.share) && + display.allUpdatable && + (targets[0].owned || groupBs.enabled(GroupPermission.is_admin)) && + display.orCapability && + display.orCapability.enabled(NavigatorCapability.share) && + (!targets[0].metadata || + (!targets[0].metadata[Metadata.share_redirect] && !targets[0].metadata[Metadata.restore_uri])); + display.showMove = display.hasUpdatable && !!display.orCapability; + display.showTags = + display.hasUpdatable && display.orCapability && display.orCapability.enabled(NavigatorCapability.update_metadata); + display.showChangeFolderColor = + display.hasUpdatable && + !display.hasFile && + display.orCapability && + display.orCapability.enabled(NavigatorCapability.update_metadata); + display.showChangeIcon = + display.hasUpdatable && display.orCapability && display.orCapability.enabled(NavigatorCapability.update_metadata); + display.showDownload = + display.hasReadable && display.orCapability && display.orCapability.enabled(NavigatorCapability.download_file); + display.showDirectLink = + display.hasOwned && + display.orCapability && + (currentUserAnonymous?.group?.direct_link_batch_size ?? 0) >= targets.length && + display.orCapability.enabled(NavigatorCapability.download_file); + display.showOpen = targets.length == 1 && display.hasFile && display.showDownload && !!viewerSetting; + display.showEnter = + targets.length == 1 && + display.hasFolder && + display.orCapability?.enabled(NavigatorCapability.enter_folder) && + display.allReadable; + display.showExtractArchive = + targets.length == 1 && + display.hasFile && + display.showDownload && + !!currentUser && + groupBs.enabled(GroupPermission.archive_task) && + supportedArchiveTypes.includes(firstFileSuffix ?? ""); + display.showTorrentRemoteDownload = + targets.length == 1 && + display.hasFile && + display.showDownload && + !!currentUser && + groupBs.enabled(GroupPermission.remote_download) && + firstFileSuffix == "torrent"; + + display.showOpenWith = () => false; + if (display.showOpen) { + display.showOpen = !!firstFileSuffix && !!viewerSetting?.[firstFileSuffix]; + display.showOpenWith = () => + !!(display.showOpen && viewerSetting && viewerSetting[firstFileSuffix ?? ""]?.length > 1); + } + display.showOrganize = display.showPin || display.showMove || display.showChangeFolderColor || display.showChangeIcon; + display.showGoToSharedLink = + targets.length == 1 && display.hasFile && targets[0].metadata && !!targets[0].metadata[Metadata.share_redirect]; + display.showInfo = targets.length == 1 && display.orCapability && canShowInfo(display.orCapability); + display.showVersionControl = + targets.length == 1 && + display.orCapability && + display.hasReadable && + canManageVersion(targets[0], display.orCapability); + display.showManageShares = + targets.length == 1 && + targets[0].shared && + display.orCapability && + !!currentUser && + groupBs.enabled(GroupPermission.share) && + display.orCapability.enabled(NavigatorCapability.share); + display.showCreateArchive = + display.hasReadable && + !!currentUser && + groupBs.enabled(GroupPermission.archive_task) && + display.orCapability && + display.orCapability.enabled(NavigatorCapability.download_file); + + display.showMore = display.showVersionControl || display.showManageShares || display.showCreateArchive; + return display; +}; + +const useActionDisplayOpt = (targets: FileResponse[], type?: string, parent?: FileResponse, fmIndex: number = 0) => { + const opt = useMemo(() => { + return getActionOpt(targets, Viewers, type, parent, fmIndex); + }, [targets, type, parent, fmIndex]); + + return opt; +}; + +export default useActionDisplayOpt; diff --git a/src/component/FileManager/Dialogs/ChangeIcon.tsx b/src/component/FileManager/Dialogs/ChangeIcon.tsx new file mode 100644 index 0000000..35c310c --- /dev/null +++ b/src/component/FileManager/Dialogs/ChangeIcon.tsx @@ -0,0 +1,205 @@ +import { useTranslation } from "react-i18next"; +import { + Box, + Button, + DialogContent, + Skeleton, + styled, + Tab, + Tabs, + useTheme, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { closeChangeIconDialog } from "../../../redux/globalStateSlice.ts"; +import { LoadingButton } from "@mui/lab"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import AutoHeight from "../../Common/AutoHeight.tsx"; +import { ConfigLoadState } from "../../../redux/siteConfigSlice.ts"; +import { applyIcon } from "../../../redux/thunks/file.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; + +interface EmojiSetting { + [key: string]: string[]; +} + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; + loading?: boolean; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +const StyledTab = styled(Tab)(({ theme }) => ({ + minWidth: 0, + minHeight: 0, + fontSize: theme.typography.h6.fontSize, + padding: "8px 10px", +})); + +const EmojiButton = styled(Button)(({ theme }) => ({ + minWidth: 0, + padding: "0px 4px", + fontSize: theme.typography.h6.fontSize, +})); + +const SelectorBox = styled(Box)({ + display: "flex", + flexWrap: "wrap", +}); + +const ChangeIcon = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + + const [tabValue, setTabValue] = useState(0); + const [loading, setLoading] = useState(false); + + const open = useAppSelector( + (state) => state.globalState.changeIconDialogOpen, + ); + const targets = useAppSelector( + (state) => state.globalState.changeIconDialogFile, + ); + const emojiStr = useAppSelector( + (state) => state.siteConfig.emojis.config.emoji_preset, + ); + const emojiStrLoaded = useAppSelector( + (state) => state.siteConfig.emojis.loaded, + ); + + const emojiSetting = useMemo((): EmojiSetting => { + if (!emojiStr) return {}; + try { + return JSON.parse(emojiStr) as EmojiSetting; + } catch (e) { + console.warn("failed to parse emoji setting", e); + } + return {}; + }, [emojiStr]); + + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + useEffect(() => { + if (open && emojiStrLoaded != ConfigLoadState.Loaded) { + dispatch(loadSiteConfig("emojis")); + } + }, [open]); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closeChangeIconDialog()); + } + }, [dispatch, loading]); + + const onAccept = useCallback( + (icon?: string) => async (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + } + + if (!targets) return; + + setLoading(true); + try { + await dispatch(applyIcon(FileManagerIndex.main, targets, icon)); + } catch (e) { + } finally { + setLoading(false); + dispatch(closeChangeIconDialog()); + } + }, + [dispatch, targets, setLoading], + ); + + return ( + + {t("application:modals.resetToDefault")} + + } + > + + + + + + {emojiStrLoaded ? ( + Object.keys(emojiSetting).map((key) => ( + + )) + ) : ( + } /> + )} + + + + {emojiStrLoaded ? ( + Object.keys(emojiSetting).map((key, index) => ( + + + {emojiSetting[key].map((emoji) => ( + + {emoji} + + ))} + + + )) + ) : ( + + + {[...Array(50).keys()].map(() => ( + + + + ))} + + + )} + + + + + + ); +}; +export default ChangeIcon; diff --git a/src/component/FileManager/Dialogs/CreateArchive.tsx b/src/component/FileManager/Dialogs/CreateArchive.tsx new file mode 100644 index 0000000..6227ea0 --- /dev/null +++ b/src/component/FileManager/Dialogs/CreateArchive.tsx @@ -0,0 +1,102 @@ +import { DialogContent, Stack, useMediaQuery, useTheme } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendCreateArchive } from "../../../api/api.ts"; +import { closeCreateArchiveDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { getFileLinkedUri } from "../../../util"; +import CrUri from "../../../util/uri.ts"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import { PathSelectorForm } from "../../Common/Form/PathSelectorForm.tsx"; +import { ViewTaskAction } from "../../Common/Snackbar/snackbar.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import Archive from "../../Icons/Archive.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; + +const CreateArchive = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [loading, setLoading] = useState(false); + const [fileName, setFileName] = useState("archive.zip"); + const [path, setPath] = useState(""); + + const open = useAppSelector((state) => state.globalState.createArchiveDialogOpen); + const targets = useAppSelector((state) => state.globalState.createArchiveDialogFiles); + const current = useAppSelector((state) => state.fileManager[FileManagerIndex.main].pure_path); + + useEffect(() => { + if (open) { + setPath(current ?? ""); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(closeCreateArchiveDialog()); + }, [dispatch]); + + const onAccept = useCallback(() => { + if (!targets) { + return; + } + + setLoading(true); + const dst = new CrUri(path); + dispatch( + sendCreateArchive({ + src: targets?.map((t) => getFileLinkedUri(t)), + dst: dst.join(fileName).toString(), + }), + ) + .then(() => { + dispatch(closeCreateArchiveDialog()); + enqueueSnackbar({ + message: t("modals.taskCreated"), + variant: "success", + action: ViewTaskAction(), + }); + }) + .finally(() => { + setLoading(false); + }); + }, [targets, fileName, path]); + + return ( + + + + } + variant="outlined" + value={fileName} + onChange={(e) => setFileName(e.target.value)} + label={t("application:modals.zipFileName")} + fullWidth + /> + + + + + + + ); +}; +export default CreateArchive; diff --git a/src/component/FileManager/Dialogs/CreateNew.tsx b/src/component/FileManager/Dialogs/CreateNew.tsx new file mode 100644 index 0000000..ad3a485 --- /dev/null +++ b/src/component/FileManager/Dialogs/CreateNew.tsx @@ -0,0 +1,156 @@ +import { useTranslation } from "react-i18next"; +import { DialogContent, Stack } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; +import { setRenameFileModalError } from "../../../redux/fileManagerSlice.ts"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { createNewDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; +import { + closeCreateNewDialog, + CreateNewDialogType, +} from "../../../redux/globalStateSlice.ts"; +import { submitCreateNew } from "../../../redux/thunks/file.ts"; +import { FileType } from "../../../api/explorer.ts"; + +const CreateNew = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [name, setName] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const formRef = useRef(null); + const inputRef = useRef(null); + + const open = useAppSelector((state) => state.globalState.createNewDialogOpen); + const promiseId = useAppSelector( + (state) => state.globalState.createNewPromiseId, + ); + const type = useAppSelector((state) => state.globalState.createNewDialogType); + const defaultName = useAppSelector( + (state) => state.globalState.createNewDialogDefault, + ); + const fmIndex = + useAppSelector((state) => state.globalState.createNewDialogFmIndex) ?? 0; + + useEffect(() => { + if (open) { + setName(defaultName ?? ""); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(closeCreateNewDialog()); + if (promiseId) { + createNewDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback( + (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + setLoading(true); + dispatch( + submitCreateNew( + fmIndex, + name, + type == CreateNewDialogType.folder ? FileType.folder : FileType.file, + ), + ) + .then((f) => { + if (promiseId) { + createNewDialogPromisePool[promiseId]?.resolve(f); + } + dispatch(closeCreateNewDialog()); + }) + .finally(() => { + setLoading(false); + }); + }, + [promiseId, name], + ); + + const onOkClicked = useCallback(() => { + if (formRef.current) { + if (formRef.current.reportValidity()) { + onAccept(); + } + } + }, [formRef, onAccept]); + + useEffect(() => { + if (open) { + const lastDot = name.lastIndexOf("."); + setTimeout( + () => + inputRef.current && + inputRef.current.setSelectionRange( + 0, + lastDot > 0 ? lastDot : name.length, + ), + 200, + ); + } + }, [open, inputRef.current]); + + const onNameChange = useCallback( + (e: ChangeEvent) => { + setName(e.target.value); + if (error) { + dispatch(setRenameFileModalError({ index: 0, value: undefined })); + } + }, + [dispatch, setName, error], + ); + + return ( + + + +
+ + +
+
+
+ ); +}; +export default CreateNew; diff --git a/src/component/FileManager/Dialogs/CreateRemoteDownload.tsx b/src/component/FileManager/Dialogs/CreateRemoteDownload.tsx new file mode 100644 index 0000000..a8a9c10 --- /dev/null +++ b/src/component/FileManager/Dialogs/CreateRemoteDownload.tsx @@ -0,0 +1,119 @@ +import { DialogContent, Stack, useMediaQuery, useTheme } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendCreateRemoteDownload } from "../../../api/api.ts"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import { closeRemoteDownloadDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { getFileLinkedUri } from "../../../util"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { FileDisplayForm } from "../../Common/Form/FileDisplayForm.tsx"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import { PathSelectorForm } from "../../Common/Form/PathSelectorForm.tsx"; +import { ViewTaskAction } from "../../Common/Snackbar/snackbar.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import Link from "../../Icons/Link.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; + +const CreateRemoteDownload = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [loading, setLoading] = useState(false); + const [path, setPath] = useState(""); + const [url, setUrl] = useState(""); + + const open = useAppSelector((state) => state.globalState.remoteDownloadDialogOpen); + const target = useAppSelector((state) => state.globalState.remoteDownloadDialogFile); + const current = useAppSelector((state) => state.fileManager[FileManagerIndex.main].pure_path); + + useEffect(() => { + if (open) { + const initialPath = new CrUri(current ?? defaultPath); + const fs = initialPath.fs(); + setPath(fs == Filesystem.shared_with_me || fs == Filesystem.trash ? defaultPath : initialPath.toString()); + setUrl(""); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(closeRemoteDownloadDialog()); + }, [dispatch]); + + const onAccept = useCallback(() => { + if (!target && !url) { + return; + } + + setLoading(true); + dispatch( + sendCreateRemoteDownload({ + src_file: target ? getFileLinkedUri(target) : undefined, + dst: path, + src: url ? url.split("\n") : undefined, + }), + ) + .then(() => { + dispatch(closeRemoteDownloadDialog()); + enqueueSnackbar({ + message: t("modals.taskCreated"), + variant: "success", + action: ViewTaskAction("/downloads"), + }); + }) + .finally(() => { + setLoading(false); + }); + }, [target, url, path]); + + return ( + + + + + {target && } + {!target && ( + } + variant="outlined" + value={url} + multiline + onChange={(e) => setUrl(e.target.value)} + placeholder={t("modals.remoteDownloadURLDescription")} + label={t("application:modals.remoteDownloadURL")} + fullWidth + /> + )} + + + + + + + + ); +}; +export default CreateRemoteDownload; diff --git a/src/component/FileManager/Dialogs/DeleteConfirmation.tsx b/src/component/FileManager/Dialogs/DeleteConfirmation.tsx new file mode 100644 index 0000000..c00d229 --- /dev/null +++ b/src/component/FileManager/Dialogs/DeleteConfirmation.tsx @@ -0,0 +1,156 @@ +import { Alert, Checkbox, Collapse, DialogContent, FormGroup, Stack, Tooltip } from "@mui/material"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import { useCallback, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Metadata } from "../../../api/explorer.ts"; +import { GroupPermission } from "../../../api/user.ts"; +import { setFileDeleteModal } from "../../../redux/fileManagerSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { deleteDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import SessionManager from "../../../session"; +import { formatDuration } from "../../../util/datetime.ts"; +import { SmallFormControlLabel } from "../../Common/StyledComponents.tsx"; +import DialogAccordion from "../../Dialogs/DialogAccordion.tsx"; +import DraggableDialog, { StyledDialogContentText } from "../../Dialogs/DraggableDialog.tsx"; + +dayjs.extend(duration); + +export interface DeleteOption { + unlink?: boolean; + skip_soft_delete?: boolean; +} +const DeleteConfirmation = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [unlink, setUnlink] = useState(false); + const [skipSoftDelete, setSkipSoftDelete] = useState(false); + + const open = useAppSelector((state) => state.fileManager[0].deleteFileModalOpen); + const targets = useAppSelector((state) => state.fileManager[0].deleteFileModalSelected); + const promiseId = useAppSelector((state) => state.fileManager[0].deleteFileModalPromiseId); + const loading = useAppSelector((state) => state.fileManager[0].deleteFileModalLoading); + + const hasTrashFiles = useMemo(() => { + if (targets) { + return targets.some((target) => target.metadata && target.metadata[Metadata.restore_uri]); + } + + return false; + }, [targets]); + + const onClose = useCallback(() => { + dispatch( + setFileDeleteModal({ + index: 0, + value: [false, targets, undefined, false], + }), + ); + if (promiseId) { + deleteDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, targets, promiseId]); + + const singleFileToTrash = targets && targets.length == 1 && !hasTrashFiles && !skipSoftDelete; + const multipleFilesToTrash = targets && targets.length > 1 && !hasTrashFiles && !skipSoftDelete; + const singleFilePermanently = targets && targets.length == 1 && (hasTrashFiles || skipSoftDelete); + const multipleFilesPermanently = targets && targets.length > 1 && (hasTrashFiles || skipSoftDelete); + + const onAccept = useCallback(() => { + if (promiseId) { + deleteDialogPromisePool[promiseId]?.resolve({ + unlink, + skip_soft_delete: singleFilePermanently || multipleFilesPermanently ? true : skipSoftDelete, + }); + } + }, [promiseId, unlink, skipSoftDelete, singleFilePermanently, multipleFilesPermanently]); + + const permission = SessionManager.currentUserGroupPermission(); + const showSkipSoftDeleteOption = !hasTrashFiles; + const showUnlinkOption = (skipSoftDelete || hasTrashFiles) && permission.enabled(GroupPermission.advance_delete); + const showAdvanceOptions = showUnlinkOption || showSkipSoftDeleteOption; + + const group = useMemo(() => SessionManager.currentUserGroup(), [open]); + + return ( + + + + + {(singleFileToTrash || singleFilePermanently) && ( + ]} + /> + )} + {(multipleFilesToTrash || multipleFilesPermanently) && + t( + multipleFilesToTrash + ? "application:modals.deleteMultipleDescription" + : "application:modals.deleteMultipleDescriptionHard", + { + num: targets.length, + }, + )} + + + ]} + /> + + + + {showAdvanceOptions && ( + + + + + setSkipSoftDelete(e.target.checked)} + checked={skipSoftDelete} + /> + } + label={t("application:modals.skipSoftDelete")} + /> + + + + + setUnlink(e.target.checked)} checked={unlink} />} + label={t("application:modals.unlinkOnly")} + /> + + + + + )} + + + + ); +}; +export default DeleteConfirmation; diff --git a/src/component/FileManager/Dialogs/Dialogs.tsx b/src/component/FileManager/Dialogs/Dialogs.tsx new file mode 100644 index 0000000..862ff6e --- /dev/null +++ b/src/component/FileManager/Dialogs/Dialogs.tsx @@ -0,0 +1,76 @@ +import DeleteConfirmation from "./DeleteConfirmation.tsx"; +import AggregatedErrorDetail from "../../Dialogs/AggregatedErrorDetail.tsx"; +import LockConflictDetails from "./LockConflictDetails.tsx"; +import Rename from "./Rename.tsx"; +import PathSelection from "./PathSelection.tsx"; +import Tags from "./Tags.tsx"; +import ChangeIcon from "./ChangeIcon.tsx"; +import ShareDialog from "./Share/ShareDialog.tsx"; +import VersionControl from "./VersionControl.tsx"; +import ManageShares from "./Share/ManageShares.tsx"; +import StaleVersionConfirm from "./StaleVersionConfirm.tsx"; +import SaveAs from "./SaveAs.tsx"; +import Photopea from "../../Viewers/Photopea/Photopea.tsx"; +import OpenWith from "./OpenWith.tsx"; +import Wopi from "../../Viewers/Wopi.tsx"; +import CodeViewer from "../../Viewers/CodeViewer/CodeViewer.tsx"; +import DrawIOViewer from "../../Viewers/DrawIO/DrawIOViewer.tsx"; +import MarkdownViewer from "../../Viewers/MarkdownEditor/MarkdownViewer.tsx"; +import VideoViewer from "../../Viewers/Video/VideoViewer.tsx"; +import PdfViewer from "../../Viewers/PdfViewer.tsx"; +import CustomViewer from "../../Viewers/CustomViewer.tsx"; +import EpubViewer from "../../Viewers/EpubViewer/EpubViewer.tsx"; +import CreateNew from "./CreateNew.tsx"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import CreateArchive from "./CreateArchive.tsx"; +import ExtractArchive from "./ExtractArchive.tsx"; +import CreateRemoteDownload from "./CreateRemoteDownload.tsx"; +import AdvanceSearch from "../Search/AdvanceSearch/AdvanceSearch.tsx"; +import React from "react"; +import ColumnSetting from "../Explorer/ListView/ColumnSetting.tsx"; +import DirectLinks from "./DirectLinks.tsx"; + +const Dialogs = () => { + const showCreateArchive = useAppSelector((state) => state.globalState.createArchiveDialogOpen); + const showExtractArchive = useAppSelector((state) => state.globalState.extractArchiveDialogOpen); + const showRemoteDownload = useAppSelector((state) => state.globalState.remoteDownloadDialogOpen); + const showAdvancedSearch = useAppSelector((state) => state.globalState.advanceSearchOpen); + const showListViewColumnSetting = useAppSelector((state) => state.globalState.listViewColumnSettingDialogOpen); + const directLink = useAppSelector((state) => state.globalState.directLinkDialogOpen); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + {showCreateArchive != undefined && } + {showExtractArchive != undefined && } + {showRemoteDownload != undefined && } + {showAdvancedSearch != undefined && } + {showListViewColumnSetting != undefined && } + {directLink != undefined && } + + ); +}; + +export default Dialogs; diff --git a/src/component/FileManager/Dialogs/DirectLinks.tsx b/src/component/FileManager/Dialogs/DirectLinks.tsx new file mode 100644 index 0000000..25ac56a --- /dev/null +++ b/src/component/FileManager/Dialogs/DirectLinks.tsx @@ -0,0 +1,96 @@ +import { useTranslation } from "react-i18next"; +import { DialogContent, FormControlLabel, TextField } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useCallback, useMemo, useState } from "react"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { closeDirectLinkDialog } from "../../../redux/globalStateSlice.ts"; +import CrUri from "../../../util/uri.ts"; +import { StyledCheckbox } from "../../Common/StyledComponents.tsx"; + +const DirectLinks = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [showFileName, setShowFileName] = useState(false); + + const open = useAppSelector( + (state) => state.globalState.directLinkDialogOpen, + ); + const targets = useAppSelector((state) => state.globalState.directLinkRes); + + const contents = useMemo(() => { + if (!targets) { + return ""; + } + + return targets + .map((link) => { + if (!showFileName) { + return link.link; + } + + const crUri = new CrUri(link.file_url); + const elements = crUri.elements(); + return `[${elements.pop()}] ${link.link}`; + }) + .join("\n"); + }, [targets, showFileName]); + + const onClose = useCallback(() => { + dispatch(closeDirectLinkDialog()); + }, [dispatch]); + + return ( + { + setShowFileName(!showFileName); + }} + disableRipple + checked={showFileName} + size="small" + /> + } + label={t("application:modals.showFileName")} + /> + } + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "sm", + }} + > + + + + + ); +}; +export default DirectLinks; diff --git a/src/component/FileManager/Dialogs/ExtractArchive.tsx b/src/component/FileManager/Dialogs/ExtractArchive.tsx new file mode 100644 index 0000000..2366853 --- /dev/null +++ b/src/component/FileManager/Dialogs/ExtractArchive.tsx @@ -0,0 +1,177 @@ +import { + DialogContent, + FormControl, + InputAdornment, + InputLabel, + MenuItem, + Select, + Stack, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendExtractArchive } from "../../../api/api.ts"; +import { closeExtractArchiveDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { fileExtension, getFileLinkedUri } from "../../../util"; +import { FileDisplayForm } from "../../Common/Form/FileDisplayForm.tsx"; +import { PathSelectorForm } from "../../Common/Form/PathSelectorForm.tsx"; +import { ViewTaskAction } from "../../Common/Snackbar/snackbar.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import Translate from "../../Icons/Translate.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; + +const encodings = [ + "ibm866", + "iso8859_2", + "iso8859_3", + "iso8859_4", + "iso8859_5", + "iso8859_6", + "iso8859_7", + "iso8859_8", + "iso8859_8I", + "iso8859_10", + "iso8859_13", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "koi8r", + "koi8u", + "macintosh", + "windows874", + "windows1250", + "windows1251", + "windows1252", + "windows1253", + "windows1254", + "windows1255", + "windows1256", + "windows1257", + "windows1258", + "macintoshcyrillic", + "gbk", + "big5", + "eucjp", + "iso2022jp", + "shiftjis", + "euckr", + "utf16be", + "utf16le", +]; + +const defaultEncodingValue = " "; + +const ExtractArchive = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [loading, setLoading] = useState(false); + const [path, setPath] = useState(""); + const [encoding, setEncoding] = useState(defaultEncodingValue); + + const open = useAppSelector((state) => state.globalState.extractArchiveDialogOpen); + const target = useAppSelector((state) => state.globalState.extractArchiveDialogFile); + const current = useAppSelector((state) => state.fileManager[FileManagerIndex.main].pure_path); + + const showEncodingOption = useMemo(() => { + return fileExtension(target?.name ?? "") === "zip"; + }, [target?.name]); + + useEffect(() => { + if (open) { + setPath(current ?? ""); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(closeExtractArchiveDialog()); + }, [dispatch]); + + const onAccept = useCallback(() => { + if (!target) { + return; + } + + setLoading(true); + dispatch( + sendExtractArchive({ + src: [getFileLinkedUri(target)], + dst: path, + encoding: showEncodingOption && encoding != defaultEncodingValue ? encoding : undefined, + }), + ) + .then(() => { + dispatch(closeExtractArchiveDialog()); + enqueueSnackbar({ + message: t("modals.taskCreated"), + variant: "success", + action: ViewTaskAction(), + }); + }) + .finally(() => { + setLoading(false); + }); + }, [target, encoding, path, showEncodingOption]); + + return ( + + + + + {target && } + {showEncodingOption && ( + + {t("modals.selectEncoding")} + + + )} + + + + + + + + ); +}; +export default ExtractArchive; diff --git a/src/component/FileManager/Dialogs/LockConflictDetails.tsx b/src/component/FileManager/Dialogs/LockConflictDetails.tsx new file mode 100644 index 0000000..1f9c07a --- /dev/null +++ b/src/component/FileManager/Dialogs/LockConflictDetails.tsx @@ -0,0 +1,234 @@ +import { + Box, + Button, + DialogContent, + Stack, + styled, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@mui/material"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ConflictDetail, FileResponse, LockApplication } from "../../../api/explorer.ts"; +import { closeLockConflictDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { ViewersByID } from "../../../redux/siteConfigSlice.ts"; +import { generalDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import { forceUnlockFiles } from "../../../redux/thunks/file.ts"; +import { NoWrapTableCell, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx"; +import DraggableDialog, { StyledDialogActions, StyledDialogContentText } from "../../Dialogs/DraggableDialog.tsx"; +import FileBadge from "../FileBadge.tsx"; +import { ViewerIcon } from "./OpenWith.tsx"; + +interface ErrorTableProps { + data: ConflictDetail[]; + loading?: boolean; + files: { + [key: string]: FileResponse; + }; + unlock: (tokens: string[]) => Promise; +} + +export const CellHeaderWithPadding = styled(Box)({ + paddingLeft: "8px", +}); + +const ErrorTable = (props: ErrorTableProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + return ( + + + + + + {t("common:object")} + + {t("application:modals.application")} + + {t("application:setting.action")} + + + + + {props.data.map((conflict, i) => ( + + + {conflict.path && ( + + )} + {!conflict.path && } + + + {conflict.owner?.application && } + + + + + + + + + + ))} + +
+ {(!props.data || props.data.length === 0) && ( + + + {t("application:setting.listEmpty")} + + + )} +
+ ); +}; + +interface ApplicationProps { + app: LockApplication; +} + +const ApplicationNameMap: { + [key: string]: string; +} = { + rename: "application:fileManager.rename", + moveCopy: "application:modals.moveCopy", + upload: "application:modals.upload", + updateMetadata: "application:modals.updateMetadata", + delete: "application:fileManager.delete", + softDelete: "application:fileManager.delete", + dav: "application:modals.webdav", + versionControl: "fileManager.manageVersions", +}; + +const viewerType = "viewer"; + +const Application = ({ app }: ApplicationProps) => { + const { t } = useTranslation(); + const title = ApplicationNameMap[app.type] ?? app.type; + if (app.type == "viewer" && ViewersByID[app.viewer_id ?? ""]) { + const viewer = ViewersByID[app.viewer_id ?? ""]; + if (viewer) { + return ( + + + + + {viewer?.display_name} + + ); + } + } + return {t(title)}; +}; + +const LockConflictDetails = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const open = useAppSelector((state) => state.globalState.lockConflictDialogOpen); + const files = useAppSelector((state) => state.globalState.lockConflictFile); + const error = useAppSelector((state) => state.globalState.lockConflictError); + const promiseId = useAppSelector((state) => state.globalState.lockConflictPromiseId); + + const [loading, setLoading] = useState(false); + + const onClose = useCallback(() => { + dispatch(closeLockConflictDialog()); + if (promiseId) { + generalDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onRetry = useCallback(() => { + if (promiseId) { + dispatch(closeLockConflictDialog()); + generalDialogPromisePool[promiseId]?.resolve(); + } + }, [promiseId]); + + const showUnlockAll = useMemo(() => { + if (error && error.data) { + for (const conflict of error.data) { + if (conflict.token) { + return true; + } + } + } + return false; + }, [error]); + + const forceUnlockByToken = useCallback( + async (tokens: string[]) => { + setLoading(true); + try { + await dispatch(forceUnlockFiles(tokens)); + } finally { + setLoading(false); + } + }, + [dispatch, setLoading], + ); + + const unlockAll = useCallback(async () => { + const tokens = error?.data?.filter((c) => c.token).map((c) => c.token ?? ""); + if (tokens) { + await forceUnlockByToken(tokens); + } + }, [forceUnlockByToken, error]); + + return ( + + + + {t("application:modals.lockConflictDescription")} + {files && error && error.data && ( + + )} + {showUnlockAll && ( + + + + )} + + + + + + + + ); +}; + +export default LockConflictDetails; diff --git a/src/component/FileManager/Dialogs/OpenWith.tsx b/src/component/FileManager/Dialogs/OpenWith.tsx new file mode 100644 index 0000000..732092f --- /dev/null +++ b/src/component/FileManager/Dialogs/OpenWith.tsx @@ -0,0 +1,248 @@ +import { useTranslation } from "react-i18next"; +import { + Avatar, + Box, + DialogContent, + Divider, + Grid, + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + Stack, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import DraggableDialog, { + StyledDialogContentText, +} from "../../Dialogs/DraggableDialog.tsx"; +import { closeViewerSelector } from "../../../redux/globalStateSlice.ts"; +import { fileExtension } from "../../../util"; +import { Viewer, ViewerType } from "../../../api/explorer.ts"; +import { builtInViewers, openViewer } from "../../../redux/thunks/viewer.ts"; +import Image from "../../Icons/Image.tsx"; +import AutoHeight from "../../Common/AutoHeight.tsx"; +import Markdown from "../../Icons/Markdown.tsx"; +import DocumentPDF from "../../Icons/DocumentPDF.tsx"; +import Book from "../../Icons/Book.tsx"; +import MusicNote1 from "../../Icons/MusicNote1.tsx"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import { ViewersByID } from "../../../redux/siteConfigSlice.ts"; +import SessionManager, { UserSettings } from "../../../session"; +import { SecondaryButton } from "../../Common/StyledComponents.tsx"; + +export interface ViewerIconProps { + viewer: Viewer; + size?: number; + py?: number; +} + +const emptyViewer: Viewer[] = []; + +export const ViewerIDWithDefaultIcons = [ + builtInViewers.image, + builtInViewers.pdf, + builtInViewers.epub, + builtInViewers.music, + builtInViewers.markdown, +]; + +export const ViewerIcon = ({ + viewer, + size = 32, + py = 0.5, +}: ViewerIconProps) => { + const BuiltinIcons = useMemo(() => { + if (viewer.icon) { + return undefined; + } + + if (viewer.type == ViewerType.builtin) { + switch (viewer.id) { + case builtInViewers.image: + return ; + case builtInViewers.pdf: + return ( + + ); + case builtInViewers.epub: + return ; + case builtInViewers.music: + return ( + + ); + case builtInViewers.markdown: + return ( + + theme.palette.mode == "dark" ? "#cbcbcb" : "#383838", + }} + /> + ); + } + } + }, [viewer]); + return ( + + {BuiltinIcons && BuiltinIcons} + {viewer.icon && ( + + )} + + ); +}; + +const OpenWith = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [selectedViewer, setSelectedViewer] = React.useState( + null, + ); + const [expanded, setExpanded] = useState(false); + + const selectorState = useAppSelector( + (state) => state.globalState.viewerSelector, + ); + + useEffect(() => { + if (selectorState?.open) { + setExpanded(false); + setSelectedViewer(null); + } + }, [selectorState]); + + const ext = useMemo(() => { + if (!selectorState?.file) { + return ""; + } + + return fileExtension(selectorState.file.name) ?? ""; + }, [selectorState?.file]); + + const onClose = useCallback(() => { + dispatch(closeViewerSelector()); + }, [dispatch]); + + const openWith = (always: boolean, viewer?: Viewer) => { + if (!selectorState || (!selectedViewer && !viewer)) { + return; + } + + if (always) { + SessionManager.set( + UserSettings.OpenWithPrefix + ext, + viewer?.id ?? selectedViewer?.id, + ); + } + + dispatch( + openViewer( + selectorState.file, + viewer ?? (selectedViewer as Viewer), + selectorState.entitySize, + selectorState.version, + ), + ); + dispatch(closeViewerSelector()); + }; + + return ( + + + + + + {t("fileManager.openWithDescription", { + ext, + })} + + + + {( + (expanded + ? Object.values(ViewersByID) + : selectorState?.viewers) ?? emptyViewer + ).map((viewer) => ( + openWith(false, viewer)} + onClick={() => setSelectedViewer(viewer)} + > + + + + + + + + ))} + {!expanded && ( + setExpanded(true)} disablePadding> + + + + + + + + + + )} + + + {!!selectedViewer && ( + <> + + + + openWith(true)} + > + {t("modals.always")} + + + + openWith(false)} + > + {t("modals.justOnce")} + + + + + )} + + + ); +}; +export default OpenWith; diff --git a/src/component/FileManager/Dialogs/PathSelection.tsx b/src/component/FileManager/Dialogs/PathSelection.tsx new file mode 100644 index 0000000..00f72ba --- /dev/null +++ b/src/component/FileManager/Dialogs/PathSelection.tsx @@ -0,0 +1,198 @@ +import { DialogContent, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useCallback, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { FileResponse, FileType } from "../../../api/explorer.ts"; +import { closePathSelectionDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { pathSelectionDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import FileBadge from "../FileBadge.tsx"; +import FolderPicker, { useFolderSelector } from "../FolderPicker.tsx"; + +export const PathSelectionVariantOptions = { + copy: "copy", + move: "move", + shortcut: "shortcut", +}; + +interface SelectedFolderIndicatorProps { + selectedFile?: FileResponse; + selectedPath?: string; + variant: PathSelectionVariant; +} + +interface PathSelectionVariant { + indicator: string; + title: string; + disableSharedWithMe?: boolean; + disableTrash?: boolean; +} + +export const PathSelectionVariants: Record = { + copy: { + indicator: "fileManager.copyToDst", + title: "application:fileManager.copyTo", + disableSharedWithMe: true, + disableTrash: true, + }, + move: { + indicator: "fileManager.moveToDst", + title: "application:fileManager.moveTo", + disableSharedWithMe: true, + disableTrash: true, + }, + shortcut: { + indicator: "application:modals.createShortcutTo", + title: "application:modals.createShortcut", + disableSharedWithMe: true, + disableTrash: true, + }, + saveAs: { + indicator: "application:modals.saveToTitleDescription", + title: "application:modals.saveAs", + disableSharedWithMe: true, + disableTrash: true, + }, + saveTo: { + indicator: "application:modals.saveToTitleDescription", + title: "application:modals.saveToTitle", + disableSharedWithMe: true, + disableTrash: true, + }, + extractTo: { + indicator: "application:modals.decompressToDst", + title: "application:modals.decompressTo", + disableSharedWithMe: true, + disableTrash: true, + }, + downloadTo: { + indicator: "application:modals.downloadToDst", + title: "application:modals.downloadTo", + disableSharedWithMe: true, + disableTrash: true, + }, + searchIn: { + indicator: "application:navbar.searchInBase", + title: "application:navbar.searchBase", + }, + davAccountRoot: { + indicator: "application:setting.rootFolderIn", + title: "application:setting.rootFolder", + disableSharedWithMe: true, + disableTrash: true, + }, +}; + +export const SelectedFolderIndicator = ({ selectedFile, selectedPath, variant }: SelectedFolderIndicatorProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + if (!selectedFile && !selectedPath) { + return null; + } + + const badge = ( + + ); + return ( + + {isMobile ? badge : } + + ); +}; + +const PathSelection = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [loading, setLoading] = useState(false); + + const open = useAppSelector((state) => state.globalState.pathSelectDialogOpen); + const variant = useAppSelector((state) => state.globalState.pathSelectDialogVariant); + const promiseId = useAppSelector((state) => state.globalState.pathSelectPromiseId); + const initialPath = useAppSelector((state) => state.globalState.pathSelectInitialPath); + + const variantProps = useMemo( + () => (variant ? PathSelectionVariants[variant] : PathSelectionVariants["copy"]), + [variant], + ); + + const [selectedFile, selectedPath] = useFolderSelector(); + + const onClose = useCallback(() => { + dispatch(closePathSelectionDialog()); + if (promiseId) { + pathSelectionDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback(async () => { + const dst = selectedPath; + + dispatch(closePathSelectionDialog()); + if (promiseId && dst) { + pathSelectionDialogPromisePool[promiseId]?.resolve(dst); + } + }, [dispatch, selectedPath, promiseId]); + + const disabled = useMemo(() => { + const dst = selectedPath; + if (dst) { + const crUri = new CrUri(dst); + if (variantProps.disableSharedWithMe && crUri.fs() == Filesystem.shared_with_me) { + return true; + } + if (variantProps.disableTrash && crUri.fs() == Filesystem.trash) { + return true; + } + } + + return !selectedPath; + }, [selectedPath, variantProps]); + + return ( + + } + onAccept={onAccept} + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "lg", + disableRestoreFocus: true, + PaperProps: { + sx: { + height: "100%", + }, + }, + }} + > + + + + + ); +}; +export default PathSelection; diff --git a/src/component/FileManager/Dialogs/PinToSidebar.tsx b/src/component/FileManager/Dialogs/PinToSidebar.tsx new file mode 100644 index 0000000..d098f0c --- /dev/null +++ b/src/component/FileManager/Dialogs/PinToSidebar.tsx @@ -0,0 +1,92 @@ +import { useTranslation } from "react-i18next"; +import { DialogContent } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { closePinFileDialog } from "../../../redux/globalStateSlice.ts"; +import { pinToSidebar } from "../../../redux/thunks/settings.ts"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; + +const PinToSidebar = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [name, setName] = useState(""); + const [loading, setLoading] = useState(false); + + const open = useAppSelector((state) => state.globalState.pinFileDialogOpen); + const uri = useAppSelector((state) => state.globalState.pinFileUri); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closePinFileDialog()); + } + }, [dispatch, loading]); + + const onAccept = useCallback( + async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + if (!uri) { + return; + } + + setLoading(true); + try { + await dispatch(pinToSidebar(uri, name)); + } catch (e) { + } finally { + setLoading(false); + dispatch(closePinFileDialog()); + } + }, + [name, dispatch, uri, setLoading], + ); + + useEffect(() => { + if (uri && open) { + setName(""); + } + }, [uri]); + + const onNameChange = useCallback( + (e: ChangeEvent) => { + setName(e.target.value); + }, + [dispatch, setName], + ); + + return ( + + + + + + ); +}; +export default PinToSidebar; diff --git a/src/component/FileManager/Dialogs/Rename.tsx b/src/component/FileManager/Dialogs/Rename.tsx new file mode 100644 index 0000000..307435e --- /dev/null +++ b/src/component/FileManager/Dialogs/Rename.tsx @@ -0,0 +1,165 @@ +import { Trans, useTranslation } from "react-i18next"; +import { DialogContent, Stack } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { + ChangeEvent, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + closeRenameFileModal, + setRenameFileModalError, +} from "../../../redux/fileManagerSlice.ts"; +import DraggableDialog, { + StyledDialogContentText, +} from "../../Dialogs/DraggableDialog.tsx"; +import { renameDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import { validateFileName } from "../../../redux/thunks/file.ts"; +import { FileType } from "../../../api/explorer.ts"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; + +const Rename = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [name, setName] = useState(""); + const formRef = useRef(null); + const inputRef = useRef(null); + + const fmIndex = useContext(FmIndexContext); + + const open = useAppSelector( + (state) => state.fileManager[0].renameFileModalOpen, + ); + const targets = useAppSelector( + (state) => state.fileManager[0].renameFileModalSelected, + ); + const promiseId = useAppSelector( + (state) => state.fileManager[0].renameFileModalPromiseId, + ); + const loading = useAppSelector( + (state) => state.fileManager[0].renameFileModalLoading, + ); + const error = useAppSelector( + (state) => state.fileManager[0].renameFileModalError, + ); + + const onClose = useCallback(() => { + dispatch( + closeRenameFileModal({ + index: 0, + value: undefined, + }), + ); + if (promiseId) { + renameDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, targets, promiseId]); + + const onAccept = useCallback( + (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + if (promiseId) { + dispatch( + validateFileName( + 0, + renameDialogPromisePool[promiseId]?.resolve, + name, + ), + ); + } + }, + [promiseId, name], + ); + + const onOkClicked = useCallback(() => { + if (formRef.current) { + if (formRef.current.reportValidity()) { + onAccept(); + } + } + }, [formRef, onAccept]); + + useEffect(() => { + if (targets && open) { + setName(targets.name); + } + }, [targets, open]); + + useEffect(() => { + if (targets && open && inputRef.current) { + const lastDot = + targets.type == FileType.folder ? 0 : targets.name.lastIndexOf("."); + inputRef.current.setSelectionRange( + 0, + lastDot > 0 ? lastDot : targets.name.length, + ); + } + }, [inputRef.current, open]); + + const onNameChange = useCallback( + (e: ChangeEvent) => { + setName(e.target.value); + if (error) { + dispatch(setRenameFileModalError({ index: 0, value: undefined })); + } + }, + [dispatch, setName, error], + ); + + return ( + + + + + ]} + /> + +
+ + +
+
+
+ ); +}; +export default Rename; diff --git a/src/component/FileManager/Dialogs/SaveAs.tsx b/src/component/FileManager/Dialogs/SaveAs.tsx new file mode 100644 index 0000000..9c0ce3a --- /dev/null +++ b/src/component/FileManager/Dialogs/SaveAs.tsx @@ -0,0 +1,94 @@ +import { Box, DialogContent, Divider } from "@mui/material"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { closeSaveAsDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { saveAsDialogPromisePool } from "../../../redux/thunks/dialog.ts"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import FolderPicker, { useFolderSelector } from "../FolderPicker.tsx"; + +const SaveAs = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [selectedFile, selectedPath] = useFolderSelector(); + const open = useAppSelector((state) => state.globalState.saveAsDialogOpen); + const initialName = useAppSelector((state) => state.globalState.saveAsInitialName); + const promiseId = useAppSelector((state) => state.globalState.saveAsPromiseId); + const [name, setName] = useState(""); + + useEffect(() => { + if (open) { + setName(initialName ?? ""); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(closeSaveAsDialog()); + if (promiseId) { + saveAsDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback( + (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + const dst = selectedFile && selectedFile.path ? selectedFile.path : selectedPath; + dispatch(closeSaveAsDialog()); + if (promiseId && dst) { + saveAsDialogPromisePool[promiseId]?.resolve({ + uri: dst, + name: name, + }); + } + }, + [promiseId, selectedFile, name, selectedPath], + ); + + return ( + + setName(e.target.value)} + label={t("modals.fileName")} + type="text" + value={name} + fullWidth + required + /> +
+ } + denseAction + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "lg", + disableRestoreFocus: true, + PaperProps: { + sx: { + height: "100%", + }, + }, + }} + > + + + + + + ); +}; +export default SaveAs; diff --git a/src/component/FileManager/Dialogs/Share/ManageShares.tsx b/src/component/FileManager/Dialogs/Share/ManageShares.tsx new file mode 100644 index 0000000..59cbf87 --- /dev/null +++ b/src/component/FileManager/Dialogs/Share/ManageShares.tsx @@ -0,0 +1,238 @@ +import { + Box, + DialogContent, + IconButton, + ListItemText, + Menu, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import React, { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getFileInfo, sendDeleteShare } from "../../../../api/api.ts"; +import { FileResponse, Share } from "../../../../api/explorer.ts"; +import { closeManageShareDialog, setShareLinkDialog } from "../../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { confirmOperation } from "../../../../redux/thunks/dialog.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import { NoWrapTableCell, StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import TimeBadge from "../../../Common/TimeBadge.tsx"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import MoreVertical from "../../../Icons/MoreVertical.tsx"; +import { SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; +import { ShareExpires, ShareStatistics } from "../../TopBar/ShareInfoPopover.tsx"; + +const ManageShares = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + const [actionTarget, setActionTarget] = useState(null); + const [fileExtended, setFileExtended] = useState(undefined); + const [loading, setLoading] = useState(false); + + const open = useAppSelector((state) => state.globalState.manageShareDialogOpen); + const target = useAppSelector((state) => state.globalState.manageShareDialogFile); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closeManageShareDialog()); + } + }, [dispatch, loading]); + + useEffect(() => { + if (target && open) { + if (target.extended_info) { + setFileExtended(target); + } else { + setFileExtended(undefined); + dispatch( + getFileInfo({ + uri: target.path, + extended: true, + }), + ).then((res) => setFileExtended(res)); + } + } + }, [target, open]); + + const handleActionClose = () => { + setAnchorEl(null); + }; + + const handleOpenAction = (event: React.MouseEvent, element: Share) => { + event.stopPropagation(); + setAnchorEl(event.currentTarget); + setActionTarget(element); + }; + + const openEditDialog = () => { + dispatch( + setShareLinkDialog({ + open: true, + file: target, + share: actionTarget ?? undefined, + }), + ); + setAnchorEl(null); + }; + + const openLink = useCallback((s: Share) => { + window.open(s.url, "_blank"); + }, []); + + const deleteShare = useCallback(() => { + if (!target || !actionTarget) { + return; + } + + dispatch(confirmOperation(t("fileManager.deleteShareWarning"))).then(() => { + setLoading(true); + dispatch(sendDeleteShare(actionTarget.id)) + .then(() => { + setFileExtended((prev) => + prev + ? { + ...prev, + extended_info: prev.extended_info + ? { + ...prev.extended_info, + shares: prev.extended_info.shares?.filter((e) => e.id !== actionTarget.id), + } + : undefined, + } + : undefined, + ); + }) + .finally(() => { + setLoading(false); + }); + }); + setAnchorEl(null); + }, [t, target, actionTarget, setLoading, dispatch]); + + return ( + <> + + + + {t(`fileManager.${actionTarget?.expired ? "editAndReactivate" : "edit"}`)} + + + + {t(`fileManager.delete`)} + + + + + + + + + + {t("fileManager.actions")} + {t("fileManager.createdAt")} + {t("fileManager.expires")} + {t("application:share.statistics")} + {t("modals.privateShare")} + + + + {!fileExtended && ( + + + + + + + + + )} + {fileExtended?.extended_info?.shares && + fileExtended?.extended_info?.shares.map((e) => ( + (e.expired ? theme.palette.text.disabled : undefined), + }, + }} + onClick={() => openLink(e)} + hover + > + + handleOpenAction(event, e)} size={"small"}> + + + + + + + + {e.expired ? ( + t("application:share.expired") + ) : ( + <> + {e.remain_downloads != undefined || e.expires ? ( + + ) : ( + t("application:fileManager.permanentValid") + )} + + )} + + + + + {t(`fileManager.${e.is_private ? "yes" : "no"}`)} + + ))} + +
+ {fileExtended && !fileExtended?.extended_info?.shares && ( + + + {t("application:setting.listEmpty")} + + + )} +
+
+
+
+ + ); +}; +export default ManageShares; diff --git a/src/component/FileManager/Dialogs/Share/ShareDialog.tsx b/src/component/FileManager/Dialogs/Share/ShareDialog.tsx new file mode 100644 index 0000000..1dc515e --- /dev/null +++ b/src/component/FileManager/Dialogs/Share/ShareDialog.tsx @@ -0,0 +1,181 @@ +import { useTranslation } from "react-i18next"; +import { Box, DialogContent, IconButton, Tooltip, useTheme } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import React, { useCallback, useEffect, useState } from "react"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { closeShareLinkDialog } from "../../../../redux/globalStateSlice.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import ShareSettingContent, { downloadOptions, expireOptions, ShareSetting } from "./ShareSetting.tsx"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { createOrUpdateShareLink } from "../../../../redux/thunks/share.ts"; +import { FilledTextField } from "../../../Common/StyledComponents.tsx"; +import { copyToClipboard, sendLink } from "../../../../util"; +import Share from "../../../Icons/Share.tsx"; +import { FileManagerIndex } from "../../FileManager.tsx"; +import { TFunction } from "i18next"; +import { Share as ShareModel } from "../../../../api/explorer.ts"; +import dayjs from "dayjs"; + +const initialSetting: ShareSetting = { + expires_val: expireOptions[2], + downloads_val: downloadOptions[0], +}; + +const shareToSetting = (share: ShareModel, t: TFunction): ShareSetting => { + const res: ShareSetting = { + is_private: share.is_private, + downloads: share.remain_downloads != undefined && share.remain_downloads > 0, + + expires_val: expireOptions[2], + downloads_val: downloadOptions[0], + }; + + if (res.downloads) { + res.downloads_val = { + value: share.remain_downloads ?? 0, + label: (share.remain_downloads ?? 0).toString(), + }; + } + + if (share.expires != undefined) { + const expires = dayjs(share.expires); + const isExpired = expires.isBefore(dayjs()); + if (!isExpired) { + res.expires = true; + const secondsTtl = dayjs(share.expires).diff(dayjs(), "second"); + res.expires_val = { + value: secondsTtl, + label: Math.round(secondsTtl / 60) + " " + t("application:modals.minutes"), + }; + } else { + res.expires = false; + } + } + + return res; +}; + +const ShareDialog = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + + const [loading, setLoading] = useState(false); + const [setting, setSetting] = useState(initialSetting); + const [shareLink, setShareLink] = useState(""); + + const open = useAppSelector((state) => state.globalState.shareLinkDialogOpen); + const target = useAppSelector((state) => state.globalState.shareLinkDialogFile); + const editTarget = useAppSelector((state) => state.globalState.shareLinkDialogShare); + + useEffect(() => { + if (open) { + if (editTarget) { + setSetting(shareToSetting(editTarget, t)); + } else { + setSetting(initialSetting); + } + setShareLink(""); + } + }, [open]); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closeShareLinkDialog()); + } + }, [dispatch, loading]); + + const onAccept = useCallback( + async (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + } + + if (!target) return; + + if (shareLink) { + copyToClipboard(shareLink); + return; + } + + setLoading(true); + try { + const shareLink = await dispatch( + createOrUpdateShareLink(FileManagerIndex.main, target, setting, editTarget?.id), + ); + setShareLink(shareLink); + } catch (e) { + } finally { + setLoading(false); + } + }, + [dispatch, target, shareLink, editTarget, setLoading, setting, setShareLink], + ); + + return ( + <> + + sendLink(target?.name ?? "", shareLink)}> + + + + ) + : undefined + } + > + + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${shareLink}`} + > + + {!shareLink && ( + + )} + {shareLink && ( + e.target.select()} + /> + )} + + + + + + + + ); +}; +export default ShareDialog; diff --git a/src/component/FileManager/Dialogs/Share/ShareSetting.tsx b/src/component/FileManager/Dialogs/Share/ShareSetting.tsx new file mode 100644 index 0000000..e09c46b --- /dev/null +++ b/src/component/FileManager/Dialogs/Share/ShareSetting.tsx @@ -0,0 +1,298 @@ +import { useTranslation } from "react-i18next"; +import { + Autocomplete, + Checkbox, + createFilterOptions, + FormControl, + List, + ListItemButton, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + styled, + TextField, + Typography, +} from "@mui/material"; +import { FileResponse, FileType } from "../../../../api/explorer.ts"; +import MuiAccordion from "@mui/material/Accordion"; +import MuiAccordionSummary from "@mui/material/AccordionSummary"; +import MuiAccordionDetails from "@mui/material/AccordionDetails"; +import { useState } from "react"; +import Eye from "../../../Icons/Eye.tsx"; +import Timer from "../../../Icons/Timer.tsx"; +import ClockArrowDownload from "../../../Icons/ClockArrowDownload.tsx"; + +const Accordion = styled(MuiAccordion)(() => ({ + border: "0px solid rgba(0, 0, 0, .125)", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + ".Mui-expanded": { + margin: "0 0", + minHeight: 0, + }, + "&.Mui-expanded": { + margin: "0 0", + minHeight: 0, + }, +})); + +const AccordionSummary = styled(MuiAccordionSummary)(({ theme }) => ({ + padding: 0, + "& .MuiAccordionSummary-content": { + margin: 0, + display: "initial", + "&.Mui-expanded": { + margin: "0 0", + }, + }, + "&.Mui-expanded": { + borderRadius: "12px 12px 0 0", + backgroundColor: theme.palette.mode == "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.09)", + minHeight: "0px!important", + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: 24, + backgroundColor: theme.palette.mode == "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.09)", + borderRadius: "0 0 12px 12px", + fontSize: theme.typography.body2.fontSize, + color: theme.palette.text.secondary, +})); + +const StyledListItemButton = styled(ListItemButton)(() => ({})); + +export interface ShareSetting { + is_private?: boolean; + downloads?: boolean; + expires?: boolean; + + downloads_val: valueOption; + expires_val: valueOption; +} + +export interface ShareSettingProps { + setting: ShareSetting; + file?: FileResponse; + onSettingChange: (value: ShareSetting) => void; + editing?: boolean; +} + +interface valueOption { + value: number; + label: string; + inputValue?: string; +} + +export const expireOptions: valueOption[] = [ + { value: 300, label: "modals.5minutes" }, + { value: 3600, label: "modals.1hour" }, + { value: 24 * 3600, label: "modals.1day" }, + { value: 7 * 24 * 3600, label: "modals.7days" }, + { value: 30 * 24 * 3600, label: "modals.30days" }, +]; + +export const downloadOptions: valueOption[] = [ + { value: 1, label: "1" }, + { value: 2, label: "2" }, + { value: 3, label: "3" }, + { value: 4, label: "4" }, + { value: 5, label: "5" }, + { value: 20, label: "20" }, + { value: 50, label: "50" }, + { value: 100, label: "100" }, +]; + +const isNumeric = (num: any) => + (typeof num === "number" || (typeof num === "string" && num.trim() !== "")) && !isNaN(num as number); + +const filter = createFilterOptions(); + +const ShareSettingContent = ({ setting, file, editing, onSettingChange }: ShareSettingProps) => { + const { t } = useTranslation(); + + const [expanded, setExpanded] = useState(undefined); + + const handleExpand = (panel: string) => (_event: any, isExpanded: boolean) => { + setExpanded(isExpanded ? panel : undefined); + }; + + const handleCheck = (prop: "is_private" | "expires" | "downloads") => () => { + if (!setting[prop]) { + handleExpand(prop)(null, true); + } + + onSettingChange({ ...setting, [prop]: !setting[prop] }); + }; + + return ( + + + + + + + + + + + + + + {t("application:modals.privateShareDes")} + + + + + + + + + + + + + + + {t("application:modals.expirePrefix")} + + { + const filtered = filter(options, params); + + const { inputValue } = params; + const value = parseInt(inputValue) * 60; + if (inputValue !== "" && isNumeric(inputValue) && parseInt(inputValue) > 0 && value != 300) { + filtered.push({ + inputValue, + value, + label: inputValue + " " + t("application:modals.minutes"), + }); + } + + return filtered; + }} + onChange={(_event, newValue) => { + let expiry = 0; + let label = ""; + if (typeof newValue === "string") { + expiry = parseInt(newValue); + label = newValue + " " + t("application:modals.minutes"); + } else { + expiry = newValue?.value ?? 0; + label = newValue?.label ?? ""; + } + + onSettingChange({ + ...setting, + expires_val: { value: expiry, label }, + }); + }} + freeSolo + getOptionLabel={(option: string | valueOption) => (typeof option === "string" ? option : t(option.label))} + disableClearable + options={expireOptions} + renderInput={(params) => } + /> + + {t("application:modals.expireSuffix")} + + + {file?.type == FileType.file && ( + + + + + + + + + + + + + + {t("application:modals.expirePrefix")} + + { + const filtered = filter(options, params); + + const { inputValue } = params; + const value = parseInt(inputValue); + if ( + inputValue !== "" && + isNumeric(inputValue) && + parseInt(inputValue) > 0 && + !filtered.find((v) => v.value == value) + ) { + filtered.push({ + inputValue, + value, + label: inputValue, + }); + } + + return filtered; + }} + onChange={(_event, newValue) => { + let downloads = 0; + let label = ""; + if (typeof newValue === "string") { + downloads = parseInt(newValue); + label = newValue; + } else { + downloads = newValue?.value ?? 0; + label = newValue?.label ?? ""; + } + + onSettingChange({ + ...setting, + downloads_val: { value: downloads, label }, + }); + }} + freeSolo + getOptionLabel={(option: string | valueOption) => + typeof option === "string" + ? option + : t("application:modals.downloadLimitOptions", { + num: option.label, + }) + } + disableClearable + options={downloadOptions} + renderInput={(params) => } + /> + + {t("application:modals.expireSuffix")} + + + )} + + ); +}; + +export default ShareSettingContent; diff --git a/src/component/FileManager/Dialogs/StaleVersionConfirm.tsx b/src/component/FileManager/Dialogs/StaleVersionConfirm.tsx new file mode 100644 index 0000000..6d380a8 --- /dev/null +++ b/src/component/FileManager/Dialogs/StaleVersionConfirm.tsx @@ -0,0 +1,103 @@ +import { Trans, useTranslation } from "react-i18next"; +import { Button, DialogContent, Stack } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useCallback } from "react"; +import DraggableDialog, { + StyledDialogContentText, +} from "../../Dialogs/DraggableDialog.tsx"; +import { + askSaveAs, + staleVersionDialogPromisePool, +} from "../../../redux/thunks/dialog.ts"; +import { closeStaleVersionDialog } from "../../../redux/globalStateSlice.ts"; +import CrUri from "../../../util/uri.ts"; + +const StaleVersionConfirm = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const open = useAppSelector( + (state) => state.globalState.staleVersionDialogOpen, + ); + const uri = useAppSelector((state) => state.globalState.staleVersionUri); + const promiseId = useAppSelector( + (state) => state.globalState.staleVersionPromiseId, + ); + + const onClose = useCallback(() => { + dispatch(closeStaleVersionDialog()); + if (promiseId) { + staleVersionDialogPromisePool[promiseId]?.reject("cancel"); + } + }, [dispatch, promiseId]); + + const onAccept = useCallback( + (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + if (promiseId) { + staleVersionDialogPromisePool[promiseId]?.resolve({ overwrite: true }); + dispatch(closeStaleVersionDialog()); + } + }, + [promiseId, name], + ); + + const onSaveAs = useCallback(async () => { + if (!uri) { + return; + } + try { + const fileName = new CrUri(uri).elements().pop(); + if (fileName && promiseId) { + const saveAsDst = await dispatch(askSaveAs(fileName)); + const dst = new CrUri(saveAsDst.uri).join(saveAsDst.name); + staleVersionDialogPromisePool[promiseId]?.resolve({ + overwrite: false, + saveAs: dst.toString(), + }); + dispatch(closeStaleVersionDialog()); + } + } catch (e) { + return; + } + }, [dispatch, promiseId, uri]); + + return ( + + {t("modals.saveAs")} + + } + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "sm", + }} + > + + + + {t("modals.conflictDes1")} +
    + ,
  • ]} + /> +
+
+
+
+
+ ); +}; +export default StaleVersionConfirm; diff --git a/src/component/FileManager/Dialogs/Tags.tsx b/src/component/FileManager/Dialogs/Tags.tsx new file mode 100644 index 0000000..bbb0214 --- /dev/null +++ b/src/component/FileManager/Dialogs/Tags.tsx @@ -0,0 +1,211 @@ +import { Autocomplete, DialogContent, Stack, useTheme } from "@mui/material"; +import { enqueueSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FileResponse, Metadata } from "../../../api/explorer.ts"; +import { defaultColors } from "../../../constants"; +import { closeTagsDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { patchTags } from "../../../redux/thunks/file.ts"; +import SessionManager, { UserSettings } from "../../../session"; +import { addRecentUsedColor } from "../../../session/utils.ts"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; +import DialogAccordion from "../../Dialogs/DialogAccordion.tsx"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import FileTag from "../Explorer/FileTag.tsx"; +import CircleColorSelector, { customizeMagicColor } from "../FileInfo/ColorCircle/CircleColorSelector.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; + +export interface Tag { + key: string; + color?: string; +} + +export const getUniqueTagsFromFiles = (targets: FileResponse[]) => { + const tags: { + [key: string]: Tag; + } = {}; + targets.forEach((target) => { + if (target.metadata) { + Object.keys(target.metadata).forEach((key: string) => { + if (key.startsWith(Metadata.tag_prefix)) { + // trim prefix for key + const tagKey = key.slice(Metadata.tag_prefix.length); + tags[tagKey] = { + key: key.slice(Metadata.tag_prefix.length), + color: target.metadata?.[key], + }; + } + }); + } + }); + return Object.values(tags); +}; + +const Tags = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + + const [hex, setHex] = useState(undefined); + const [tags, setTags] = useState([]); + const [name, setName] = useState(""); + const [loading, setLoading] = useState(false); + + const open = useAppSelector((state) => state.globalState.tagsDialogOpen); + const targets = useAppSelector((state) => state.globalState.tagsDialogFile); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closeTagsDialog()); + } + }, [dispatch, loading]); + + const onAccept = useCallback( + async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + if (!targets) return; + + setLoading(true); + try { + await dispatch(patchTags(FileManagerIndex.main, targets, tags)); + } catch (e) { + } finally { + setLoading(false); + dispatch(closeTagsDialog()); + } + }, + [name, dispatch, targets, tags, setLoading], + ); + + const presetColors = useMemo(() => { + const colors = new Set(defaultColors); + + const recentColors = SessionManager.get(UserSettings.UsedCustomizedTagColors) as string[] | undefined; + + if (recentColors) { + recentColors.forEach((color) => { + colors.add(color); + }); + } + + return [...colors]; + }, [hex]); + + useEffect(() => { + if (targets && open) { + setTags(getUniqueTagsFromFiles(targets)); + } + }, [targets, open]); + + const onColorChange = useCallback( + (color: string | undefined) => { + color = color == theme.palette.action.selected ? undefined : color; + addRecentUsedColor(color, UserSettings.UsedCustomizedTagColors); + setHex(color); + }, + [theme, setHex], + ); + + const onTagAdded = useCallback( + (_e: any, newValue: (string | Tag)[]) => { + const duplicateMap: { [key: string]: boolean } = {}; + newValue = newValue.filter((tag) => { + const tagKey = typeof tag === "string" ? tag : tag.key; + if (!tagKey) { + return false; + } + if (duplicateMap[tagKey]) { + enqueueSnackbar(t("application:modals.duplicateTag", { tag: tagKey }), { variant: "warning" }); + return false; + } + duplicateMap[tagKey] = true; + return true; + }); + setTags(newValue.map((tag) => (typeof tag === "string" ? { key: tag, color: hex } : tag) as Tag)); + }, + [hex, setTags], + ); + + // const onNameChange = useCallback( + // (e: ChangeEvent) => { + // setName(e.target.value); + // }, + // [dispatch, setName], + // ); + + return ( + + + + o?.key} + value={tags} + freeSolo + autoSelect={true} + onChange={onTagAdded} + renderTags={(value: readonly Tag[], getTagProps) => + value.map((option: Tag, index: number) => ( + + )) + } + renderInput={(params) => ( + + )} + /> + + + + + + + ); +}; +export default Tags; diff --git a/src/component/FileManager/Dialogs/VersionControl.tsx b/src/component/FileManager/Dialogs/VersionControl.tsx new file mode 100644 index 0000000..0134a2d --- /dev/null +++ b/src/component/FileManager/Dialogs/VersionControl.tsx @@ -0,0 +1,279 @@ +import { useTranslation } from "react-i18next"; +import { + Box, + DialogContent, + IconButton, + ListItemText, + Menu, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { closeVersionControlDialog } from "../../../redux/globalStateSlice.ts"; +import { Entity, EntityType, FileResponse } from "../../../api/explorer.ts"; +import { deleteVersion, getFileInfo } from "../../../api/api.ts"; +import { NoWrapTableCell, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import { sizeToString } from "../../../util"; +import UserBadge from "../../Common/User/UserBadge.tsx"; +import MoreVertical from "../../Icons/MoreVertical.tsx"; +import { SquareMenuItem } from "../ContextMenu/ContextMenu.tsx"; +import { downloadSingleFile } from "../../../redux/thunks/download.ts"; +import AutoHeight from "../../Common/AutoHeight.tsx"; +import { confirmOperation } from "../../../redux/thunks/dialog.ts"; +import { openViewers } from "../../../redux/thunks/viewer.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { setFileVersion } from "../../../redux/thunks/file.ts"; +import { AnonymousUser } from "../../Common/User/UserAvatar.tsx"; + +const VersionControl = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + const [actionTarget, setActionTarget] = useState(null); + const [fileExtended, setFileExtended] = useState(undefined); + const [loading, setLoading] = useState(false); + + const open = useAppSelector((state) => state.globalState.versionControlDialogOpen); + const target = useAppSelector((state) => state.globalState.versionControlDialogFile); + const highlight = useAppSelector((state) => state.globalState.versionControlHighlight); + + const onClose = useCallback(() => { + if (!loading) { + dispatch(closeVersionControlDialog()); + } + }, [dispatch, loading]); + + useEffect(() => { + if (target && open) { + if (target.extended_info) { + setFileExtended(target); + } else { + setFileExtended(undefined); + dispatch( + getFileInfo({ + uri: target.path, + extended: true, + }), + ).then((res) => setFileExtended(res)); + } + } + }, [target, open]); + + const versionEntities = useMemo(() => { + return fileExtended?.extended_info?.entities?.filter((e) => e.type == EntityType.version); + }, [fileExtended?.extended_info?.entities]); + + const handleActionClose = () => { + setAnchorEl(null); + }; + + const handleOpenAction = (event: React.MouseEvent, element: Entity) => { + setAnchorEl(event.currentTarget); + setActionTarget(element); + }; + + const downloadEntity = useCallback(() => { + if (!target || !actionTarget) { + return; + } + dispatch(downloadSingleFile(target, actionTarget.id)); + setAnchorEl(null); + }, [target, actionTarget, dispatch]); + + const openEntity = useCallback(() => { + if (!target || !actionTarget) { + return; + } + dispatch(openViewers(FileManagerIndex.main, target, actionTarget.size, actionTarget.id)); + setAnchorEl(null); + }, [target, actionTarget, dispatch]); + + const setAsCurrent = useCallback(() => { + if (!target || !actionTarget) { + return; + } + + setLoading(true); + dispatch(setFileVersion(FileManagerIndex.main, target, actionTarget.id)) + .then(() => { + setFileExtended((prev) => + prev + ? { + ...prev, + primary_entity: actionTarget.id, + } + : undefined, + ); + }) + .finally(() => { + setLoading(false); + }); + + setAnchorEl(null); + }, [target, actionTarget, setLoading, dispatch]); + + const deleteTargetVersion = useCallback(() => { + if (!target || !actionTarget) { + return; + } + + dispatch(confirmOperation(t("fileManager.deleteVersionWarning"))).then(() => { + setLoading(true); + dispatch( + deleteVersion({ + uri: target.path, + version: actionTarget.id, + }), + ) + .then(() => { + setFileExtended((prev) => + prev + ? { + ...prev, + extended_info: prev.extended_info + ? { + ...prev.extended_info, + entities: prev.extended_info.entities?.filter((e) => e.id !== actionTarget.id), + } + : undefined, + } + : undefined, + ); + }) + .finally(() => { + setLoading(false); + }); + }); + + setAnchorEl(null); + }, [t, target, actionTarget, setLoading, dispatch]); + + return ( + <> + + + {t("application:fileManager.open")} + + + {t("application:fileManager.download")} + + {target?.owned && actionTarget?.id !== fileExtended?.primary_entity && ( + + {t("application:fileManager.setAsCurrent")} + + )} + {target?.owned && actionTarget?.id !== fileExtended?.primary_entity && ( + + {t("application:fileManager.delete")} + + )} + + + + + + + + + {t("fileManager.actions")} + {t("fileManager.createdAt")} + {t("fileManager.size")} + {t("fileManager.createdBy")} + {t("application:fileManager.storagePolicy")} + + + + {!fileExtended && ( + + + + + + + + + )} + {versionEntities && + versionEntities.map((e) => ( + + highlight == e.id ? `inset 0 0 0 2px ${theme.palette.primary.light}` : "none", + "&:last-child td, &:last-child th": { border: 0 }, + }} + hover + > + + handleOpenAction(event, e)} size={"small"}> + + + + + + + {sizeToString(e.size)} + + + + {e.storage_policy?.name} + + ))} + +
+ {!versionEntities && fileExtended && ( + + + {t("application:setting.listEmpty")} + + + )} +
+
+
+
+ + ); +}; +export default VersionControl; diff --git a/src/component/FileManager/DnD/DragLayer.js b/src/component/FileManager/DnD/DragLayer.js deleted file mode 100644 index 6b7ee6c..0000000 --- a/src/component/FileManager/DnD/DragLayer.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useMemo } from "react"; -import { useDragLayer } from "react-dnd"; -import Preview from "./Preview"; -import { useSelector } from "react-redux"; - -const layerStyles = { - position: "fixed", - pointerEvents: "none", - zIndex: 100, - left: 0, - top: 0, - width: "100%", - height: "100%", -}; - -function getItemStyles( - initialOffset, - currentOffset, - pointerOffset, - viewMethod -) { - if (!initialOffset || !currentOffset) { - return { - display: "none", - }; - } - let { x, y } = currentOffset; - if (viewMethod === "list") { - x += pointerOffset.x - initialOffset.x; - y += pointerOffset.y - initialOffset.y; - } - - const transform = `translate(${x}px, ${y}px)`; - return { - opacity: 0.5, - transform, - WebkitTransform: transform, - }; -} -const CustomDragLayer = (props) => { - const { - itemType, - isDragging, - item, - initialOffset, - currentOffset, - pointerOffset, - } = useDragLayer((monitor) => ({ - item: monitor.getItem(), - itemType: monitor.getItemType(), - initialOffset: monitor.getInitialSourceClientOffset(), - currentOffset: monitor.getSourceClientOffset(), - pointerOffset: monitor.getInitialClientOffset(), - isDragging: monitor.isDragging(), - })); - const viewMethod = useSelector( - (state) => state.viewUpdate.explorerViewMethod - ); - const image = useMemo(() => { - switch (itemType) { - case "object": - return ; - default: - return null; - } - }, [itemType, item]); - if (!isDragging) { - return null; - } - return ( -
-
- {image} -
-
- ); -}; -export default CustomDragLayer; diff --git a/src/component/FileManager/DnD/DropWarpper.js b/src/component/FileManager/DnD/DropWarpper.js deleted file mode 100644 index 2833ffb..0000000 --- a/src/component/FileManager/DnD/DropWarpper.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import { useDrop } from "react-dnd"; -import Folder from "../Folder"; -import classNames from "classnames"; -import TableItem from "../TableRow"; -export default function FolderDropWarpper({ - isListView, - folder, - onIconClick, - contextMenu, - handleClick, - handleDoubleClick, - className, - pref, -}) { - const [{ canDrop, isOver }, drop] = useDrop({ - accept: "object", - drop: () => ({ folder }), - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); - const isActive = canDrop && isOver; - if (!isListView) { - return ( -
- -
- ); - } - return ( - - ); -} diff --git a/src/component/FileManager/DnD/Preview.js b/src/component/FileManager/DnD/Preview.js deleted file mode 100644 index 93d0370..0000000 --- a/src/component/FileManager/DnD/Preview.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from "react"; -import SmallIcon from "../SmallIcon"; -import FileIcon from "../FileIcon"; -import { useSelector } from "react-redux"; -import { makeStyles } from "@material-ui/core"; -import Folder from "../Folder"; - -const useStyles = makeStyles(() => ({ - dragging: { - width: "200px", - }, - cardDragged: { - position: "absolute", - "transform-origin": "bottom left", - }, -})); - -const diliverIcon = (object, viewMethod, classes) => { - if (object.type === "dir") { - return ( -
- -
- ); - } - if (object.type === "file" && viewMethod === "icon") { - return ( -
- -
- ); - } - if ( - (object.type === "file" && viewMethod === "smallIcon") || - viewMethod === "list" - ) { - return ( -
- -
- ); - } -}; - -const Preview = (props) => { - const selected = useSelector((state) => state.explorer.selected); - const viewMethod = useSelector( - (state) => state.viewUpdate.explorerViewMethod - ); - const classes = useStyles(); - return ( - <> - {selected.length === 0 && - diliverIcon(props.object, viewMethod, classes)} - {selected.length > 0 && ( - <> - {selected.slice(0, 3).map((card, i) => ( -
- {diliverIcon(card, viewMethod, classes)} -
- ))} - - )} - - ); -}; -export default Preview; diff --git a/src/component/FileManager/DnD/Scrolling.js b/src/component/FileManager/DnD/Scrolling.js deleted file mode 100644 index 8c0d030..0000000 --- a/src/component/FileManager/DnD/Scrolling.js +++ /dev/null @@ -1,63 +0,0 @@ -import { useRef } from "react"; -import { throttle } from "lodash"; - -const useDragScrolling = () => { - const isScrolling = useRef(false); - const target = document.querySelector("#explorer-container"); - - const goDown = () => { - target.scrollTop += 10; - - const { offsetHeight, scrollTop, scrollHeight } = target; - const isScrollEnd = offsetHeight + scrollTop >= scrollHeight; - - if (isScrolling.current && !isScrollEnd) { - window.requestAnimationFrame(goDown); - } - }; - - const goUp = () => { - target.scrollTop -= 10; - - if (isScrolling.current && target.scrollTop > 0) { - window.requestAnimationFrame(goUp); - } - }; - - const onDragOver = (event) => { - const isMouseOnTop = event.clientY < 100; - const isMouseOnDown = window.innerHeight - event.clientY < 100; - - if (!isScrolling.current && (isMouseOnTop || isMouseOnDown)) { - isScrolling.current = true; - - if (isMouseOnTop) { - window.requestAnimationFrame(goUp); - } - - if (isMouseOnDown) { - window.requestAnimationFrame(goDown); - } - } else if (!isMouseOnTop && !isMouseOnDown) { - isScrolling.current = false; - } - }; - - const throttleOnDragOver = throttle(onDragOver, 300); - - const addEventListenerForWindow = () => { - window.addEventListener("dragover", throttleOnDragOver, false); - }; - - const removeEventListenerForWindow = () => { - window.removeEventListener("dragover", throttleOnDragOver, false); - isScrolling.current = false; - }; - - return { - addEventListenerForWindow, - removeEventListenerForWindow, - }; -}; - -export default useDragScrolling; diff --git a/src/component/FileManager/Dnd/DisableDropDelay.tsx b/src/component/FileManager/Dnd/DisableDropDelay.tsx new file mode 100644 index 0000000..4f44857 --- /dev/null +++ b/src/component/FileManager/Dnd/DisableDropDelay.tsx @@ -0,0 +1,20 @@ +import { useDrop } from "react-dnd"; +import { useEffect } from "react"; + +const DisableDropDelay = () => { + const [_, bodyDropRef] = useDrop(() => ({ + accept: "file", + drop: () => { + // do something + }, + })); + + useEffect(() => { + bodyDropRef(document.body); + return () => { + bodyDropRef(null); + }; + }, []); +}; + +export default DisableDropDelay; diff --git a/src/component/FileManager/Dnd/DndWrappedFile.tsx b/src/component/FileManager/Dnd/DndWrappedFile.tsx new file mode 100644 index 0000000..b87b9e2 --- /dev/null +++ b/src/component/FileManager/Dnd/DndWrappedFile.tsx @@ -0,0 +1,151 @@ +import { useDrag, useDrop } from "react-dnd"; +import { memo, useCallback, useContext, useEffect } from "react"; +import { FileResponse, FileType } from "../../../api/explorer.ts"; +import { getEmptyImage } from "react-dnd-html5-backend"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { setDragging } from "../../../redux/globalStateSlice.ts"; +import { getFileLinkedUri, mergeRefs } from "../../../util"; +import { processDnd } from "../../../redux/thunks/file.ts"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FileBlockProps } from "../Explorer/Explorer.tsx"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; + +export interface DragItem { + target: FileResponse; + includeSelected?: boolean; +} + +export interface DropResult { + dropEffect: string; + uri?: string; +} + +export const DropEffect = { + copy: "copy", + move: "move", +}; + +export interface UseFileDragProps { + file?: FileResponse; + includeSelected?: boolean; + dropUri?: string; +} + +export const NoOpDropUri = "noop"; +export const useFileDrag = ({ file, includeSelected, dropUri }: UseFileDragProps) => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down("md")); + const fmIndex = useContext(FmIndexContext); + // const { addEventListenerForWindow, removeEventListenerForWindow } = + // useDragScrolling(["#" + MainExplorerContainerID]); + + // @ts-ignore + const [{ isDragging }, drag, preview] = useDrag({ + type: "file", + item: { + target: file, + includeSelected, + }, + end: (item, monitor) => { + // Ignore NoOpDropUri + const target = monitor.getDropResult(); + if (!item || !target || !target.uri || target.uri == NoOpDropUri) { + return; + } + + dispatch(processDnd(0, item as DragItem, target)); + }, + canDrag: () => { + if (!file || fmIndex == FileManagerIndex.selector || isTablet) { + return false; + } + + const crUri = new CrUri(file.path); + return file.owned && crUri.fs() != Filesystem.share; + }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const [{ canDrop, isOver }, drop] = useDrop({ + accept: "file", + drop: () => (file ? { uri: getFileLinkedUri(file) } : { uri: dropUri }), + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + canDrop: (item, _monitor) => { + const dropExist = !!dropUri || (!!file && file.type == FileType.folder); + if (!dropExist || fmIndex == FileManagerIndex.selector) { + return false; + } + + if (!item) { + return false; + } + + return true; + }, + }); + const isActive = canDrop && isOver; + + useEffect(() => { + preview(getEmptyImage(), { captureDraggingState: true }); + // eslint-disable-next-line + }, []); + + useEffect(() => { + if (isDragging) { + // addEventListenerForWindow(); + } + dispatch( + setDragging({ + dragging: isDragging, + draggingWithSelected: !!includeSelected, + }), + ); + }, [isDragging]); + + return [drag, drop, isActive, isDragging] as const; +}; + +export interface DndWrappedFileProps extends FileBlockProps { + component: React.MemoExoticComponent<(props: FileBlockProps) => JSX.Element>; +} + +const DndWrappedFile = memo((props: DndWrappedFileProps) => { + const fmIndex = useContext(FmIndexContext); + const globalDragging = useAppSelector((state) => state.globalState.dndState); + const isSelected = useAppSelector((state) => state.fileManager[fmIndex].selected[props.file.path]); + + const [drag, drop, isOver, isDragging] = useFileDrag({ + file: props.file.placeholder ? undefined : props.file, + includeSelected: true, + }); + + const mergedRef = useCallback( + (val: any) => { + mergeRefs(drop, drag)(val); + }, + [drop, drag], + ); + + const Component = props.component; + + return ( + + ); +}); + +export default DndWrappedFile; diff --git a/src/component/FileManager/Dnd/DragLayer.tsx b/src/component/FileManager/Dnd/DragLayer.tsx new file mode 100644 index 0000000..de38e10 --- /dev/null +++ b/src/component/FileManager/Dnd/DragLayer.tsx @@ -0,0 +1,115 @@ +import { useDragLayer, XYCoord } from "react-dnd"; +import { FileResponse } from "../../../api/explorer.ts"; +import { Badge, Box, Paper, PaperProps } from "@mui/material"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { useEffect, useMemo, useState } from "react"; +import { DragItem } from "./DndWrappedFile.tsx"; +import DisableDropDelay from "./DisableDropDelay.tsx"; +import { FileNameText, Header } from "../Explorer/GridView/GridFile.tsx"; +import FileSmallIcon from "../Explorer/FileSmallIcon.tsx"; + +interface DragPreviewProps extends PaperProps { + files: FileResponse[]; + pointerOffset: XYCoord | null; +} + +const DragPreview = ({ pointerOffset, files, ...rest }: DragPreviewProps) => { + const [size, setSize] = useState([0, 0]); + useEffect(() => { + setSize([220, 48]); + }, []); + if (!files || files.length == 0) { + return undefined; + } + return ( + + theme.transitions.create(["width", "height"]), + }} + {...rest} + > +
+ + {files[0]?.name} +
+
+ {[...Array(Math.min(2, files.length - 1)).keys()].map((i) => ( + + theme.transitions.create(["width", "height"]), + }} + elevation={3} + /> + ))} +
+ ); +}; + +const DragLayer = () => { + DisableDropDelay(); + + const { itemType, isDragging, item, pointerOffset } = useDragLayer( + (monitor) => ({ + item: monitor.getItem(), + itemType: monitor.getItemType(), + pointerOffset: monitor.getClientOffset(), + isDragging: monitor.isDragging(), + }), + ); + + const selected = useAppSelector((state) => state.fileManager[0].selected); + const draggingFiles = useMemo(() => { + if (item && (item as DragItem) && item.target) { + const selectedList = item.includeSelected + ? Object.keys(selected) + .map((key) => selected[key]) + .filter((x) => x.path != item.target.path) + : []; + return [item.target, ...selectedList]; + } + + return []; + }, [selected, item]); + + if (!isDragging) { + return null; + } + + return ( + + + + ); +}; + +export default DragLayer; diff --git a/src/component/FileManager/Dnd/useDndScrolling.ts b/src/component/FileManager/Dnd/useDndScrolling.ts new file mode 100644 index 0000000..7a7764f --- /dev/null +++ b/src/component/FileManager/Dnd/useDndScrolling.ts @@ -0,0 +1,80 @@ +import { useRef } from "react"; +import { throttle } from "lodash"; + +const threshold = 0.1; + +const useDragScrolling = (containers: string[]) => { + const isScrolling = useRef(false); + const targets = containers.map( + (id) => document.querySelector(id) as HTMLElement, + ); + const rects = useRef([]); + + const goDown = (target: HTMLElement) => { + return () => { + target.scrollTop += 5; + + const { offsetHeight, scrollTop, scrollHeight } = target; + const isScrollEnd = offsetHeight + scrollTop >= scrollHeight; + + if (isScrolling.current && !isScrollEnd) { + window.requestAnimationFrame(goDown(target)); + } + }; + }; + + const goUp = (target: HTMLElement) => { + return () => { + target.scrollTop -= 5; + if (isScrolling.current && target.scrollTop > 0) { + window.requestAnimationFrame(goUp(target)); + } + }; + }; + + const onDragOver = (event: MouseEvent) => { + // detect if mouse is in any rect + rects.current.forEach((rect, index) => { + if (event.clientX < rect.left || event.clientX > rect.right) { + isScrolling.current = false; + return; + } + + const height = rect.bottom - rect.top; + if ( + event.clientY > rect.top && + event.clientY < rect.top + threshold * height + ) { + isScrolling.current = true; + window.requestAnimationFrame(goUp(targets[index])); + } else if ( + event.clientY < rect.bottom && + event.clientY > rect.bottom - threshold * height + ) { + isScrolling.current = true; + window.requestAnimationFrame(goDown(targets[index])); + } else { + isScrolling.current = false; + } + }); + }; + + const throttleOnDragOver = throttle(onDragOver, 300); + + const addEventListenerForWindow = () => { + rects.current = targets.map((t) => t.getBoundingClientRect()); + window.addEventListener("dragover", throttleOnDragOver, false); + }; + + const removeEventListenerForWindow = () => { + window.removeEventListener("dragover", throttleOnDragOver, false); + isScrolling.current = false; + }; + + return { + addEventListenerForWindow, + removeEventListenerForWindow, + }; +}; + +export default useDragScrolling; diff --git a/src/component/FileManager/Explorer.js b/src/component/FileManager/Explorer.js deleted file mode 100644 index 5a12abf..0000000 --- a/src/component/FileManager/Explorer.js +++ /dev/null @@ -1,477 +0,0 @@ -import { - CircularProgress, - Grid, - Paper, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from "@material-ui/core"; -import TableSortLabel from "@material-ui/core/TableSortLabel"; -import classNames from "classnames"; -import React, { useCallback, useEffect, useMemo } from "react"; -import { configure, GlobalHotKeys } from "react-hotkeys"; -import explorer, { - changeContextMenu, - openRemoveDialog, - setSelectedTarget, -} from "../../redux/explorer"; -import { isMac } from "../../utils"; -import pathHelper from "../../utils/page"; -import ContextMenu from "./ContextMenu"; -import ImgPreivew from "./ImgPreview"; -import ObjectIcon from "./ObjectIcon"; -import Nothing from "../Placeholder/Nothing"; -import { useDispatch, useSelector } from "react-redux"; -import { useLocation } from "react-router"; -import { usePagination } from "../../hooks/pagination"; -import { makeStyles } from "@material-ui/core/styles"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - paper: { - padding: theme.spacing(2), - textAlign: "center", - color: theme.palette.text.secondary, - margin: "10px", - }, - root: { - padding: "10px", - [theme.breakpoints.up("sm")]: { - height: "calc(100vh - 113px)", - }, - }, - rootTable: { - padding: "0px", - backgroundColor: theme.palette.background.paper.white, - [theme.breakpoints.up("sm")]: { - height: "calc(100vh - 113px)", - }, - }, - typeHeader: { - margin: "10px 25px", - color: "#6b6b6b", - fontWeight: "500", - }, - loading: { - justifyContent: "center", - display: "flex", - marginTop: "40px", - }, - errorBox: { - padding: theme.spacing(4), - }, - errorMsg: { - marginTop: "10px", - }, - hideAuto: { - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - flexFix: { - minWidth: 0, - }, - upButton: { - marginLeft: "20px", - marginTop: "10px", - marginBottom: "10px", - }, - clickAway: { - height: "100%", - width: "100%", - }, - rootShare: { - height: "100%", - minHeight: 500, - }, - visuallyHidden: { - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }, - gridContainer: { - [theme.breakpoints.down("sm")]: { - gridTemplateColumns: - "repeat(auto-fill,minmax(180px,1fr))!important", - }, - [theme.breakpoints.up("md")]: { - gridTemplateColumns: - "repeat(auto-fill,minmax(220px,1fr))!important", - }, - display: "grid!important", - }, - gridItem: { - flex: "1 1 220px!important", - }, -})); - -const keyMap = { - DELETE_FILE: "del", - SELECT_ALL_SHOWED: `${isMac() ? "command" : "ctrl"}+a`, - SELECT_ALL: `${isMac() ? "command" : "ctrl"}+shift+a`, - DESELECT_ALL: "esc", -}; - -export default function Explorer({ share }) { - const { t } = useTranslation("application", { keyPrefix: "fileManager" }); - const location = useLocation(); - const dispatch = useDispatch(); - const selected = useSelector((state) => state.explorer.selected); - const search = useSelector((state) => state.explorer.search); - const loading = useSelector((state) => state.viewUpdate.navigatorLoading); - const path = useSelector((state) => state.navigator.path); - const sortMethod = useSelector((state) => state.viewUpdate.sortMethod); - const navigatorErrorMsg = useSelector( - (state) => state.viewUpdate.navigatorErrorMsg - ); - const navigatorError = useSelector( - (state) => state.viewUpdate.navigatorError - ); - const viewMethod = useSelector( - (state) => state.viewUpdate.explorerViewMethod - ); - - const OpenRemoveDialog = useCallback(() => dispatch(openRemoveDialog()), [ - dispatch, - ]); - const SetSelectedTarget = useCallback( - (targets) => dispatch(setSelectedTarget(targets)), - [dispatch] - ); - const ChangeContextMenu = useCallback( - (type, open) => dispatch(changeContextMenu(type, open)), - [dispatch] - ); - const ChangeSortMethod = useCallback( - (method) => dispatch(explorer.actions.changeSortMethod(method)), - [dispatch] - ); - const SelectAll = useCallback( - () => dispatch(explorer.actions.selectAll()), - [dispatch] - ); - - const { dirList, fileList, startIndex } = usePagination(); - - const handlers = { - DELETE_FILE: () => { - if (selected.length > 0 && !share) { - OpenRemoveDialog(); - } - }, - SELECT_ALL_SHOWED: (e) => { - e.preventDefault(); - if (selected.length >= dirList.length + fileList.length) { - SetSelectedTarget([]); - } else { - SetSelectedTarget([...dirList, ...fileList]); - } - }, - SELECT_ALL: (e) => { - e.preventDefault(); - SelectAll(); - }, - DESELECT_ALL: (e) => { - e.preventDefault(); - SetSelectedTarget([]); - }, - }; - - useEffect( - () => - configure({ - ignoreTags: ["input", "select", "textarea"], - }), - [] - ); - - const contextMenu = (e) => { - e.preventDefault(); - if (!search && !pathHelper.isSharePage(location.pathname)) { - if (!loading) { - ChangeContextMenu("empty", true); - } - } - }; - - const ClickAway = (e) => { - const element = e.target; - if (element.dataset.clickaway) { - SetSelectedTarget([]); - } - }; - - const classes = useStyles(); - const isHomePage = pathHelper.isHomePage(location.pathname); - - const showView = - !loading && (dirList.length !== 0 || fileList.length !== 0); - - const listView = useMemo( - () => ( - - - - - { - ChangeSortMethod( - sortMethod === "namePos" - ? "nameRev" - : "namePos" - ); - }} - > - {t("name")} - {sortMethod === "namePos" || - sortMethod === "nameRev" ? ( - - {sortMethod === "nameRev" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - { - ChangeSortMethod( - sortMethod === "sizePos" - ? "sizeRes" - : "sizePos" - ); - }} - > - {t("size")} - {sortMethod === "sizePos" || - sortMethod === "sizeRes" ? ( - - {sortMethod === "sizeRes" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - { - ChangeSortMethod( - sortMethod === "modifyTimePos" - ? "modifyTimeRev" - : "modifyTimePos" - ); - }} - > - {t("lastModified")} - {sortMethod === "modifyTimePos" || - sortMethod === "modifyTimeRev" ? ( - - {sortMethod === "sizeRes" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - - - - - {pathHelper.isMobile() && path !== "/" && ( - - )} - {dirList.map((value, index) => ( - - ))} - {fileList.map((value, index) => ( - - ))} - -
- ), - [dirList, fileList, path, sortMethod, ChangeSortMethod, classes] - ); - - const normalView = useMemo( - () => ( -
- {dirList.length !== 0 && ( - <> - - {t("folders")} - - - {dirList.map((value, index) => ( - - - - ))} - - - )} - {fileList.length !== 0 && ( - <> - - {t("files")} - - - {fileList.map((value, index) => ( - - - - ))} - - - )} -
- ), - [dirList, fileList, classes] - ); - - const view = viewMethod === "list" ? listView : normalView; - - return ( -
- - - - {navigatorError && ( - - - {t("listError")} - - - {navigatorErrorMsg.message} - - - )} - - {loading && !navigatorError && ( -
- -
- )} - - {!search && - isHomePage && - dirList.length === 0 && - fileList.length === 0 && - !loading && - !navigatorError && ( - - )} - {((search && - dirList.length === 0 && - fileList.length === 0 && - !loading && - !navigatorError) || - (dirList.length === 0 && - fileList.length === 0 && - !loading && - !navigatorError && - !isHomePage)) && } - {showView && view} -
- ); -} diff --git a/src/component/FileManager/Explorer/EmojiIcon.tsx b/src/component/FileManager/Explorer/EmojiIcon.tsx new file mode 100644 index 0000000..fa3de32 --- /dev/null +++ b/src/component/FileManager/Explorer/EmojiIcon.tsx @@ -0,0 +1,24 @@ +import { SvgIconProps, Typography } from "@mui/material"; + +export interface EmojiIconProps extends SvgIconProps { + emoji: string; +} + +const EmojiIcon = ({ sx, fontSize, emoji, ...rest }: EmojiIconProps) => { + return ( + theme.palette.text.primary, + minWidth: "24px", + pl: "4px", + ...sx, + }} + fontSize={fontSize} + {...rest} + > + {emoji} + + ); +}; + +export default EmojiIcon; diff --git a/src/component/FileManager/Explorer/EmptyFileList.tsx b/src/component/FileManager/Explorer/EmptyFileList.tsx new file mode 100644 index 0000000..6dbbf67 --- /dev/null +++ b/src/component/FileManager/Explorer/EmptyFileList.tsx @@ -0,0 +1,235 @@ +import { + Alert, + AlertTitle, + Box, + ListItemIcon, + ListItemText, + MenuList, + Paper, + Stack, + Tooltip, + Typography, +} from "@mui/material"; +import { grey } from "@mui/material/colors"; +import React, { memo, useContext, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { NavigatorCapability } from "../../../api/explorer.ts"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { isMacbook } from "../../../redux/thunks/file.ts"; +import Boolset from "../../../util/boolset.ts"; +import { Filesystem } from "../../../util/uri.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import { KeyIndicator } from "../../Frame/NavBar/SearchBar.tsx"; +import ArrowSync from "../../Icons/ArrowSync.tsx"; +import Border from "../../Icons/Border.tsx"; +import BorderAll from "../../Icons/BorderAll.tsx"; +import BorderInside from "../../Icons/BorderInside.tsx"; +import FolderLink from "../../Icons/FolderLink.tsx"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import PinOutlined from "../../Icons/PinOutlined.tsx"; +import StorageOutlined from "../../Icons/StorageOutlined.tsx"; +import { DenseDivider, SquareMenuItem } from "../ContextMenu/ContextMenu.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { ActionButton, ActionButtonGroup } from "../TopBar/TopActions.tsx"; + +interface EmptyFileListProps { + [key: string]: any; +} + +export const SearchLimitReached = () => { + const { t } = useTranslation("application"); + return ( + + {t("fileManager.recursiveLimitReached")} + {t("fileManager.recursiveLimitReachedDes")} + + ); +}; + +export const SharedWithMeEmpty = () => { + const { t } = useTranslation("application"); + + return ( + + (t.palette.mode == "dark" ? grey[900] : grey[100]), + borderRadius: (t) => `${t.shape.borderRadius}px`, + p: 1, + position: "relative", + "&::after": { + content: '""', + position: "absolute", + bottom: 0, + left: 0, + width: "100%", + height: "50px", + background: (t) => + `linear-gradient(to bottom, transparent, ${t.palette.mode == "dark" ? grey[900] : grey[100]})`, + pointerEvents: "none", + }, + }} + > + + + `1px solid ${t.palette.divider}`, + backgroundColor: (t) => t.palette.background.paper, + height: "42px", + borderRadius: (t) => `${t.shape.borderRadius}px`, + }} + /> + t.palette.background.paper, + height: "42px", + }} + > + + + + + + + + + + + `1px solid ${t.palette.primary.main}`, + }} + > + + + + + + + + + + + + + {t("application:fileManager.pin")} + + + + + + + + {t("application:fileManager.saveShortcut")} + + + + + + + + + {t("application:fileManager.pin")} + + + + + + + {t("application:fileManager.selectAll")} + + {isMacbook ? "⌘" : "Crtl"}+A + + + + + + + {t("application:fileManager.selectNone")} + + + + + + {t("application:fileManager.invertSelection")} + + + + + + + + + {t("application:fileManager.shareWithMeEmpty")} + + + {t("application:fileManager.shareWithMeEmptyDes")} + + + + ); +}; + +const EmptyFileList = memo( + React.forwardRef(({ ...rest }: EmptyFileListProps, ref) => { + const { t } = useTranslation("application"); + const fmIndex = useContext(FmIndexContext); + const currentFs = useAppSelector((state) => state.fileManager[fmIndex]?.current_fs); + const search_params = useAppSelector((state) => state.fileManager[fmIndex]?.search_params); + const recursion_limit_reached = useAppSelector((state) => state.fileManager[fmIndex].list?.recursion_limit_reached); + const capability = useAppSelector((state) => state.fileManager[fmIndex].list?.props.capability); + + const canCreate = useMemo(() => { + const bs = new Boolset(capability); + return bs.enabled(NavigatorCapability.create_file); + }, [capability]); + + return ( + + {currentFs == Filesystem.shared_with_me && ( + <> + + {recursion_limit_reached && } + + )} + {currentFs != Filesystem.shared_with_me && ( + <> + + {recursion_limit_reached && } + + )} + + ); + }), +); + +export default EmptyFileList; diff --git a/src/component/FileManager/Explorer/Explorer.tsx b/src/component/FileManager/Explorer/Explorer.tsx new file mode 100644 index 0000000..4a5df60 --- /dev/null +++ b/src/component/FileManager/Explorer/Explorer.tsx @@ -0,0 +1,158 @@ +import { Box, useMediaQuery, useTheme } from "@mui/material"; +import React, { RefCallback, useCallback, useContext, useEffect, useMemo } from "react"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { useAreaSelection } from "../../../hooks/areaSelection.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { ConfigLoadState } from "../../../redux/siteConfigSlice.ts"; +import { openEmptyContextMenu } from "../../../redux/thunks/filemanager.ts"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import CircularProgress from "../../Common/CircularProgress.tsx"; +import "../../Common/FadeTransition.css"; +import { RadiusFrame } from "../../Frame/RadiusFrame.tsx"; +import ExplorerError from "./ExplorerError.tsx"; +import GridView, { FmFile } from "./GridView/GridView.tsx"; + +import { Layouts } from "../../../redux/fileManagerSlice.ts"; +import { SearchParam } from "../../../util/uri.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import EmptyFileList, { SearchLimitReached } from "./EmptyFileList.tsx"; +import GalleryView from "./GalleryView/GalleryView.tsx"; +import { ListViewColumn } from "./ListView/Column.tsx"; +import ListView from "./ListView/ListView.tsx"; +import SingleFileView from "./SingleFileView.tsx"; + +export const ExplorerPage = { + Error: 1, + Loading: 2, + GridView: 0, + SingleFileView: 3, + Empty: 4, + ListView: 5, + GalleryView: 6, +}; + +export interface FileBlockProps { + showThumb?: boolean; + file: FmFile; + isDragging?: boolean; + isDropOver?: boolean; + dragRef?: RefCallback; + index?: number; + search?: SearchParam; + columns?: ListViewColumn[]; +} + +const Explorer = () => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isTouch = useMediaQuery("(pointer: coarse)"); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const fmIndex = useContext(FmIndexContext); + const loading = useAppSelector((state) => state.fileManager[fmIndex].loading); + const error = useAppSelector((state) => state.fileManager[fmIndex].error); + const showError = useAppSelector((state) => state.fileManager[fmIndex].showError); + const singleFileView = useAppSelector((state) => state.fileManager[fmIndex].list?.single_file_view); + const explorerConfigLoading = useAppSelector((state) => state.siteConfig.explorer.loaded); + const files = useAppSelector((state) => state.fileManager[fmIndex].list?.files); + const recursion_limit_reached = useAppSelector((state) => state.fileManager[fmIndex].list?.recursion_limit_reached); + const layout = useAppSelector((state) => state.fileManager[fmIndex].layout); + + const selectContainerRef = React.useRef(null); + + useEffect(() => { + dispatch(loadSiteConfig("explorer")); + }, []); + + const index = useMemo(() => { + if (showError) { + return ExplorerPage.Error; + } else if (loading || explorerConfigLoading == ConfigLoadState.NotLoaded) { + return ExplorerPage.Loading; + } else { + if (files?.length === 0) { + return ExplorerPage.Empty; + } + + if (singleFileView && fmIndex == FileManagerIndex.main) { + return ExplorerPage.SingleFileView; + } + + switch (layout) { + case Layouts.grid: + return ExplorerPage.GridView; + case Layouts.list: + return ExplorerPage.ListView; + case Layouts.gallery: + return ExplorerPage.GalleryView; + default: + return ExplorerPage.GridView; + } + } + }, [loading, showError, explorerConfigLoading, singleFileView, fmIndex, files?.length, layout]); + + const enableAreaSelection = index == ExplorerPage.GridView; + + const [handleMouseDown, handleMouseUp, handleMouseMove] = useAreaSelection( + selectContainerRef, + fmIndex, + enableAreaSelection, + ); + + const onContextMenu = useCallback( + (e: React.MouseEvent) => { + if (index == ExplorerPage.Error || index == ExplorerPage.Loading) return; + dispatch(openEmptyContextMenu(fmIndex, e)); + }, + [dispatch, index], + ); + + return ( + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={index} + > + + {index == ExplorerPage.Error && } + {index == ExplorerPage.Loading && ( + + + + )} + {index == ExplorerPage.GridView && } + {index == ExplorerPage.SingleFileView && } + {index == ExplorerPage.Empty && } + {index == ExplorerPage.ListView && } + {index == ExplorerPage.GalleryView && } + {recursion_limit_reached && (index == ExplorerPage.GridView || index == ExplorerPage.GalleryView) && ( + + + + )} + + + + + ); +}; + +export default Explorer; diff --git a/src/component/FileManager/Explorer/ExplorerError.tsx b/src/component/FileManager/Explorer/ExplorerError.tsx new file mode 100644 index 0000000..a9044b5 --- /dev/null +++ b/src/component/FileManager/Explorer/ExplorerError.tsx @@ -0,0 +1,118 @@ +import { Alert, AlertTitle, Box, Button, Typography } from "@mui/material"; +import React, { memo, useCallback, useContext, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AppError, Code, Response } from "../../../api/request.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { navigateToPath, retrySharePassword } from "../../../redux/thunks/filemanager.ts"; +import { Filesystem } from "../../../util/uri.ts"; +import { FilledTextField } from "../../Common/StyledComponents.tsx"; +import ArrowLeft from "../../Icons/ArrowLeft.tsx"; +import LinkDismiss from "../../Icons/LinkDismiss.tsx"; +import LockClosed from "../../Icons/LockClosed.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +interface ExplorerErrorProps { + error?: Response; + [key: string]: any; +} + +const RetryPassword = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const [password, setPassword] = useState(""); + + return ( + e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}> + + + setPassword(e.target.value)} + label={t("application:share.enterPassword")} + /> + + + + ); +}; + +const ExplorerError = memo( + React.forwardRef(({ error, ...rest }: ExplorerErrorProps, ref) => { + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const fs = useAppSelector((state) => state.fileManager[fmIndex].current_fs); + const previousPath = useAppSelector((state) => state.fileManager[fmIndex].previous_path); + const { t } = useTranslation("application"); + const appErr = useMemo(() => { + if (error) { + return new AppError(error); + } + + return undefined; + }, [error]); + const navigateBack = useCallback(() => { + previousPath && dispatch(navigateToPath(fmIndex, previousPath)); + }, [dispatch, fmIndex, previousPath]); + + const innerError = () => { + switch (error?.code) { + case Code.IncorrectPassword: + return ; + // @ts-ignore + case Code.NodeFound: + if (fs == Filesystem.share) { + return ( + + + {t("application:share.shareNotExist")} + + ); + } + default: + return ( + + {t("application:fileManager.listError")} + {appErr && appErr.message} + {error?.correlation_id && ( + + {t("common:requestID", { id: error.correlation_id })} + + )} + + ); + } + }; + return ( + + {innerError()} + + ); + }), +); + +export default ExplorerError; diff --git a/src/component/FileManager/Explorer/FileIcon.tsx b/src/component/FileManager/Explorer/FileIcon.tsx new file mode 100644 index 0000000..62b664d --- /dev/null +++ b/src/component/FileManager/Explorer/FileIcon.tsx @@ -0,0 +1,116 @@ +import { Avatar, Badge, BadgeProps, Box, Skeleton, styled, SvgIconProps, Tooltip } from "@mui/material"; +import { forwardRef, memo, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { FileResponse, FileType, Metadata } from "../../../api/explorer.ts"; +import UserAvatar from "../../Common/User/UserAvatar.tsx"; +import ShareAndroid from "../../Icons/ShareAndroid.tsx"; +import EmojiIcon from "./EmojiIcon.tsx"; +import FileTypeIcon from "./FileTypeIcon.tsx"; + +export interface FileIconProps { + file?: FileResponse; + variant?: "default" | "small" | "large"; + loading?: boolean; + notLoaded?: boolean; + [key: string]: any; + iconProps?: SvgIconProps; +} + +interface StyledBadgeProps extends BadgeProps { + iconVariant?: "default" | "small" | "large" | "largeMobile" | "shareSingle"; +} + +const StyledBadge = styled(Badge)(({ iconVariant }) => ({ + "& .MuiBadge-badge": { + right: 3, + top: variantTop[iconVariant ?? "default"], + padding: "0", + height: "initial", + minWidth: "initial", + }, + verticalAlign: "initial", +})); + +const variantTop = { + default: 18, + small: 15, + large: 70, + largeMobile: 52, + shareSingle: 26, +}; + +const variantAvatarSize = { + default: 16, + small: 13, + large: 32, + largeMobile: 24, + shareSingle: 20, +}; + +const FileIcon = memo( + forwardRef(({ file, loading, variant = "default", iconProps, notLoaded, sx, ...rest }: FileIconProps, ref) => { + const { t } = useTranslation(); + const iconColor = useMemo(() => { + if (file && file.metadata && file.metadata[Metadata.icon_color]) { + return file.metadata[Metadata.icon_color]; + } + }, [file]); + const typedIcon = useMemo(() => { + if (file?.metadata?.[Metadata.emoji]) { + const { sx, ...restIcon } = iconProps ?? {}; + return ; + } + return ( + + ); + }, [file, iconColor, iconProps, notLoaded]); + const badgeContent = useMemo(() => { + const avatarSize = variantAvatarSize[variant]; + if (file?.metadata?.[Metadata.share_redirect]) { + return ( + + ); + } else if (file?.shared) { + return ( + + theme.palette.background.default, + }} + > + + + + ); + } + }, [file, variant]); + return ( + + {!loading && + (badgeContent ? ( + + {typedIcon} + + ) : ( + typedIcon + ))} + {loading && } + + ); + }), +); + +export default FileIcon; diff --git a/src/component/FileManager/Explorer/FileSmallIcon.tsx b/src/component/FileManager/Explorer/FileSmallIcon.tsx new file mode 100644 index 0000000..0fbe652 --- /dev/null +++ b/src/component/FileManager/Explorer/FileSmallIcon.tsx @@ -0,0 +1,95 @@ +import { Box, Fade } from "@mui/material"; +import { memo, useCallback, useContext } from "react"; +import { TransitionGroup } from "react-transition-group"; +import { FileResponse } from "../../../api/explorer.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { fileIconClicked } from "../../../redux/thunks/file.ts"; +import CheckmarkCircle from "../../Icons/CheckmarkCircle.tsx"; +import CheckUnchecked from "../../Icons/CheckUnchecked.tsx"; +import FileIcon from "./FileIcon.tsx"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; + +export interface FileSmallIconProps { + selected: boolean; + file: FileResponse; + loading?: boolean; + ignoreHovered?: boolean; + variant?: "list" | "grid"; +} + +const FileSmallIcon = memo(({ selected, variant, loading, file, ignoreHovered }: FileSmallIconProps) => { + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const hovered = useAppSelector((state) => state.fileManager[fmIndex].multiSelectHovered[file.path]); + const onIconClick = useCallback( + (e: React.MouseEvent) => { + if (!loading) { + return dispatch(fileIconClicked(fmIndex, file, e)); + } + }, + [file, loading, dispatch], + ); + const isInList = variant === "list"; + return ( + + {!selected && (!hovered || ignoreHovered) && ( + + + + )} + {!selected && hovered && !ignoreHovered && ( + + + + + + )} + {selected && ( + + + + + + )} + + + ); +}); + +export default FileSmallIcon; diff --git a/src/component/FileManager/Explorer/FileTag.tsx b/src/component/FileManager/Explorer/FileTag.tsx new file mode 100644 index 0000000..c14acf7 --- /dev/null +++ b/src/component/FileManager/Explorer/FileTag.tsx @@ -0,0 +1,101 @@ +import { + Chip, + ChipProps, + darken, + styled, + Tooltip, + useTheme, +} from "@mui/material"; +import { useCallback, useContext } from "react"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { Metadata } from "../../../api/explorer.ts"; +import { searchMetadata } from "../../../redux/thunks/filemanager.ts"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +export const TagChip = styled(Chip)<{ defaultStyle?: boolean }>(({ + defaultStyle, +}) => { + const base = { + "& .MuiChip-deleteIcon": {}, + }; + if (!defaultStyle) return { ...base, height: 18, minWidth: 32 }; + return base; +}); + +export interface FileTagProps extends ChipProps { + tagColor?: string; + defaultStyle?: boolean; + spacing?: number; + openInNewTab?: boolean; + disableClick?: boolean; +} + +const FileTag = ({ + disableClick, + tagColor, + sx, + label, + defaultStyle, + spacing, + openInNewTab, + ...rest +}: FileTagProps) => { + const theme = useTheme(); + const fmIndex = useContext(FmIndexContext); + const dispatch = useAppDispatch(); + const root = useAppSelector((state) => state.fileManager[fmIndex].path_root); + const stopPropagation = useCallback( + (e: any) => { + if (!disableClick) e.stopPropagation(); + }, + [disableClick], + ); + const onClick = useCallback( + (e: any) => { + if (disableClick) { + return; + } + e.stopPropagation(); + dispatch( + searchMetadata( + fmIndex, + Metadata.tag_prefix + label, + tagColor, + openInNewTab, + ), + ); + }, + [root, dispatch, fmIndex, disableClick], + ); + + const hackColor = + !!tagColor && + theme.palette.getContrastText(tagColor) != theme.palette.text.primary + ? "error" + : undefined; + return ( + + theme.palette.getContrastText(tagColor), + "&:hover": { + backgroundColor: darken(tagColor, 0.1), + }, + }, + spacing !== undefined && { mr: spacing }, + ]} + onClick={onClick} + onMouseDown={stopPropagation} + size="small" + label={label} + color={hackColor} + {...rest} + /> + + ); +}; + +export default FileTag; diff --git a/src/component/FileManager/Explorer/FileTagSummary.tsx b/src/component/FileManager/Explorer/FileTagSummary.tsx new file mode 100644 index 0000000..d6e8117 --- /dev/null +++ b/src/component/FileManager/Explorer/FileTagSummary.tsx @@ -0,0 +1,97 @@ +import * as React from "react"; +import { memo, useCallback, useMemo } from "react"; +import { Box, Popover, Stack, useMediaQuery, useTheme } from "@mui/material"; +import { bindHover, bindPopover } from "material-ui-popup-state"; +import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import FileTag, { TagChip } from "./FileTag.tsx"; + +export interface FileTagSummaryProps { + tags: { key: string; value: string }[]; + max?: number; + [key: string]: any; +} + +const FileTagSummary = memo( + ({ tags, sx, max = 1, ...restProps }: FileTagSummaryProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const popupState = usePopupState({ + variant: "popover", + popupId: "demoMenu", + }); + + const { open, ...rest } = bindPopover(popupState); + const stopPropagation = useCallback((e: any) => e.stopPropagation(), []); + const [shown, hidden] = useMemo(() => { + if (tags.length <= max) { + return [tags, []]; + } + return [tags.slice(0, max), tags.slice(max)]; + }, [tags, max]); + + const { onClick, ...triggerProps } = bindTrigger(popupState); + const onMobileClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick(e); + }; + + const PopoverComponent = isMobile ? Popover : HoverPopover; + + return ( + + {shown.map((tag) => ( + + ))} + {hidden.length > 0 && ( + + )} + + {open && ( + + + {hidden.map((tag, i) => ( + + ))} + + + )} + + ); + }, +); + +export default FileTagSummary; diff --git a/src/component/FileManager/Explorer/FileTypeIcon.tsx b/src/component/FileManager/Explorer/FileTypeIcon.tsx new file mode 100644 index 0000000..08912ea --- /dev/null +++ b/src/component/FileManager/Explorer/FileTypeIcon.tsx @@ -0,0 +1,162 @@ +import { Android } from "@mui/icons-material"; +import { Box, SvgIconProps, useTheme } from "@mui/material"; +import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; +import { useMemo } from "react"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { fileExtension } from "../../../util"; +import Book from "../../Icons/Book.tsx"; +import Document from "../../Icons/Document.tsx"; +import DocumentFlowchart from "../../Icons/DocumentFlowchart.tsx"; +import DocumentPDF from "../../Icons/DocumentPDF.tsx"; +import FileExclBox from "../../Icons/FileExclBox.tsx"; +import FilePowerPointBox from "../../Icons/FilePowerPointBox.tsx"; +import FileWordBox from "../../Icons/FileWordBox.tsx"; +import Folder from "../../Icons/Folder.tsx"; +import FolderOutlined from "../../Icons/FolderOutlined.tsx"; +import FolderZip from "../../Icons/FolderZip.tsx"; +import Image from "../../Icons/Image.tsx"; +import LanguageC from "../../Icons/LanguageC.tsx"; +import LanguageCPP from "../../Icons/LanguageCPP.tsx"; +import LanguageGo from "../../Icons/LanguageGo.tsx"; +import LanguageJS from "../../Icons/LanguageJS.tsx"; +import LanguagePython from "../../Icons/LanguagePython.tsx"; +import LanguageRust from "../../Icons/LanguageRust.tsx"; +import MagnetOn from "../../Icons/MagnetOn.tsx"; +import Markdown from "../../Icons/Markdown.tsx"; +import MusicNote1 from "../../Icons/MusicNote1.tsx"; +import Notepad from "../../Icons/Notepad.tsx"; +import Raw from "../../Icons/Raw.tsx"; +import Video from "../../Icons/Video.tsx"; +import Whiteboard from "../../Icons/Whiteboard.tsx"; +import WindowApps from "../../Icons/WindowApps.tsx"; + +export interface FileTypeIconProps extends SvgIconProps { + name: string; + fileType: number; + notLoaded?: boolean; + customizedColor?: string; + [key: string]: any; +} + +export interface FileTypeIconSetting { + exts: string[]; + icon?: string; + img?: string; + color?: string; + color_dark?: string; +} + +export interface ExpandedIconSettings { + [key: string]: FileTypeIconSetting; +} + +export const builtInIcons: { + [key: string]: typeof SvgIcon | ((props: SvgIconProps) => JSX.Element); +} = { + audio: MusicNote1, + video: Video, + image: Image, + pdf: DocumentPDF, + word: FileWordBox, + ppt: FilePowerPointBox, + excel: FileExclBox, + text: Notepad, + torrent: MagnetOn, + zip: FolderZip, + exe: WindowApps, + android: Android, + go: LanguageGo, + c: LanguageC, + cpp: LanguageCPP, + js: LanguageJS, + python: LanguagePython, + book: Book, + rust: LanguageRust, + raw: Raw, + flowchart: DocumentFlowchart, + whiteboard: Whiteboard, + markdown: Markdown, +}; + +interface TypeIcon { + icon?: typeof SvgIcon | ((props: SvgIconProps) => JSX.Element); + color?: string; + color_dark?: string; + img?: string; + hideUnknown?: boolean; +} + +const FileTypeIcon = ({ name, fileType, notLoaded, sx, hideUnknown, customizedColor, ...rest }: FileTypeIconProps) => { + const theme = useTheme(); + const iconOptions = useAppSelector((state) => state.siteConfig.explorer.typed?.icons) as ExpandedIconSettings; + const IconComponent = useMemo(() => { + if (fileType === 1) { + return notLoaded ? { icon: FolderOutlined } : { icon: Folder }; + } + + if (name) { + const fileSuffix = fileExtension(name); + if (fileSuffix && iconOptions) { + const options = iconOptions[fileSuffix]; + if (options) { + const { icon, color, color_dark, img } = options; + if (icon) { + return { + icon: builtInIcons[icon], + color, + color_dark, + }; + } else if (img) { + return { + img, + }; + } + } + } + } + + return { icon: Document, isDefault: true }; + }, [fileType, name, notLoaded]); + + const iconColor = useMemo(() => { + if (customizedColor) { + return customizedColor; + } + if (theme.palette.mode == "dark") { + return IconComponent.color_dark ?? IconComponent.color ?? theme.palette.action.active; + } else { + return IconComponent.color ?? theme.palette.action.active; + } + }, [IconComponent, theme, customizedColor]); + + if (IconComponent.icon) { + if (IconComponent.isDefault && hideUnknown) { + return <>; + } + return ( + + ); + } else { + return ( + //@ts-ignore + + ); + } +}; + +export default FileTypeIcon; diff --git a/src/component/FileManager/Explorer/GalleryView/GalleryImage.tsx b/src/component/FileManager/Explorer/GalleryView/GalleryImage.tsx new file mode 100644 index 0000000..57bc53b --- /dev/null +++ b/src/component/FileManager/Explorer/GalleryView/GalleryImage.tsx @@ -0,0 +1,244 @@ +import { FileBlockProps } from "../Explorer.tsx"; +import React, { memo, useCallback, useEffect, useState } from "react"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { + LargeIconContainer, + ThumbBox, + ThumbBoxContainer, + ThumbLoadingPlaceholder, + useFileBlockState, +} from "../GridView/GridFile.tsx"; +import { navigateReconcile } from "../../../../redux/thunks/filemanager.ts"; +import { + Box, + Fade, + IconButton, + ImageListItem, + ImageListItemBar, + lighten, + styled, +} from "@mui/material"; +import { + fileIconClicked, + loadFileThumb, +} from "../../../../redux/thunks/file.ts"; +import FileIcon from "../FileIcon.tsx"; +import { TransitionGroup } from "react-transition-group"; +import { FileType, Metadata } from "../../../../api/explorer.ts"; +import CheckUnchecked from "../../../Icons/CheckUnchecked.tsx"; +import { CheckCircle } from "@mui/icons-material"; + +const StyledImageListItem = styled(ImageListItem)<{ + transparent?: boolean; + disabled?: boolean; + isDropOver?: boolean; +}>(({ transparent, isDropOver, disabled, theme }) => { + return { + opacity: transparent || disabled ? 0.5 : 1, + pointerEvents: disabled ? "none" : "auto", + cursor: "pointer", + boxShadow: isDropOver ? `0 0 0 2px ${theme.palette.primary.light}` : "none", + transition: theme.transitions.create([ + "height", + "width", + "opacity", + "box-shadow", + ]), + }; +}); + +const GalleryImage = memo((props: FileBlockProps) => { + const { file, columns, search, isDragging, isDropOver } = props; + const dispatch = useAppDispatch(); + + const { + fmIndex, + isSelected, + isLoadingIndicator, + noThumb, + uploading, + ref, + inView, + showLock, + fileTag, + onClick, + onDoubleClicked, + hoverStateOff, + hoverStateOn, + onContextMenu, + setRefFunc, + disabled, + fileDisabled, + } = useFileBlockState(props); + + const [hovered, setHovered] = useState(false); + + // undefined: not loaded, null: no thumb + const [thumbSrc, setThumbSrc] = useState( + noThumb ? null : undefined, + ); + const [imageLoading, setImageLoading] = useState(true); + + const tryLoadThumbSrc = useCallback(async () => { + const thumbSrc = await dispatch(loadFileThumb(0, file)); + setThumbSrc(thumbSrc); + }, [dispatch, file, setThumbSrc, setImageLoading]); + + const onImgLoadError = useCallback(() => { + setImageLoading(false); + setThumbSrc(null); + }, [setImageLoading, setThumbSrc]); + + useEffect(() => { + if (!inView) { + return; + } + + if (isLoadingIndicator) { + if (file.first) { + dispatch(navigateReconcile(fmIndex, { next_page: true })); + } + return; + } + + if (file.type == FileType.folder) { + return; + } + + if ( + (file.metadata && file.metadata[Metadata.thumbDisabled] !== undefined) || + showLock + ) { + // No thumb available + setThumbSrc(null); + return; + } + + tryLoadThumbSrc(); + }, [inView]); + + const onIconClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + return dispatch(fileIconClicked(fmIndex, file, e)); + }, + [file, dispatch], + ); + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + {thumbSrc && ( + + + + isSelected + ? lighten(theme.palette.primary.light, 0.85) + : "initial", + transition: (theme) => + theme.transitions.create(["padding"], { + duration: theme.transitions.duration.shortest, + }), + }} + > + setImageLoading(false)} + onError={onImgLoadError} + /> + + + + )} + {(thumbSrc === undefined || (thumbSrc && imageLoading)) && ( + + + + )} + {thumbSrc === null && ( + + + + + + )} + + + + + {!isSelected && ( + + + + + + )} + {isSelected && ( + + + + + + )} + + + } + actionPosition="left" + /> + + + ); +}); + +export default GalleryImage; diff --git a/src/component/FileManager/Explorer/GalleryView/GalleryView.tsx b/src/component/FileManager/Explorer/GalleryView/GalleryView.tsx new file mode 100644 index 0000000..befaebc --- /dev/null +++ b/src/component/FileManager/Explorer/GalleryView/GalleryView.tsx @@ -0,0 +1,129 @@ +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { Box, ImageList } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import { FmFile, loadingPlaceHolderNumb } from "../GridView/GridView.tsx"; +import DndWrappedFile from "../../Dnd/DndWrappedFile.tsx"; +import GalleryImage from "./GalleryImage.tsx"; +import { mergeRefs } from "../../../../util"; + +const GalleryView = React.forwardRef( + ( + { + ...rest + }: { + [key: string]: any; + }, + ref, + ) => { + const { t } = useTranslation("application"); + const dispatch = useAppDispatch(); + const containerRef = useRef(); + const fmIndex = useContext(FmIndexContext); + const [boxHeight, setBoxHeight] = useState(0); + const [col, setCol] = useState(0); + + const files = useAppSelector( + (state) => state.fileManager[fmIndex].list?.files, + ); + const pagination = useAppSelector( + (state) => state.fileManager[fmIndex].list?.pagination, + ); + const search_params = useAppSelector( + (state) => state.fileManager[fmIndex]?.search_params, + ); + const galleryWidth = useAppSelector( + (state) => state.fileManager[fmIndex].galleryWidth, + ); + + const mergedRef = useCallback( + (val: any) => { + mergeRefs(containerRef, ref)(val); + }, + [containerRef, ref], + ); + + const list = useMemo(() => { + const list: FmFile[] = []; + if (!files) { + return list; + } + + files.forEach((file) => { + list.push(file); + }); + + // Add loading placeholder if there is next page + if (pagination && pagination.next_token) { + for (let i = 0; i < loadingPlaceHolderNumb; i++) { + const id = `loadingPlaceholder-${pagination.next_token}-${i}`; + list.push({ + ...files[0], + path: files[0].path + "/" + id, + id: `loadingPlaceholder-${pagination.next_token}-${i}`, + first: i == 0, + placeholder: true, + }); + } + } + return list; + }, [files, pagination, search_params]); + + const resizeGallery = useCallback( + (containerWidth: number, boxSize: number) => { + const boxCount = Math.floor(containerWidth / boxSize); + const newCols = Math.max(1, boxCount); + const boxHeight = containerWidth / newCols; + setBoxHeight(boxHeight); + setCol(newCols); + }, + [setBoxHeight, setCol], + ); + + useEffect(() => { + if (!containerRef.current) return; + const resizeObserver = new ResizeObserver(() => { + const containerWidth = containerRef.current?.clientWidth ?? 100; + resizeGallery(containerWidth, galleryWidth); + }); + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); // clean up + }, [galleryWidth]); + + return ( + + + {list.map((file, index) => ( + + ))} + + + ); + }, +); + +export default GalleryView; diff --git a/src/component/FileManager/Explorer/GridView/GridFile.tsx b/src/component/FileManager/Explorer/GridView/GridFile.tsx new file mode 100644 index 0000000..8679b90 --- /dev/null +++ b/src/component/FileManager/Explorer/GridView/GridFile.tsx @@ -0,0 +1,463 @@ +import { + alpha, + Box, + ButtonBase, + Fade, + Skeleton, + styled, + Tooltip, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover } from "material-ui-popup-state"; +import { usePopupState } from "material-ui-popup-state/hooks"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useInView } from "react-intersection-observer"; +import { TransitionGroup } from "react-transition-group"; +import { FileType, Metadata } from "../../../../api/explorer.ts"; +import { bindDelayedHover } from "../../../../hooks/delayedHover.tsx"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { fileClicked, fileDoubleClicked, loadFileThumb, openFileContextMenu } from "../../../../redux/thunks/file.ts"; +import { fileHovered, navigateReconcile } from "../../../../redux/thunks/filemanager.ts"; +import FileIcon from "../FileIcon.tsx"; +import FileSmallIcon from "../FileSmallIcon.tsx"; +import FileTagSummary from "../FileTagSummary.tsx"; +// @ts-ignore +import Highlighter from "react-highlight-words"; + +import { ContextMenuTypes } from "../../../../redux/fileManagerSlice.ts"; +import { FileManagerIndex } from "../../FileManager.tsx"; +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import { getFileTags } from "../../Sidebar/Tags.tsx"; +import { FileBlockProps } from "../Explorer.tsx"; +import UploadingTag from "../UploadingTag.tsx"; + +const StyledButtonBase = styled(ButtonBase)<{ + selected: boolean; + square?: boolean; + transparent?: boolean; + isDropOver?: boolean; +}>(({ theme, transparent, isDropOver, square, selected }) => { + let bgColor = theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]; + let bgColorHover = theme.palette.mode === "light" ? theme.palette.grey[300] : theme.palette.grey[700]; + + if (selected) { + bgColor = alpha(theme.palette.primary.main, 0.18); + bgColorHover = bgColor; + } + return { + opacity: transparent ? 0.5 : 1, + borderRadius: theme.shape.borderRadius, + backgroundColor: bgColor, + width: "100%", + display: "flex", + alignItems: "stretch", + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + transitionProperty: "background-color,opacity,box-shadow", + boxShadow: isDropOver ? `0 0 0 2px ${theme.palette.primary.light}` : "none", + "&:hover": { + backgroundColor: bgColorHover, + }, + "&::before": square && { + content: "''", + display: "inline-block", + flex: "0 0 0px", + height: 0, + paddingBottom: "100%", + }, + }; +}); + +const Content = styled(Box)(() => ({ + display: "flex", + flexDirection: "column", + flexGrow: 1, + overflow: "hidden", +})); + +export const Header = styled(Box)(() => ({ + height: 48, + display: "flex", + justifyContent: "left", + alignItems: "initial", + width: "100%", +})); + +const ThumbContainer = styled(Box)(({ theme }) => ({ + flexGrow: "1", + borderRadius: "8px", + height: "100%", + overflow: "hidden", + margin: `0 ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)}`, + position: "relative", +})); + +export const FileNameText = styled(Typography)(() => ({ + flexGrow: 1, + textAlign: "left", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + padding: "14px 12px 14px 0", +})); + +export const ThumbBoxContainer = styled(Box)(() => ({ + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", +})); + +export const ThumbBox = styled("img")<{ loaded: boolean }>(({ theme, loaded }) => ({ + objectFit: "cover", + width: "100%", + height: "100%", + transition: theme.transitions.create(["opacity", "border-radius"], { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.standard, + }), + opacity: loaded ? 1 : 0, + userSelect: "none", + WebkitUserDrag: "none", + MozUserDrag: "none", + msUserDrag: "none", +})); + +export const ThumbLoadingPlaceholder = styled(Skeleton)(() => ({ + borderRadius: "8px", + position: "absolute", + height: "100%", + width: "100%", +})); + +export const LargeIconContainer = styled(Box)(({ theme }) => ({ + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + backgroundColor: theme.palette.background.default, +})); + +export const ThumbPopoverImg = styled("img")<{ width?: number; height?: number }>(({ width, height }) => ({ + display: "block", + maxWidth: width ?? "initial", + maxHeight: height ?? "initial", + objectFit: "contain", + width: "auto", + height: "auto", + userSelect: "none", + WebkitUserDrag: "none", + MozUserDrag: "none", + msUserDrag: "none", +})); + +export const useFileBlockState = (props: FileBlockProps) => { + const { file, search, dragRef } = props; + const dispatch = useAppDispatch(); + const isTouch = useMediaQuery("(pointer: coarse)"); + const fmIndex = useContext(FmIndexContext); + const isSelected = useAppSelector((state) => state.fileManager[fmIndex].selected[file.path]); + const thumbWidth = useAppSelector((state) => state.siteConfig.explorer.config.thumbnail_width); + const thumbHeight = useAppSelector((state) => state.siteConfig.explorer.config.thumbnail_height); + const isLoadingIndicator = file.placeholder; + const noThumb = + (file.type == FileType.folder || (file.metadata && file.metadata[Metadata.thumbDisabled] != undefined)) && + !isLoadingIndicator; + const uploading = file.metadata && file.metadata[Metadata.upload_session_id] != undefined; + const { ref, inView } = useInView({ + triggerOnce: true, + rootMargin: "200px 0px", + skip: noThumb, + }); + const fileTag = useMemo(() => getFileTags(file), [file]); + + const onClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (!isLoadingIndicator) { + dispatch(fileClicked(fmIndex, file, e)); + } + }, + [file, dispatch, fmIndex, isLoadingIndicator], + ); + + const onDoubleClicked = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (!isLoadingIndicator) { + dispatch(fileDoubleClicked(fmIndex, file, e)); + } + }, + [file, dispatch, fmIndex, isLoadingIndicator], + ); + + const setHoverState = useCallback( + (hovered: boolean) => { + dispatch(fileHovered(fmIndex, file, hovered)); + }, + [dispatch, fmIndex, file], + ); + + const hoverStateOff = useCallback(() => { + if (!isTouch) { + setHoverState(false); + } + }, [setHoverState, isTouch]); + const hoverStateOn = useCallback(() => { + if (!isTouch) { + setHoverState(true); + } + }, [setHoverState, isTouch]); + + const onContextMenu = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + dispatch( + openFileContextMenu(fmIndex, file, false, e, !search ? ContextMenuTypes.file : ContextMenuTypes.searchResult), + ); + }, + [dispatch, file, fmIndex, search], + ); + + const setRefFunc = useCallback( + (e: HTMLElement | null) => { + if (isLoadingIndicator) { + ref(e); + } + + if (dragRef) { + dragRef(e); + } + }, + [dragRef, isLoadingIndicator, ref], + ); + + const fileDisabled = fmIndex == FileManagerIndex.selector && file.type == FileType.file; + const disabled = isLoadingIndicator || fileDisabled; + + return { + onClick, + fmIndex, + isSelected, + isLoadingIndicator, + noThumb, + uploading, + ref, + inView, + fileTag, + onDoubleClicked, + hoverStateOff, + hoverStateOn, + onContextMenu, + setRefFunc, + disabled, + fileDisabled, + thumbWidth, + thumbHeight, + }; +}; + +const GridFile = memo((props: FileBlockProps) => { + const { file, isDragging, isDropOver, search, showThumb, index, dragRef } = props; + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const isTouch = useMediaQuery("(pointer: coarse)"); + const { + fmIndex, + isSelected, + isLoadingIndicator, + noThumb, + uploading, + ref, + inView, + fileTag, + onClick, + onDoubleClicked, + hoverStateOff, + hoverStateOn, + onContextMenu, + setRefFunc, + disabled, + fileDisabled, + thumbWidth, + thumbHeight, + } = useFileBlockState(props); + + const popupState = usePopupState({ + variant: "popover", + popupId: "thumbPreview" + file.id, + }); + + // undefined: not loaded, null: no thumb + const [thumbSrc, setThumbSrc] = useState(noThumb ? null : undefined); + const [imageLoading, setImageLoading] = useState(true); + + const tryLoadThumbSrc = useCallback(async () => { + const thumbSrc = await dispatch(loadFileThumb(0, file)); + setThumbSrc(thumbSrc); + }, [dispatch, file, setThumbSrc, setImageLoading]); + + const onImgLoadError = useCallback(() => { + setImageLoading(false); + setThumbSrc(null); + }, [setImageLoading, setThumbSrc]); + + useEffect(() => { + if (!inView) { + return; + } + + if (isLoadingIndicator) { + if (file.first) { + dispatch(navigateReconcile(fmIndex, { next_page: true })); + } + return; + } + + if (!showThumb || file.type == FileType.folder) { + return; + } + + if (file.metadata && file.metadata[Metadata.thumbDisabled] !== undefined) { + // No thumb available + setThumbSrc(null); + return; + } + + tryLoadThumbSrc(); + }, [inView]); + + const hoverProps = bindDelayedHover(popupState, 800); + const { open: thumbPopoverOpen, ...rest } = bindPopover(popupState); + + const stopPop = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + }, []); + + return ( + <> + + +
+ + {!isLoadingIndicator && ( + + + {search?.name ? ( + + ) : ( + file.name + )} + + + )} + {!uploading && fileTag && fileTag.length > 0 && ( + + )} + {uploading && } + {isLoadingIndicator && ( + + )} +
+ {showThumb && ( + + + {thumbSrc && ( + + + setImageLoading(false)} + onError={onImgLoadError} + {...(isTouch ? {} : hoverProps)} + /> + + + )} + {(thumbSrc === undefined || (thumbSrc && imageLoading)) && ( + + + + )} + {thumbSrc === null && ( + + + + + + )} + + + )} +
+ {thumbSrc && showThumb && ( + t.zIndex.drawer, + }} + anchorOrigin={{ + vertical: "center", + horizontal: "center", + }} + transformOrigin={{ + vertical: "center", + horizontal: "center", + }} + {...rest} + > + + + )} +
+ + ); +}); + +export default GridFile; diff --git a/src/component/FileManager/Explorer/GridView/GridView.tsx b/src/component/FileManager/Explorer/GridView/GridView.tsx new file mode 100644 index 0000000..b6d8847 --- /dev/null +++ b/src/component/FileManager/Explorer/GridView/GridView.tsx @@ -0,0 +1,162 @@ +import React, { useContext, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, Grid, Stack, styled, Typography } from "@mui/material"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { FileResponse, FileType } from "../../../../api/explorer.ts"; +import DndWrappedFile from "../../Dnd/DndWrappedFile.tsx"; + +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import GridFile from "./GridFile.tsx"; + +export interface GridViewProps { + [key: string]: any; +} + +export interface FmFile extends FileResponse { + id: string; + first?: boolean; + placeholder?: boolean; +} + +interface listComponents { + Folders?: JSX.Element[]; + Files: JSX.Element[]; +} + +const AutoFillGrid = styled(Grid)(({ theme }) => ({ + [theme.breakpoints.down("md")]: { + gridTemplateColumns: "repeat(auto-fill,minmax(160px,1fr))!important", + }, + [theme.breakpoints.up("md")]: { + gridTemplateColumns: "repeat(auto-fill,minmax(220px,1fr))!important", + }, + gridGap: theme.spacing(2), + display: "grid!important", + padding: theme.spacing(1), +})); + +const GridItem = styled(Grid)(() => ({ + flex: "1 1 220px!important", +})); + +export const loadingPlaceHolderNumb = 3; + +const GridView = React.forwardRef(({ ...rest }: GridViewProps, ref) => { + const { t } = useTranslation("application"); + const fmIndex = useContext(FmIndexContext); + const files = useAppSelector( + (state) => state.fileManager[fmIndex].list?.files, + ); + const mixedType = useAppSelector( + (state) => state.fileManager[fmIndex].list?.mixed_type, + ); + const pagination = useAppSelector( + (state) => state.fileManager[fmIndex].list?.pagination, + ); + const showThumb = useAppSelector( + (state) => state.fileManager[fmIndex].showThumb, + ); + const search_params = useAppSelector( + (state) => state.fileManager[fmIndex]?.search_params, + ); + const list = useMemo(() => { + const list: listComponents = { + Files: [], + }; + if (files) { + files.forEach((file, index) => { + if (file.type === FileType.folder && !mixedType) { + if (!list.Folders) { + list.Folders = []; + } + list.Folders.push( + + + , + ); + } else { + list.Files.push( + + + , + ); + } + }); + + // Add loading placeholder if there is next page + if (pagination && pagination.next_token) { + for (let i = 0; i < loadingPlaceHolderNumb; i++) { + const id = `loadingPlaceholder-${pagination.next_token}-${i}`; + const loadingPlaceholder = ( + + 0 ? showThumb : mixedType} + file={{ + ...files[0], + path: files[0].path + "/" + id, + id: `loadingPlaceholder-${pagination.next_token}-${i}`, + first: i == 0, + placeholder: true, + }} + /> + + ); + const _ = + list.Files.length > 0 + ? list.Files.push(loadingPlaceholder) + : list.Folders?.push(loadingPlaceholder); + } + } + } + return list; + }, [files, mixedType, pagination, showThumb]); + return ( + + + {list.Folders && list.Folders.length > 0 && ( + + + {t("fileManager.folders")} + + + {list.Folders.map((f) => f)} + + + )} + {list.Files.length > 0 && ( + + {!mixedType && ( + + {t("fileManager.files")} + + )} + + {list.Files.map((f) => f)} + + + )} + + + ); +}); + +export default GridView; diff --git a/src/component/FileManager/Explorer/ListView/AddColumn.tsx b/src/component/FileManager/Explorer/ListView/AddColumn.tsx new file mode 100644 index 0000000..2de609f --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/AddColumn.tsx @@ -0,0 +1,105 @@ +import { useTranslation } from "react-i18next"; + +import { Menu } from "@mui/material"; +import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { SecondaryButton } from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; +import { CascadingSubmenu } from "../../ContextMenu/CascadingMenu.tsx"; +import { DenseDivider, SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; +import { ColumType, ColumTypeProps, getColumnTypeDefaults, ListViewColumnSetting } from "./Column.tsx"; + +export interface AddColumnProps { + onColumnAdded: (column: ListViewColumnSetting) => void; +} + +const options: ColumType[] = [ + ColumType.name, + ColumType.size, + ColumType.date_modified, + ColumType.date_created, + ColumType.parent, +]; + +const recycleOptions: ColumType[] = [ColumType.recycle_restore_parent, ColumType.recycle_expire]; + +// null => divider +const mediaInfoOptions: (ColumType | null)[] = [ + ColumType.aperture, + ColumType.exposure, + ColumType.iso, + ColumType.focal_length, + ColumType.exposure_bias, + ColumType.flash, + null, + ColumType.camera_make, + ColumType.camera_model, + ColumType.lens_make, + ColumType.lens_model, + null, + ColumType.software, + ColumType.taken_at, + ColumType.image_size, + null, + ColumType.title, + ColumType.artist, + ColumType.album, + ColumType.duration, +]; + +const AddColumn = (props: AddColumnProps) => { + const { t } = useTranslation(); + const conditionPopupState = usePopupState({ + variant: "popover", + popupId: "columns", + }); + const { onClose, ...menuProps } = bindMenu(conditionPopupState); + const onConditionAdd = (type: ColumType, p?: ColumTypeProps) => { + props.onColumnAdded({ type, props: p }); + onClose(); + }; + return ( + <> + } sx={{ px: "15px" }}> + {t("fileManager.addColumn")} + + + {options.map((option, index) => ( + onConditionAdd(option)}> + {t(getColumnTypeDefaults({ type: option }).title)} + + ))} + + {recycleOptions.map((option, index) => ( + onConditionAdd(option)}> + {t(getColumnTypeDefaults({ type: option }).title)} + + ))} + + + {mediaInfoOptions.map((option, index) => + option ? ( + onConditionAdd(option)}> + {t(getColumnTypeDefaults({ type: option }).title)} + + ) : ( + + ), + )} + + + + ); +}; + +export default AddColumn; diff --git a/src/component/FileManager/Explorer/ListView/Cell.tsx b/src/component/FileManager/Explorer/ListView/Cell.tsx new file mode 100644 index 0000000..0ed1bb7 --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/Cell.tsx @@ -0,0 +1,336 @@ +import { Box, Fade, PopoverProps, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { memo, useCallback, useEffect, useState } from "react"; +import { sizeToString } from "../../../../util"; +import CrUri, { SearchParam } from "../../../../util/uri.ts"; +import FileSmallIcon from "../FileSmallIcon.tsx"; +import { FmFile } from "../GridView/GridView.tsx"; +import { ColumType, ListViewColumn } from "./Column.tsx"; +// @ts-ignore +import dayjs, { Dayjs } from "dayjs"; +import { bindPopover } from "material-ui-popup-state"; +import { usePopupState } from "material-ui-popup-state/hooks"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import Highlighter from "react-highlight-words"; +import { useTranslation } from "react-i18next"; +import { TransitionGroup } from "react-transition-group"; +import { FileType, Metadata } from "../../../../api/explorer.ts"; +import { bindDelayedHover } from "../../../../hooks/delayedHover.tsx"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { loadFileThumb } from "../../../../redux/thunks/file.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import { NoWrapBox } from "../../../Common/StyledComponents.tsx"; +import TimeBadge from "../../../Common/TimeBadge.tsx"; +import Info from "../../../Icons/Info.tsx"; +import FileBadge from "../../FileBadge.tsx"; +import { + getAlbum, + getAperture, + getArtist, + getCameraMake, + getCameraModel, + getDuration, + getExposure, + getExposureBias, + getFlash, + getFocalLength, + getImageSize, + getIso, + getLensMake, + getLensModel, + getMediaTitle, + getSoftware, + takenAt, +} from "../../Sidebar/MediaInfo.tsx"; +import { MediaMetaElements } from "../../Sidebar/MediaMetaCard.tsx"; +import FileTagSummary from "../FileTagSummary.tsx"; +import { ThumbLoadingPlaceholder, ThumbPopoverImg } from "../GridView/GridFile.tsx"; +import UploadingTag from "../UploadingTag.tsx"; + +export interface CellProps { + file: FmFile; + column: ListViewColumn; + isSelected?: boolean; + search?: SearchParam; + fileTag?: { + key: string; + value: string; + }[]; + uploading?: boolean; + noThumb?: boolean; + thumbWidth?: number; + thumbHeight?: number; +} + +export interface ThumbPopoverProps { + file: FmFile; + popupState: PopoverProps; + thumbWidth?: number; + thumbHeight?: number; +} + +export const ThumbPopover = memo((props: ThumbPopoverProps) => { + const { t } = useTranslation(); + const { + file, + popupState: { open, ...rest }, + thumbWidth, + thumbHeight, + } = props; + + const dispatch = useAppDispatch(); + // undefined: not loaded, null: no thumb + const [thumbSrc, setThumbSrc] = useState(undefined); + const [imageLoading, setImageLoading] = useState(true); + + const tryLoadThumbSrc = useCallback(async () => { + const thumbSrc = await dispatch(loadFileThumb(0, file)); + setThumbSrc(thumbSrc); + }, [dispatch, file, setThumbSrc, setImageLoading]); + + const onImgLoadError = useCallback(() => { + setImageLoading(false); + setThumbSrc(null); + }, [setImageLoading, setThumbSrc]); + + useEffect(() => { + if (open && !thumbSrc) { + tryLoadThumbSrc(); + } + }, [open]); + + const showPlaceholder = thumbSrc === undefined || (thumbSrc && imageLoading); + + return ( + + + + {showPlaceholder && ( + + + + )} + {thumbSrc && ( + + setImageLoading(false)} + onError={onImgLoadError} + src={thumbSrc} + draggable={false} + /> + + )} + {thumbSrc === null && ( + + + + {t("fileManager.failedLoadPreview")} + + + )} + + + + ); +}); + +const FileNameCell = memo((props: CellProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const isTouch = useMediaQuery("(pointer: coarse)"); + const { file, uploading, noThumb, fileTag, search, isSelected, thumbWidth, thumbHeight } = props; + + const popupState = usePopupState({ + variant: "popover", + popupId: "thumbPreview" + file.id, + }); + + const hoverState = bindDelayedHover(popupState, 800); + + return ( + <> + + + + + + + + {search?.name ? ( + + ) : ( + file.name + )} + + + {!uploading && fileTag && fileTag.length > 0 && } + {uploading && } + + {!noThumb && ( + + )} + + ); +}); + +interface FolderSizeCellProps { + file: FmFile; +} + +const FolderSizeCell = memo(({ file }: FolderSizeCellProps) => { + const { t } = useTranslation(); + if (file.type == FileType.folder || file.metadata?.[Metadata.share_redirect]) { + return ; + } + return {sizeToString(file.size)}; +}); + +interface FolderDateCellProps { + file: FmFile; + dateType: "created" | "modified" | "expired"; +} + +const FolderDateCell = memo(({ file, dateType }: FolderDateCellProps) => { + const { t } = useTranslation(); + let datetime: string | Dayjs = ""; + switch (dateType) { + case "created": + datetime = file.created_at; + break; + case "modified": + datetime = file.updated_at; + break; + case "expired": + datetime = file.metadata?.[Metadata.expected_collect_time] + ? dayjs.unix(parseInt(file.metadata?.[Metadata.expected_collect_time])) + : ""; + } + + if (!datetime) { + return ; + } + return ; +}); + +const FolderCell = memo(({ path }: { path: string }) => { + return ( + + ); +}); + +const MediaElementsCell = memo(({ element }: { element?: MediaMetaElements | string }) => { + if (!element) { + return ; + } + if (typeof element === "string") { + return {element}; + } + return ; +}); + +const Cell = memo((props: CellProps) => { + const { t } = useTranslation(); + const { file, column, uploading, showLock, fileTag, search, isSelected } = props; + switch (column.type) { + case ColumType.name: + return ; + case ColumType.size: + return ; + case ColumType.date_modified: + return ; + case ColumType.date_created: + return ; + case ColumType.parent: { + let crUrl = new CrUri(file.path); + return ; + } + case ColumType.recycle_restore_parent: { + if (!file.metadata?.[Metadata.restore_uri]) { + return ; + } + + let crUrl = new CrUri(file.metadata[Metadata.restore_uri]); + return ; + } + case ColumType.recycle_expire: + return ; + case ColumType.aperture: + return ; + case ColumType.exposure: + return ; + case ColumType.iso: + return ; + case ColumType.camera_make: + return ; + case ColumType.camera_model: + return ; + case ColumType.lens_make: + return ; + case ColumType.lens_model: + return ; + case ColumType.focal_length: + return ; + case ColumType.exposure_bias: + return ; + case ColumType.flash: + return ; + case ColumType.software: + return ; + case ColumType.taken_at: + return ; + case ColumType.image_size: + return ( + {getImageSize(file)?.map((size) => )} + ); + case ColumType.title: + return ; + case ColumType.artist: + return ; + case ColumType.album: + return ; + case ColumType.duration: + return ; + } +}); + +export default Cell; diff --git a/src/component/FileManager/Explorer/ListView/Column.tsx b/src/component/FileManager/Explorer/ListView/Column.tsx new file mode 100644 index 0000000..27daf1e --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/Column.tsx @@ -0,0 +1,285 @@ +import { useTranslation } from "react-i18next"; +import { Box, Fade, IconButton, styled } from "@mui/material"; +import Divider from "../../../Icons/Divider.tsx"; +import { ResizeProps } from "./ListHeader.tsx"; +import ArrowSortDownFilled from "../../../Icons/ArrowSortDownFilled.tsx"; +import { useCallback, useState } from "react"; +import { NoWrapTypography } from "../../../Common/StyledComponents.tsx"; + +export interface ListViewColumn { + type: ColumType; + width?: number; + defaults: ColumTypeDefaults; + props?: ColumTypeProps; +} + +export interface ListViewColumnSetting { + type: ColumType; + width?: number; + props?: ColumTypeProps; +} + +export interface ColumTypeProps { + metadata_key?: string; +} + +export enum ColumType { + name = 0, + date_modified = 1, + size = 2, + metadata = 3, + date_created = 4, + permission = 5, + parent = 6, + recycle_restore_parent = 7, + recycle_expire = 8, + + // Media info + aperture = 9, + exposure = 10, + iso = 11, + camera_make = 12, + camera_model = 13, + lens_make = 14, + lens_model = 15, + focal_length = 16, + exposure_bias = 17, + flash = 18, + software = 19, + taken_at = 20, + image_size = 21, + title = 22, + artist = 23, + album = 24, + duration = 25, +} + +export interface ColumTypeDefaults { + title: string; + width: number; + widthMobile?: number; + minWidth?: number; + order_by?: string; +} + +export interface ColumnProps { + index: number; + column: ListViewColumn; + showDivider?: boolean; + startResizing: (props: ResizeProps) => void; + sortable?: boolean; + sortDirection?: string; + setSortBy?: (order_by: string, order_direction: string) => void; +} + +export const ColumnTypeDefaults: { [key: number]: ColumTypeDefaults } = { + [ColumType.name]: { + title: "application:fileManager.name", + widthMobile: 300, + width: 600, + order_by: "name", + }, + [ColumType.size]: { + title: "application:fileManager.size", + width: 100, + order_by: "size", + }, + [ColumType.date_modified]: { + title: "application:fileManager.lastModified", + width: 200, + order_by: "updated_at", + }, + [ColumType.date_created]: { + title: "application:fileManager.createDate", + width: 200, + order_by: "created_at", + }, + [ColumType.parent]: { + title: "application:fileManager.parentFolder", + width: 200, + }, + [ColumType.recycle_restore_parent]: { + title: "application:fileManager.originalLocation", + width: 200, + }, + [ColumType.recycle_expire]: { + title: "application:fileManager.expires", + width: 200, + }, + [ColumType.aperture]: { + title: "application:fileManager.aperture", + width: 100, + }, + [ColumType.exposure]: { + title: "application:fileManager.exposure", + width: 100, + }, + [ColumType.iso]: { + title: "application:fileManager.iso", + width: 100, + }, + [ColumType.camera_make]: { + title: "application:fileManager.cameraMake", + width: 100, + }, + [ColumType.camera_model]: { + title: "application:fileManager.cameraModel", + width: 100, + }, + [ColumType.lens_make]: { + title: "application:fileManager.lensMake", + width: 100, + }, + [ColumType.lens_model]: { + title: "application:fileManager.lensModel", + width: 100, + }, + [ColumType.focal_length]: { + title: "application:fileManager.focalLength", + width: 100, + }, + [ColumType.exposure_bias]: { + title: "application:fileManager.exposureBias", + width: 100, + }, + [ColumType.flash]: { + title: "application:fileManager.flash", + width: 100, + }, + [ColumType.software]: { + title: "application:fileManager.software", + width: 100, + }, + [ColumType.taken_at]: { + title: "application:fileManager.takenAt", + width: 200, + }, + [ColumType.image_size]: { + title: "application:fileManager.resolution", + width: 100, + }, + [ColumType.title]: { + title: "application:fileManager.title", + width: 200, + }, + [ColumType.artist]: { + title: "application:fileManager.artist", + width: 100, + }, + [ColumType.album]: { + title: "application:fileManager.album", + width: 200, + }, + [ColumType.duration]: { + title: "application:fileManager.duration", + width: 100, + }, +}; + +export const getColumnTypeDefaults = (c: ListViewColumnSetting, isMobile?: boolean): ColumTypeDefaults => { + if (ColumnTypeDefaults[c.type]) { + return { + ...ColumnTypeDefaults[c.type], + width: + isMobile && ColumnTypeDefaults[c.type].widthMobile + ? ColumnTypeDefaults[c.type].widthMobile + : ColumnTypeDefaults[c.type].width, + }; + } + + return { + title: "application:fileManager.metadataColumn", + width: 100, + }; +}; + +const ColumnContainer = styled(Box)<{ + w: number; +}>(({ w }) => ({ + height: "39px", + width: `${w}px`, + display: "flex", + alignItems: "center", + padding: "0 10px", +})); + +const DividerContainer = styled(Box)(({ theme }) => ({ + color: theme.palette.divider, + maxWidth: "10px", + display: "flex", + alignItems: "center", + cursor: "col-resize", + "&:hover": { + color: theme.palette.primary.main, + }, + transition: theme.transitions.create(["color"], { + duration: theme.transitions.duration.shortest, + }), + position: "relative", + right: "-8px", +})); + +const SortArrow = styled(ArrowSortDownFilled)<{ + direction?: string; +}>(({ theme, direction }) => ({ + width: "18px", + height: "18px", + color: !direction ? theme.palette.action.disabled : theme.palette.action.active, + transform: `rotate(${direction === "asc" ? 180 : 0}deg)`, + transition: theme.transitions.create(["color", "transform"], { + duration: theme.transitions.duration.shortest, + }), +})); + +const Column = ({ column, showDivider, index, startResizing, sortDirection, setSortBy, sortable }: ColumnProps) => { + const [showSortButton, setShowSortButton] = useState(false); + const { t } = useTranslation(); + const onSortOptionChange = useCallback(() => { + if (!sortable || !column.defaults.order_by) return; + const newDirection = sortDirection === "asc" ? "desc" : "asc"; + setSortBy && setSortBy(column.defaults.order_by, newDirection); + }, [setSortBy, sortDirection, sortable, column]); + + return ( + + setShowSortButton(!!sortable)} + onMouseLeave={() => setShowSortButton(false)} + onClick={sortable ? onSortOptionChange : undefined} + > + + {t(column.defaults.title, { + metadata: column.props?.metadata_key, + })} + + {sortable && ( + + + + + + )} + + + + startResizing({ + index, + startX: e.clientX, + }) + } + > + + + + + ); +}; + +export default Column; diff --git a/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx b/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx new file mode 100644 index 0000000..3253012 --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/ColumnSetting.tsx @@ -0,0 +1,196 @@ +import { useTranslation } from "react-i18next"; +import { + Box, + DialogContent, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { useCallback, useEffect, useState } from "react"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { setListViewColumnSettingDialog } from "../../../../redux/globalStateSlice.ts"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import { FileManagerIndex } from "../../FileManager.tsx"; +import { getColumnTypeDefaults, ListViewColumnSetting } from "./Column.tsx"; +import { StyledTableContainerPaper } from "../../../Common/StyledComponents.tsx"; +import ArrowDown from "../../../Icons/ArrowDown.tsx"; +import Dismiss from "../../../Icons/Dismiss.tsx"; +import { setListViewColumns } from "../../../../redux/fileManagerSlice.ts"; +import SessionManager, { UserSettings } from "../../../../session"; +import AddColumn from "./AddColumn.tsx"; +import { useSnackbar } from "notistack"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; + +const ColumnSetting = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const [columns, setColumns] = useState([]); + + const open = useAppSelector( + (state) => state.globalState.listViewColumnSettingDialogOpen, + ); + const listViewColumns = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main].listViewColumns, + ); + + useEffect(() => { + if (open) { + setColumns(listViewColumns ?? []); + } + }, [open]); + + const onClose = useCallback(() => { + dispatch(setListViewColumnSettingDialog(false)); + }, [dispatch]); + + const onSubmitted = useCallback(() => { + if (columns.length > 0) { + dispatch(setListViewColumns(columns)); + SessionManager.set(UserSettings.ListViewColumns, columns); + } + dispatch(setListViewColumnSettingDialog(false)); + }, [dispatch, columns]); + + const onColumnAdded = useCallback( + (column: ListViewColumnSetting) => { + const existed = columns.find((c) => c.type === column.type); + if ( + !existed || + existed.props?.metadata_key != column.props?.metadata_key + ) { + setColumns((prev) => [...prev, column]); + } else { + enqueueSnackbar(t("application:fileManager.columnExisted"), { + variant: "warning", + action: DefaultCloseAction, + }); + } + }, + [columns], + ); + + return ( + } + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "sm", + disableRestoreFocus: true, + }} + > + + + + + + + {t("fileManager.column")} + {t("fileManager.actions")} + + + + {columns.map((column, index) => ( + + + {t(getColumnTypeDefaults(column).title)} + + + + {index > 0 && columns.length > 1 ? ( + { + setColumns((prev) => { + const newColumns = [...prev]; + const temp = newColumns[index]; + newColumns[index] = newColumns[index - 1]; + newColumns[index - 1] = temp; + return newColumns; + }); + }} + > + + + ) : ( + + )} + {index < columns.length - 1 && columns.length > 1 ? ( + { + setColumns((prev) => { + const newColumns = [...prev]; + const temp = newColumns[index]; + newColumns[index] = newColumns[index + 1]; + newColumns[index + 1] = temp; + return newColumns; + }); + }} + > + + + ) : ( + + )} + {columns.length > 1 ? ( + { + setColumns((prev) => { + return prev.filter((_, i) => i !== index); + }); + }} + > + + + ) : ( + + )} + + + + ))} + +
+
+
+
+
+ ); +}; +export default ColumnSetting; diff --git a/src/component/FileManager/Explorer/ListView/ListBody.tsx b/src/component/FileManager/Explorer/ListView/ListBody.tsx new file mode 100644 index 0000000..9ec6376 --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/ListBody.tsx @@ -0,0 +1,77 @@ +import { ListViewColumn } from "./Column.tsx"; +import React, { useContext, useMemo } from "react"; +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { Virtuoso } from "react-virtuoso"; +import DndWrappedFile from "../../Dnd/DndWrappedFile.tsx"; +import Row from "./Row.tsx"; +import { FmFile, loadingPlaceHolderNumb } from "../GridView/GridView.tsx"; + +export interface ListBodyProps { + columns: ListViewColumn[]; +} + +const ListBody = ({ columns }: ListBodyProps) => { + const fmIndex = useContext(FmIndexContext); + const files = useAppSelector( + (state) => state.fileManager[fmIndex].list?.files, + ); + const mixedType = useAppSelector( + (state) => state.fileManager[fmIndex].list?.mixed_type, + ); + const pagination = useAppSelector( + (state) => state.fileManager[fmIndex].list?.pagination, + ); + const search_params = useAppSelector( + (state) => state.fileManager[fmIndex]?.search_params, + ); + + const list = useMemo(() => { + const list: FmFile[] = []; + if (!files) { + return list; + } + + files.forEach((file) => { + list.push(file); + }); + + // Add loading placeholder if there is next page + if (pagination && pagination.next_token) { + for (let i = 0; i < loadingPlaceHolderNumb; i++) { + const id = `loadingPlaceholder-${pagination.next_token}-${i}`; + list.push({ + ...files[0], + path: files[0].path + "/" + id, + id: `loadingPlaceholder-${pagination.next_token}-${i}`, + first: i == 0, + placeholder: true, + }); + } + } + return list; + }, [files, mixedType, pagination, search_params]); + + return ( + ( + + )} + /> + ); +}; + +export default ListBody; diff --git a/src/component/FileManager/Explorer/ListView/ListHeader.tsx b/src/component/FileManager/Explorer/ListView/ListHeader.tsx new file mode 100644 index 0000000..c0ea612 --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/ListHeader.tsx @@ -0,0 +1,161 @@ +import Column, { ListViewColumn } from "./Column.tsx"; +import { Box, Fade, IconButton, Tooltip } from "@mui/material"; +import { useCallback, useContext, useMemo, useRef, useState } from "react"; +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { changeSortOption } from "../../../../redux/thunks/filemanager.ts"; +import SessionManager, { UserSettings } from "../../../../session"; +import { Add } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { setListViewColumnSettingDialog } from "../../../../redux/globalStateSlice.ts"; + +export interface ListHeaderProps { + columns: ListViewColumn[]; + setColumns: React.Dispatch>; + commitColumnSetting: () => void; +} + +export interface ResizeProps { + index: number; + startX: number; +} + +const ListHeader = ({ + setColumns, + commitColumnSetting, + columns, +}: ListHeaderProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const [showDivider, setShowDivider] = useState(false); + const resizeProps = useRef(); + const startResizing = (props: ResizeProps) => { + resizeProps.current = props; + document.body.style.cursor = "col-resize"; + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + }; + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (!resizeProps.current) { + return; + } + const column = columns[resizeProps.current.index]; + const currentWidth = column.width ?? column.defaults.width; + const minWidth = column.defaults.minWidth ?? 100; + const newWidth = Math.max( + minWidth, + currentWidth + (e.clientX - resizeProps.current.startX), + ); + setColumns((prev) => + prev.map((c, index) => + index === resizeProps.current?.index ? { ...c, width: newWidth } : c, + ), + ); + }, + [columns, setColumns], + ); + + const onMouseUp = useCallback(() => { + document.body.style.removeProperty("cursor"); + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + commitColumnSetting(); + }, [onMouseMove, commitColumnSetting]); + + const fmIndex = useContext(FmIndexContext); + const orderMethodOptions = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.order_by_options, + ); + const orderDirectionOption = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.order_direction_options, + ); + const sortBy = useAppSelector((state) => state.fileManager[fmIndex].sortBy); + const sortDirection = useAppSelector( + (state) => state.fileManager[fmIndex].sortDirection, + ); + + const allAvailableSortOptions = useMemo((): { + [key: string]: boolean; + } => { + if (!orderMethodOptions || !orderDirectionOption) return {}; + const res: { [key: string]: boolean } = {}; + orderMethodOptions.forEach((method) => { + // make sure orderDirectionOption contains both asc and desc + if ( + orderDirectionOption.includes("asc") && + orderDirectionOption.includes("desc") + ) { + res[method] = true; + } + }); + return res; + }, [orderMethodOptions, sortDirection]); + + const setSortBy = useCallback( + (order_by: string, order_direction: string) => { + dispatch(changeSortOption(fmIndex, order_by, order_direction)); + SessionManager.set(UserSettings.SortBy, order_by); + SessionManager.set(UserSettings.SortDirection, order_direction); + }, + [dispatch, fmIndex], + ); + + return ( + setShowDivider(true)} + onMouseLeave={() => setShowDivider(false)} + sx={{ + display: "flex", + borderBottom: (theme) => `1px solid ${theme.palette.divider}`, + }} + > + {columns.map((column, index) => ( + + ))} + + + + dispatch(setListViewColumnSettingDialog(true))} + sx={{ ml: 1 }} + size={"small"} + > + + + + + + + ); +}; + +export default ListHeader; diff --git a/src/component/FileManager/Explorer/ListView/ListView.tsx b/src/component/FileManager/Explorer/ListView/ListView.tsx new file mode 100644 index 0000000..994c3f7 --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/ListView.tsx @@ -0,0 +1,117 @@ +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { Box, useMediaQuery, useTheme } from "@mui/material"; +import { + getColumnTypeDefaults, + ListViewColumn, + ListViewColumnSetting, +} from "./Column.tsx"; +import ListHeader from "./ListHeader.tsx"; +import ListBody from "./ListBody.tsx"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { FmIndexContext } from "../../FmIndexContext.tsx"; +import { setListViewColumns } from "../../../../redux/fileManagerSlice.ts"; +import SessionManager, { UserSettings } from "../../../../session"; +import { SearchLimitReached } from "../EmptyFileList.tsx"; + +const ListView = React.forwardRef( + ( + { + ...rest + }: { + [key: string]: any; + }, + ref, + ) => { + const { t } = useTranslation("application"); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const recursion_limit_reached = useAppSelector( + (state) => state.fileManager[fmIndex].list?.recursion_limit_reached, + ); + const columnSetting = useAppSelector( + (state) => state.fileManager[fmIndex].listViewColumns, + ); + + const [columns, setColumns] = useState( + columnSetting.map( + (c): ListViewColumn => ({ + type: c.type, + width: c.width, + defaults: getColumnTypeDefaults(c, isMobile), + }), + ), + ); + + useEffect(() => { + setColumns( + columnSetting.map( + (c): ListViewColumn => ({ + type: c.type, + width: c.width, + defaults: getColumnTypeDefaults(c, isMobile), + }), + ), + ); + }, [columnSetting]); + + const totalWidth = useMemo(() => { + return columns.reduce( + (acc, column) => acc + (column.width ?? column.defaults.width), + 0, + ); + }, [columns]); + + const commitColumnSetting = useCallback(() => { + let settings: ListViewColumnSetting[] = []; + setColumns((prev) => { + settings = [ + ...prev.map((c) => ({ + type: c.type, + width: c.width, + })), + ]; + return prev; + }); + if (settings.length > 0) { + dispatch(setListViewColumns(settings)); + SessionManager.set(UserSettings.ListViewColumns, settings); + } + }, [dispatch, setColumns]); + + return ( + + + + {recursion_limit_reached && ( + + + + )} + + ); + }, +); + +export default ListView; diff --git a/src/component/FileManager/Explorer/ListView/Row.tsx b/src/component/FileManager/Explorer/ListView/Row.tsx new file mode 100644 index 0000000..4a9321d --- /dev/null +++ b/src/component/FileManager/Explorer/ListView/Row.tsx @@ -0,0 +1,129 @@ +import { alpha, Box, Skeleton, styled } from "@mui/material"; +import { memo, useEffect } from "react"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { navigateReconcile } from "../../../../redux/thunks/filemanager.ts"; +import { NoWrapTypography } from "../../../Common/StyledComponents.tsx"; +import { FileBlockProps } from "../Explorer.tsx"; +import { useFileBlockState } from "../GridView/GridFile.tsx"; +import Cell from "./Cell.tsx"; + +const RowContainer = styled(Box)<{ + selected: boolean; + transparent?: boolean; + isDropOver?: boolean; + disabled?: boolean; +}>(({ theme, disabled, transparent, isDropOver, selected }) => { + let bgColor = "initial"; + let bgColorHover = theme.palette.action.hover; + + if (selected) { + bgColor = alpha(theme.palette.primary.main, 0.18); + bgColorHover = bgColor; + } + return { + minHeight: "36px", + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + backgroundColor: bgColor, + "&:hover": { + backgroundColor: bgColorHover, + }, + pointerEvents: disabled ? "none" : "auto", + opacity: transparent || disabled ? 0.5 : 1, + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + transitionProperty: "background-color,opacity,box-shadow", + boxShadow: isDropOver ? `inset 0 0 0 2px ${theme.palette.primary.light}` : "none", + }; +}); + +const Column = styled(Box)<{ w: number }>(({ theme, w }) => ({ + display: "flex", + alignItems: "center", + width: `${w}px`, + padding: "0 10px", +})); + +const Row = memo((props: FileBlockProps) => { + const { file, columns, search, isDragging, isDropOver } = props; + const dispatch = useAppDispatch(); + + const { + fmIndex, + isSelected, + isLoadingIndicator, + noThumb, + uploading, + ref, + inView, + showLock, + fileTag, + onClick, + onDoubleClicked, + hoverStateOff, + hoverStateOn, + onContextMenu, + setRefFunc, + disabled, + fileDisabled, + thumbWidth, + thumbHeight, + } = useFileBlockState(props); + + useEffect(() => { + if (!inView) { + return; + } + + if (isLoadingIndicator) { + if (file.first) { + dispatch(navigateReconcile(fmIndex, { next_page: true })); + } + return; + } + }, [inView]); + + return ( + + {columns?.map((column, index) => ( + + + {!file.placeholder && ( + + )} + + {file.placeholder && } + + + ))} + + ); +}); + +export default Row; diff --git a/src/component/FileManager/Explorer/SingleFileView.tsx b/src/component/FileManager/Explorer/SingleFileView.tsx new file mode 100644 index 0000000..1c0f235 --- /dev/null +++ b/src/component/FileManager/Explorer/SingleFileView.tsx @@ -0,0 +1,245 @@ +import { + Box, + Button, + ButtonGroup, + Container, + Divider, + Link, + Stack, + styled, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover, usePopupState } from "material-ui-popup-state/hooks"; +import React, { forwardRef, useCallback, useContext, useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { FileResponse, Share } from "../../../api/explorer.ts"; +import { bindDelayedHover } from "../../../hooks/delayedHover.tsx"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { downloadSingleFile } from "../../../redux/thunks/download.ts"; +import { openFileContextMenu } from "../../../redux/thunks/file.ts"; +import { queueLoadShareInfo } from "../../../redux/thunks/share.ts"; +import { sizeToString } from "../../../util/index.ts"; +import CrUri from "../../../util/uri.ts"; +import UserAvatar from "../../Common/User/UserAvatar.tsx"; +import CaretDown from "../../Icons/CaretDown.tsx"; +import Download from "../../Icons/Download.tsx"; +import Eye from "../../Icons/Eye.tsx"; +import Timer from "../../Icons/Timer.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { PropTypography, ShareExpires, ShareStatistics } from "../TopBar/ShareInfoPopover.tsx"; +import FileIcon from "./FileIcon.tsx"; +import FileTagSummary from "./FileTagSummary.tsx"; +import { useFileBlockState } from "./GridView/GridFile.tsx"; +import { ThumbPopover } from "./ListView/Cell.tsx"; + +const ShareContainer = styled(Box)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(2), + width: "100%", + backgroundColor: theme.palette.background.default, + boxShadow: `0 0 10px 0 rgba(0, 0, 0, 0.1)`, +})); + +const FileList = ({ file }: { file: FileResponse }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isTouch = useMediaQuery("(pointer: coarse)"); + + const { uploading, noThumb, showLock, fileTag, isSelected, thumbWidth, thumbHeight } = useFileBlockState({ + file, + }); + + const popupState = usePopupState({ + variant: "popover", + popupId: "thumbPreview" + file.id, + }); + + const hoverState = bindDelayedHover(popupState, 800); + const stopPropagation = useCallback((e: React.MouseEvent) => e.stopPropagation(), []); + + return ( + <> + + + + + + + {file?.name}{" "} + {fileTag && fileTag.length > 0 && ( + + )} + + + {sizeToString(file?.size ?? 0)} + + + + {!noThumb && ( + + )} + + ); +}; +const SingleFileView = forwardRef((_props, ref: React.Ref) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const file = useAppSelector((state) => state.fileManager[fmIndex].list?.files[0]); + const [loading, setLoading] = useState(false); + const [shareInfo, setShareInfo] = useState(null); + + useEffect(() => { + if (file) { + dispatch(queueLoadShareInfo(new CrUri(file.path))) + .then((info) => { + setShareInfo(info); + }) + .catch((_e) => { + setShareInfo(null); + }) + .finally(() => { + setLoading(false); + }); + } else { + setShareInfo(null); + } + }, [file]); + + const openMore = useCallback( + (e: React.MouseEvent) => { + if (file) { + dispatch(openFileContextMenu(fmIndex, file, true, e)); + } + }, + [dispatch, file], + ); + const download = useCallback(async () => { + if (!file) { + return; + } + + setLoading(true); + try { + await dispatch(downloadSingleFile(file)); + } finally { + setLoading(false); + } + }, [file, dispatch]); + return ( + + {shareInfo && ( + + + + + + + {shareInfo.owner.nickname} + , + ]} + values={{ nick: shareInfo.owner.nickname, num: 1 }} + /> + + + + + + + + + + {file && } + + + + {(shareInfo.remain_downloads || shareInfo.expires) && ( + + + + + )} + + + + + + + + + )} + + ); +}); + +export default SingleFileView; diff --git a/src/component/FileManager/Explorer/UploadingTag.tsx b/src/component/FileManager/Explorer/UploadingTag.tsx new file mode 100644 index 0000000..5cd2289 --- /dev/null +++ b/src/component/FileManager/Explorer/UploadingTag.tsx @@ -0,0 +1,41 @@ +import { Stack } from "@mui/material"; +import { memo, useCallback, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { Metadata } from "../../../api/explorer.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { searchMetadata } from "../../../redux/thunks/filemanager.ts"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { TagChip } from "./FileTag.tsx"; + +export interface UploadingTagProps { + disabled?: boolean; + [key: string]: any; +} + +const FileTagSummary = memo(({ sx, disabled, ...restProps }: UploadingTagProps) => { + const fmIndex = useContext(FmIndexContext); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const stopPropagation = useCallback((e: any) => { + e.stopPropagation(); + }, []); + const onClick = useCallback( + (e: any) => { + e.stopPropagation(); + dispatch(searchMetadata(fmIndex, Metadata.upload_session_id)); + }, + [dispatch, fmIndex], + ); + return ( + + + + ); +}); + +export default FileTagSummary; diff --git a/src/component/FileManager/FileBadge.tsx b/src/component/FileManager/FileBadge.tsx new file mode 100644 index 0000000..c819fd4 --- /dev/null +++ b/src/component/FileManager/FileBadge.tsx @@ -0,0 +1,162 @@ +import { FileResponse, FileType } from "../../api/explorer.ts"; +import FileIcon from "./Explorer/FileIcon.tsx"; +import React, { useMemo } from "react"; +import { Box, ButtonProps, Skeleton, Tooltip } from "@mui/material"; +import { BadgeText, DefaultButton } from "../Common/StyledComponents.tsx"; +import CrUri from "../../util/uri.ts"; +import { useTranslation } from "react-i18next"; +import { usePopupState } from "material-ui-popup-state/hooks"; +import { bindHover, bindPopover } from "material-ui-popup-state"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import Breadcrumb from "./TopBar/Breadcrumb.tsx"; +import { useBreadcrumbButtons } from "./TopBar/BreadcrumbButton.tsx"; + +export interface FileBadgeFile { + path: string; + type: number; +} + +export interface FileBadgeProps extends ButtonProps { + file?: FileResponse; + simplifiedFile?: FileBadgeFile; + unknown?: boolean; + clickable?: boolean; +} + +const FileBadge = ({ + file, + clickable, + simplifiedFile, + unknown, + ...rest +}: FileBadgeProps) => { + const { t } = useTranslation(); + const popupState = usePopupState({ + variant: "popover", + popupId: "fileBadge", + }); + const hoverProps = bindHover(popupState); + const popoverProps = bindPopover(popupState); + + const name = useMemo(() => { + if (unknown) { + return t("application:modals.unknownParent"); + } + + if (file?.name) { + return file?.name; + } + + try { + const uri = new CrUri(simplifiedFile?.path ?? ""); + return uri.elements().pop() ?? ""; + } catch (e) { + return ""; + } + }, [file, unknown, simplifiedFile]); + + const f = useMemo(() => { + if (file) { + return file; + } + + return { + name, + type: simplifiedFile?.type ?? FileType.folder, + id: "", + created_at: "", + updated_at: "", + size: 0, + path: simplifiedFile?.path ?? "", + } as FileResponse; + }, [file, unknown, simplifiedFile]); + + const [loading, displayName, startIcon, onClick] = useBreadcrumbButtons({ + name, + is_latest: false, + path: f.path, + }); + + const StartIcon = useMemo(() => { + if (loading) { + return ; + } + if (startIcon?.Icons?.[0]) { + const Icon = startIcon?.Icons?.[0]; + return ; + } + if (startIcon?.Element) { + return startIcon.Element({ sx: { width: 20, height: 20 } }); + } + }, [startIcon, loading]); + + const tooltip = useMemo(() => { + if (unknown) { + return t("application:modals.unknownParentDes"); + } + + return ""; + }, [file, unknown, simplifiedFile]); + + const parent = useMemo(() => { + const uri = simplifiedFile?.path ?? file?.path; + if (!uri) { + return ""; + } + + const crUri = new CrUri(uri); + return crUri.parent().toString(); + }, [file, unknown, simplifiedFile]); + + return ( + <> + + + + {StartIcon ? ( + StartIcon + ) : ( + + )} + + + {name == "" ? displayName : name} + + + + + {!unknown && ( + + + + + + )} + + ); +}; + +export default FileBadge; diff --git a/src/component/FileManager/FileIcon.js b/src/component/FileManager/FileIcon.js deleted file mode 100644 index 377608f..0000000 --- a/src/component/FileManager/FileIcon.js +++ /dev/null @@ -1,302 +0,0 @@ -import { - ButtonBase, - Divider, - fade, - Tooltip, - Typography, - withStyles, -} from "@material-ui/core"; -import classNames from "classnames"; -import PropTypes from "prop-types"; -import React, { Component } from "react"; -import ContentLoader from "react-content-loader"; -import { LazyLoadImage } from "react-lazy-load-image-component"; -import { connect } from "react-redux"; -import { withRouter } from "react-router"; -import { baseURL } from "../../middleware/Api"; -import pathHelper from "../../utils/page"; -import TypeIcon from "./TypeIcon"; -import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; -import statusHelper from "../../utils/page"; -import Grow from "@material-ui/core/Grow"; -import FileName from "./FileName"; - -const styles = (theme) => ({ - container: {}, - - selected: { - "&:hover": { - border: "1px solid #d0d0d0", - }, - backgroundColor: fade( - theme.palette.primary.main, - theme.palette.type === "dark" ? 0.3 : 0.18 - ), - }, - - notSelected: { - "&:hover": { - backgroundColor: theme.palette.background.default, - border: "1px solid #d0d0d0", - }, - backgroundColor: theme.palette.background.paper, - }, - - button: { - border: "1px solid " + theme.palette.divider, - width: "100%", - borderRadius: theme.shape.borderRadius, - boxSizing: "border-box", - transition: - "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", - alignItems: "initial", - display: "initial", - }, - folderNameSelected: { - color: - theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, - fontWeight: "500", - }, - folderNameNotSelected: { - color: theme.palette.text.secondary, - }, - folderName: { - marginTop: "15px", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - marginRight: "20px", - }, - preview: { - overflow: "hidden", - height: "150px", - width: "100%", - borderRadius: "12px 12px 0 0", - backgroundColor: theme.palette.background.default, - }, - previewIcon: { - overflow: "hidden", - height: "149px", - width: "100%", - borderRadius: "12px 12px 0 0", - backgroundColor: theme.palette.background.paper, - paddingTop: "50px", - }, - iconBig: { - fontSize: 50, - }, - picPreview: { - objectFit: "cover", - width: "100%", - height: "100%", - }, - fileInfo: { - height: "50px", - display: "flex", - }, - icon: { - margin: "10px 10px 10px 16px", - height: "30px", - minWidth: "30px", - backgroundColor: theme.palette.background.paper, - borderRadius: "90%", - paddingTop: "3px", - color: theme.palette.text.secondary, - }, - hide: { - display: "none", - }, - loadingAnimation: { - borderRadius: "12px 12px 0 0", - height: "100%", - width: "100%", - }, - shareFix: { - marginLeft: "20px", - }, - checkIcon: { - color: theme.palette.primary.main, - }, - noDrag: { - userDrag: "none", - }, -}); - -const mapStateToProps = (state) => { - return { - path: state.navigator.path, - selected: state.explorer.selected, - shareInfo: state.viewUpdate.shareInfo, - }; -}; - -const mapDispatchToProps = () => { - return {}; -}; - -class FileIconCompoment extends Component { - static defaultProps = { - share: false, - }; - - state = { - loading: false, - showPicIcon: false, - }; - - shouldComponentUpdate(nextProps, nextState, nextContext) { - const isSelectedCurrent = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - const isSelectedNext = - nextProps.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - if ( - nextProps.selected !== this.props.selected && - isSelectedCurrent === isSelectedNext - ) { - return false; - } - - return true; - } - - render() { - const { classes } = this.props; - const isSelected = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - const isSharePage = pathHelper.isSharePage( - this.props.location.pathname - ); - const isMobile = statusHelper.isMobile(); - - return ( -
- - {this.props.file.thumb && !this.state.showPicIcon && ( -
- - this.setState({ loading: false }) - } - beforeLoad={() => - this.setState({ loading: true }) - } - onError={() => - this.setState({ showPicIcon: true }) - } - /> - - - -
- )} - {(!this.props.file.thumb || this.state.showPicIcon) && ( -
- -
- )} - {(!this.props.file.thumb || this.state.showPicIcon) && ( - - )} -
- {!this.props.share && ( -
- {!isSelected && ( - - )} - {isSelected && ( - - - - )} -
- )} - - - - - -
-
-
- ); - } -} - -FileIconCompoment.propTypes = { - classes: PropTypes.object.isRequired, - file: PropTypes.object.isRequired, -}; - -const FileIcon = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(FileIconCompoment))); - -export default FileIcon; diff --git a/src/component/FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx b/src/component/FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx new file mode 100644 index 0000000..ae31fd4 --- /dev/null +++ b/src/component/FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx @@ -0,0 +1,212 @@ +import { + Box, + Button, + Divider, + Popover, + styled, + Tooltip, + useTheme, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { CSSProperties, useCallback, useState } from "react"; +import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { bindPopover } from "material-ui-popup-state"; +import Sketch from "@uiw/react-color-sketch"; + +export interface CircleColorSelectorProps { + colors: string[]; + selectedColor: string; + onChange: (color: string) => void; + showColorValueInCustomization?: boolean; +} + +export const customizeMagicColor = "-"; + +export const SelectorBox = styled(Box)({ + display: "flex", + flexWrap: "wrap", + gap: 4, +}); + +interface ColorCircleProps { + color: string; + selected: boolean; + isCustomization?: boolean; + onClick: (e: React.MouseEvent) => void; + size?: number; + noMb?: boolean; +} + +const ColorCircleBox = styled("div")(({ + color, + selected, + size = 20, + + noMb, +}: { + color: string; + selected: boolean; + size?: number; + noMb?: boolean; +}) => { + return { + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: `${size}px`, + height: `${size}px`, + padding: "3px", + borderRadius: "50%", + marginRight: 0, + marginTop: 0, + marginBottom: noMb ? 0 : "4px", + boxSizing: "border-box", + transform: "scale(1)", + boxShadow: `${color} 0px 0px ${selected ? 5 : 0}px`, + transition: "transform 100ms ease 0s, box-shadow 100ms ease 0s", + background: color, + ":hover": { + transform: "scale(1.2)", + }, + }; +}); + +const ColorCircleBoxChild = styled("div")(({ + selected, +}: { + selected: boolean; +}) => { + const theme = useTheme(); + return { + "--circle-point-background-color": theme.palette.background.default, + height: selected ? "100%" : 0, + width: selected ? "100%" : 0, + borderRadius: "50%", + backgroundColor: "var(--circle-point-background-color)", + boxSizing: "border-box", + transition: "height 100ms ease 0s, width 100ms ease 0s", + transform: "scale(0.5)", + }; +}); + +export const ColorCircle = ({ + color, + selected, + isCustomization, + onClick, + size, + noMb, +}: ColorCircleProps) => { + const { t } = useTranslation(); + const displayColor = isCustomization + ? "conic-gradient(red, yellow, lime, aqua, blue, magenta, red)" + : color == "" + ? "linear-gradient(45deg, rgba(217,217,217,1) 46%, rgba(217,217,217,1) 47%, rgba(128,128,128,1) 47%)" + : color; + return ( + + + + + + ); +}; + +const CircleColorSelector = (props: CircleColorSelectorProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [customizeColor, setCustomizeColor] = useState( + props.selectedColor, + ); + const popupState = usePopupState({ + variant: "popover", + popupId: "color-picker", + }); + + const onClick = useCallback( + (color: string) => () => { + if (color === customizeMagicColor) { + return; + } + props.onChange(color); + }, + [props.onChange], + ); + + const { onClose, ...restPopover } = bindPopover(popupState); + const onApply = () => { + onClose(); + onClick(customizeColor)(); + }; + return ( + + {props.colors.map((color) => ( + + ))} + + { + setCustomizeColor(color.hex); + }} + /> + + + + + + + ); +}; + +export default CircleColorSelector; diff --git a/src/component/FileManager/FileInfo/FolderColorQuickAction.tsx b/src/component/FileManager/FileInfo/FolderColorQuickAction.tsx new file mode 100644 index 0000000..f1b8efc --- /dev/null +++ b/src/component/FileManager/FileInfo/FolderColorQuickAction.tsx @@ -0,0 +1,79 @@ +import { + Box, + BoxProps, + Stack, + styled, + Typography, + useTheme, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useMemo, useState } from "react"; +import { FileResponse, Metadata } from "../../../api/explorer.ts"; +import CircleColorSelector, { + customizeMagicColor, +} from "./ColorCircle/CircleColorSelector.tsx"; +import SessionManager, { UserSettings } from "../../../session"; +import { defaultColors } from "../../../constants"; + +const StyledBox = styled(Box)(({ theme }) => ({ + margin: `0 ${theme.spacing(0.5)}`, + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, +})); + +export interface FolderColorQuickActionProps extends BoxProps { + file: FileResponse; + onColorChange: (color?: string) => void; +} + +const FolderColorQuickAction = ({ + file, + onColorChange, + ...rest +}: FolderColorQuickActionProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [hex, setHex] = useState( + (file.metadata && file.metadata[Metadata.icon_color]) ?? + theme.palette.action.active, + ); + const presetColors = useMemo(() => { + const colors = new Set(defaultColors); + + const recentColors = SessionManager.get( + UserSettings.UsedCustomizedIconColors, + ) as string[] | undefined; + + if (recentColors) { + recentColors.forEach((color) => { + colors.add(color); + }); + } + + return [...colors]; + }, []); + return ( + + + + {t("application:fileManager.folderColor")} + + { + onColorChange( + color == theme.palette.action.active ? undefined : color, + ); + setHex(color); + }} + /> + + + ); +}; + +export default FolderColorQuickAction; diff --git a/src/component/FileManager/FileManager.js b/src/component/FileManager/FileManager.js deleted file mode 100644 index c446ccd..0000000 --- a/src/component/FileManager/FileManager.js +++ /dev/null @@ -1,116 +0,0 @@ -import React, { Component } from "react"; -import { DndProvider } from "react-dnd"; -import HTML5Backend from "react-dnd-html5-backend"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import { changeSubTitle } from "../../redux/viewUpdate/action"; -import pathHelper from "../../utils/page"; -import DragLayer from "./DnD/DragLayer"; -import Explorer from "./Explorer"; -import Modals from "./Modals"; -import Navigator from "./Navigator/Navigator"; -import SideDrawer from "./Sidebar/SideDrawer"; -import classNames from "classnames"; -import { - closeAllModals, - navigateTo, - setSelectedTarget, - toggleSnackbar, -} from "../../redux/explorer"; -import PaginationFooter from "./Pagination"; -import withStyles from "@material-ui/core/styles/withStyles"; - -const styles = (theme) => ({ - root: { - display: "flex", - flexDirection: "column", - height: "calc(100vh - 64px)", - [theme.breakpoints.down("xs")]: { - height: "100%", - }, - }, - rootShare: { - display: "flex", - flexDirection: "column", - height: "100%", - minHeight: 500, - }, - explorer: { - display: "flex", - flexDirection: "column", - overflowY: "auto", - }, -}); - -const mapStateToProps = () => ({}); - -const mapDispatchToProps = (dispatch) => { - return { - changeSubTitle: (text) => { - dispatch(changeSubTitle(text)); - }, - setSelectedTarget: (targets) => { - dispatch(setSelectedTarget(targets)); - }, - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - closeAllModals: () => { - dispatch(closeAllModals()); - }, - navigateTo: (path) => { - dispatch(navigateTo(path)); - }, - }; -}; - -class FileManager extends Component { - constructor(props) { - super(props); - this.image = React.createRef(); - } - componentWillUnmount() { - this.props.setSelectedTarget([]); - this.props.closeAllModals(); - this.props.navigateTo("/"); - } - - componentDidMount() { - if (pathHelper.isHomePage(this.props.location.pathname)) { - this.props.changeSubTitle(null); - } - } - render() { - const { classes } = this.props; - return ( -
- - - -
- - -
- - -
- -
- ); - } -} - -FileManager.propTypes = {}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(FileManager))); diff --git a/src/component/FileManager/FileManager.tsx b/src/component/FileManager/FileManager.tsx new file mode 100644 index 0000000..606c9c9 --- /dev/null +++ b/src/component/FileManager/FileManager.tsx @@ -0,0 +1,106 @@ +import { Box, Stack, useMediaQuery, useTheme } from "@mui/material"; +import { useEffect } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import useNavigation from "../../hooks/useNavigation.tsx"; +import { clearSelected } from "../../redux/fileManagerSlice.ts"; +import { resetDialogs } from "../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../redux/hooks.ts"; +import { resetFm, selectAll, shortCutDelete } from "../../redux/thunks/filemanager.ts"; +import ImageViewer from "../Viewers/ImageViewer/ImageViewer.tsx"; +import Explorer from "./Explorer/Explorer.tsx"; +import { FmIndexContext } from "./FmIndexContext.tsx"; +import PaginationFooter from "./Pagination/PaginationFooter.tsx"; +import Sidebar from "./Sidebar/Sidebar.tsx"; +import SidebarDialog from "./Sidebar/SidebarDialog.tsx"; +import NavHeader from "./TopBar/NavHeader.tsx"; + +export const FileManagerIndex = { + main: 0, + selector: 1, +}; + +export interface FileManagerProps { + index?: number; + initialPath?: string; + skipRender?: boolean; +} + +export const FileManager = ({ index = 0, initialPath, skipRender }: FileManagerProps) => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isTablet = useMediaQuery(theme.breakpoints.down("md")); + + useNavigation(index, initialPath); + + useEffect(() => { + if (index == FileManagerIndex.main) { + dispatch(resetDialogs()); + return () => { + dispatch(resetFm(index)); + }; + } + }, []); + + const selectAllRef = useHotkeys( + ["Control+a", "Meta+a"], + () => { + dispatch(selectAll(index)); + }, + { enabled: index == FileManagerIndex.main, preventDefault: true }, + ); + + const delRef = useHotkeys( + ["meta+backspace", "delete"], + () => { + dispatch(shortCutDelete(index)); + }, + { enabled: index == FileManagerIndex.main, preventDefault: true }, + ); + + const escRef = useHotkeys( + "esc", + () => { + dispatch(clearSelected({ index, value: {} })); + }, + { enabled: index == FileManagerIndex.main, preventDefault: true }, + ); + + if (skipRender) { + return null; + } + + return ( + + { + e.currentTarget.focus(); + }} + ref={(ref) => { + selectAllRef(ref); + delRef(ref); + escRef(ref); + }} + direction={"column"} + sx={{ + flexGrow: 1, + mb: index == FileManagerIndex.main && !isMobile ? 1 : 0, + overflow: "auto", + "&:focus": { + outline: "none", + }, + }} + tabIndex={0} + spacing={1} + > + + + + {index == FileManagerIndex.main && (isTablet ? : )} + + + + {index == FileManagerIndex.main && } + + ); +}; diff --git a/src/component/FileManager/FileName.js b/src/component/FileManager/FileName.js deleted file mode 100644 index 81813f8..0000000 --- a/src/component/FileManager/FileName.js +++ /dev/null @@ -1,28 +0,0 @@ -import Highlighter from "react-highlight-words"; -import { trimPrefix } from "../Uploader/core/utils"; -import React from "react"; -import { useSelector } from "react-redux"; -import { makeStyles } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - highlight: { - backgroundColor: theme.palette.warning.light, - }, -})); - -export default function FileName({ name }) { - const classes = useStyles(); - const search = useSelector((state) => state.explorer.search); - if (!search) { - return name; - } - - return ( - - ); -} diff --git a/src/component/FileManager/FmIndexContext.tsx b/src/component/FileManager/FmIndexContext.tsx new file mode 100644 index 0000000..47b7864 --- /dev/null +++ b/src/component/FileManager/FmIndexContext.tsx @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const FmIndexContext = createContext(0); \ No newline at end of file diff --git a/src/component/FileManager/Folder.js b/src/component/FileManager/Folder.js deleted file mode 100644 index d89ba5a..0000000 --- a/src/component/FileManager/Folder.js +++ /dev/null @@ -1,128 +0,0 @@ -import React from "react"; -import FolderIcon from "@material-ui/icons/Folder"; -import classNames from "classnames"; -import { - ButtonBase, - fade, - makeStyles, - Tooltip, - Typography, -} from "@material-ui/core"; -import { useSelector } from "react-redux"; -import statusHelper from "../../utils/page"; -import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; - -const useStyles = makeStyles((theme) => ({ - container: { - padding: "7px", - }, - - selected: { - "&:hover": { - border: "1px solid #d0d0d0", - }, - backgroundColor: fade( - theme.palette.primary.main, - theme.palette.type === "dark" ? 0.3 : 0.18 - ), - }, - - notSelected: { - "&:hover": { - backgroundColor: theme.palette.background.default, - border: "1px solid #d0d0d0", - }, - backgroundColor: theme.palette.background.paper, - }, - - button: { - height: "50px", - border: "1px solid " + theme.palette.divider, - width: "100%", - borderRadius: theme.shape.borderRadius, - boxSizing: "border-box", - transition: - "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", - display: "flex", - justifyContent: "left", - alignItems: "initial", - }, - icon: { - margin: "10px 10px 10px 16px", - height: "30px", - minWidth: "30px", - backgroundColor: theme.palette.background.paper, - borderRadius: "90%", - paddingTop: "3px", - color: theme.palette.text.secondary, - }, - folderNameSelected: { - color: - theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, - fontWeight: "500", - }, - folderNameNotSelected: { - color: theme.palette.text.secondary, - }, - folderName: { - marginTop: "15px", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - marginRight: "20px", - }, - active: { - boxShadow: "0 0 0 2px " + theme.palette.primary.light, - }, - checkIcon: { - color: theme.palette.primary.main, - }, -})); - -export default function Folder({ folder, isActive, onIconClick }) { - const selected = useSelector((state) => state.explorer.selected); - const classes = useStyles(); - const isMobile = statusHelper.isMobile(); - const isSelected = - selected.findIndex((value) => { - return value === folder; - }) !== -1; - - return ( - -
- {!isSelected && } - {isSelected && ( - - )} -
- - - {folder.name} - - -
- ); -} diff --git a/src/component/FileManager/FolderPicker.tsx b/src/component/FileManager/FolderPicker.tsx new file mode 100644 index 0000000..c15d516 --- /dev/null +++ b/src/component/FileManager/FolderPicker.tsx @@ -0,0 +1,70 @@ +import { Box, styled, useMediaQuery, useTheme } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { useAppSelector } from "../../redux/hooks.ts"; +import { getFileLinkedUri } from "../../util"; // Grid version 2 +import ContextMenu from "./ContextMenu/ContextMenu.tsx"; +import { FileManager, FileManagerIndex } from "./FileManager.tsx"; +import TreeNavigation from "./TreeView/TreeNavigation.tsx"; + +const StyledGridItem = styled(Grid)(() => ({ + display: "flex", + height: "100%", +})); + +export const useFolderSelector = () => { + const currentPath = useAppSelector((state) => state.fileManager[FileManagerIndex.selector].pure_path); + const selected = useAppSelector((state) => state.fileManager[FileManagerIndex.selector].selected); + + if (selected && Object.keys(selected).length > 0) { + const selectedFile = selected[Object.keys(selected)[0]]; + return [selectedFile, getFileLinkedUri(selectedFile)] as const; + } + + return [undefined, currentPath] as const; +}; + +export interface FolderPickerProps { + disableSharedWithMe?: boolean; + disableTrash?: boolean; + initialPath?: string; +} + +const FolderPicker = ({ disableSharedWithMe, disableTrash, initialPath }: FolderPickerProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const path = useAppSelector((state) => state.fileManager[FileManagerIndex.main].path); + + return ( + + + + + + + + + + + + ); +}; +export default FolderPicker; diff --git a/src/component/FileManager/ImgPreview.js b/src/component/FileManager/ImgPreview.js deleted file mode 100644 index e367bc2..0000000 --- a/src/component/FileManager/ImgPreview.js +++ /dev/null @@ -1,137 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import { baseURL } from "../../middleware/Api"; -import { imgPreviewSuffix } from "../../config"; -import { withStyles } from "@material-ui/core"; -import pathHelper from "../../utils/page"; -import { withRouter } from "react-router"; -import { PhotoSlider } from "react-photo-view"; -import "react-photo-view/dist/index.css"; -import * as explorer from "../../redux/explorer/reducer"; -import { showImgPreivew } from "../../redux/explorer"; - -const styles = () => ({}); - -const mapStateToProps = (state) => { - return { - first: state.explorer.imgPreview.first, - other: state.explorer.imgPreview.other, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - showImgPreivew: (first) => { - dispatch(showImgPreivew(first)); - }, - }; -}; - -class ImagPreviewComponent extends Component { - state = { - items: [], - photoIndex: 0, - isOpen: false, - }; - - UNSAFE_componentWillReceiveProps = (nextProps) => { - const items = []; - let firstOne = 0; - if (nextProps.first.id !== "") { - if ( - pathHelper.isSharePage(this.props.location.pathname) && - !nextProps.first.path - ) { - const newImg = { - intro: nextProps.first.name, - src: baseURL + "/share/preview/" + nextProps.first.key, - }; - firstOne = 0; - items.push(newImg); - this.setState({ - photoIndex: firstOne, - items: items, - isOpen: true, - }); - return; - } - // eslint-disable-next-line - nextProps.other.map((value) => { - const fileType = value.name.split(".").pop().toLowerCase(); - if (imgPreviewSuffix.indexOf(fileType) !== -1) { - let src = ""; - if (pathHelper.isSharePage(this.props.location.pathname)) { - src = baseURL + "/share/preview/" + value.key; - src = - src + - "?path=" + - encodeURIComponent( - value.path === "/" - ? value.path + value.name - : value.path + "/" + value.name - ); - } else { - src = baseURL + "/file/preview/" + value.id; - } - const newImg = { - intro: value.name, - src: src, - }; - if ( - value.path === nextProps.first.path && - value.name === nextProps.first.name - ) { - firstOne = items.length; - } - items.push(newImg); - } - }); - this.setState({ - photoIndex: firstOne, - items: items, - isOpen: true, - }); - } - }; - - handleClose = () => { - this.props.showImgPreivew(explorer.initState.imgPreview.first); - this.setState({ - isOpen: false, - }); - }; - - render() { - const { photoIndex, isOpen, items } = this.state; - - return ( -
- {isOpen && ( - this.handleClose()} - index={photoIndex} - onIndexChange={(n) => - this.setState({ - photoIndex: n, - }) - } - /> - )} -
- ); - } -} - -ImagPreviewComponent.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const ImgPreivew = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(ImagPreviewComponent))); - -export default ImgPreivew; diff --git a/src/component/FileManager/Modals.js b/src/component/FileManager/Modals.js deleted file mode 100644 index 2560a9c..0000000 --- a/src/component/FileManager/Modals.js +++ /dev/null @@ -1,725 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import PathSelector from "./PathSelector"; -import API from "../../middleware/Api"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField, - withStyles, -} from "@material-ui/core"; -import Loading from "../Modals/Loading"; -import CopyDialog from "../Modals/Copy"; -import DirectoryDownloadDialog from "../Modals/DirectoryDownload"; -import CreatShare from "../Modals/CreateShare"; -import { withRouter } from "react-router-dom"; -import DecompressDialog from "../Modals/Decompress"; -import CompressDialog from "../Modals/Compress"; -import { - closeAllModals, - openLoadingDialog, - refreshFileList, - refreshStorage, - setModalsLoading, - toggleSnackbar, -} from "../../redux/explorer"; -import OptionSelector from "../Modals/OptionSelector"; -import { getDownloadURL } from "../../services/file"; -import { Trans, withTranslation } from "react-i18next"; -import RemoteDownload from "../Modals/RemoteDownload"; -import Delete from "../Modals/Delete"; - -const styles = (theme) => ({ - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, - contentFix: { - padding: "10px 24px 0px 24px", - }, -}); - -const mapStateToProps = (state) => { - return { - path: state.navigator.path, - selected: state.explorer.selected, - modalsStatus: state.viewUpdate.modals, - modalsLoading: state.viewUpdate.modalsLoading, - dirList: state.explorer.dirList, - fileList: state.explorer.fileList, - dndSignale: state.explorer.dndSignal, - dndTarget: state.explorer.dndTarget, - dndSource: state.explorer.dndSource, - loading: state.viewUpdate.modals.loading, - loadingText: state.viewUpdate.modals.loadingText, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - closeAllModals: () => { - dispatch(closeAllModals()); - }, - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - setModalsLoading: (status) => { - dispatch(setModalsLoading(status)); - }, - refreshFileList: () => { - dispatch(refreshFileList()); - }, - refreshStorage: () => { - dispatch(refreshStorage()); - }, - openLoadingDialog: (text) => { - dispatch(openLoadingDialog(text)); - }, - }; -}; - -class ModalsCompoment extends Component { - state = { - newFolderName: "", - newFileName: "", - newName: "", - selectedPath: "", - selectedPathName: "", - secretShare: false, - sharePwd: "", - shareUrl: "", - purchaseCallback: null, - }; - - handleInputChange = (e) => { - this.setState({ - [e.target.id]: e.target.value, - }); - }; - - newNameSuffix = ""; - - UNSAFE_componentWillReceiveProps = (nextProps) => { - if (this.props.dndSignale !== nextProps.dndSignale) { - this.dragMove(nextProps.dndSource, nextProps.dndTarget); - return; - } - - if (this.props.modalsStatus.rename !== nextProps.modalsStatus.rename) { - const name = nextProps.selected[0].name; - this.setState({ - newName: name, - }); - return; - } - }; - - scoreHandler = (callback) => { - callback(); - }; - - Download = () => { - getDownloadURL(this.props.selected[0]) - .then((response) => { - window.location.assign(response.data); - this.onClose(); - this.downloaded = true; - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.onClose(); - }); - }; - - submitMove = (e) => { - if (e != null) { - e.preventDefault(); - } - this.props.setModalsLoading(true); - const dirs = [], - items = []; - // eslint-disable-next-line - this.props.selected.map((value) => { - if (value.type === "dir") { - dirs.push(value.id); - } else { - items.push(value.id); - } - }); - API.patch("/object", { - action: "move", - src_dir: this.props.selected[0].path, - src: { - dirs: dirs, - items: items, - }, - dst: this.DragSelectedPath - ? this.DragSelectedPath - : this.state.selectedPath === "//" - ? "/" - : this.state.selectedPath, - }) - .then(() => { - this.onClose(); - this.props.refreshFileList(); - this.props.setModalsLoading(false); - this.DragSelectedPath = ""; - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.props.setModalsLoading(false); - this.DragSelectedPath = ""; - }) - .then(() => { - this.props.closeAllModals(); - }); - }; - - dragMove = (source, target) => { - if (this.props.selected.length === 0) { - this.props.selected[0] = source; - } - let doMove = true; - - // eslint-disable-next-line - this.props.selected.map((value) => { - // 根据ID过滤 - if (value.id === target.id && value.type === target.type) { - doMove = false; - // eslint-disable-next-line - return; - } - // 根据路径过滤 - if ( - value.path === - target.path + (target.path === "/" ? "" : "/") + target.name - ) { - doMove = false; - // eslint-disable-next-line - return; - } - }); - if (doMove) { - this.DragSelectedPath = - target.path === "/" - ? target.path + target.name - : target.path + "/" + target.name; - this.props.openLoadingDialog(this.props.t("modals.processing")); - this.submitMove(); - } - }; - - submitRename = (e) => { - e.preventDefault(); - this.props.setModalsLoading(true); - const newName = this.state.newName; - - const src = { - dirs: [], - items: [], - }; - - if (this.props.selected[0].type === "dir") { - src.dirs[0] = this.props.selected[0].id; - } else { - src.items[0] = this.props.selected[0].id; - } - - // 检查重名 - if ( - this.props.dirList.findIndex((value) => { - return value.name === newName; - }) !== -1 || - this.props.fileList.findIndex((value) => { - return value.name === newName; - }) !== -1 - ) { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("modals.duplicatedObjectName"), - "warning" - ); - this.props.setModalsLoading(false); - } else { - API.post("/object/rename", { - action: "rename", - src: src, - new_name: newName, - }) - .then(() => { - this.onClose(); - this.props.refreshFileList(); - this.props.setModalsLoading(false); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.props.setModalsLoading(false); - }); - } - }; - - submitCreateNewFolder = (e) => { - e.preventDefault(); - this.props.setModalsLoading(true); - if ( - this.props.dirList.findIndex((value) => { - return value.name === this.state.newFolderName; - }) !== -1 - ) { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("modals.duplicatedFolderName"), - "warning" - ); - this.props.setModalsLoading(false); - } else { - API.put("/directory", { - path: - (this.props.path === "/" ? "" : this.props.path) + - "/" + - this.state.newFolderName, - }) - .then(() => { - this.onClose(); - this.props.refreshFileList(); - this.props.setModalsLoading(false); - }) - .catch((error) => { - this.props.setModalsLoading(false); - - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - } - //this.props.toggleSnackbar(); - }; - - submitCreateNewFile = (e) => { - e.preventDefault(); - this.props.setModalsLoading(true); - if ( - this.props.dirList.findIndex((value) => { - return value.name === this.state.newFileName; - }) !== -1 - ) { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("modals.duplicatedFolderName"), - "warning" - ); - this.props.setModalsLoading(false); - } else { - API.post("/file/create", { - path: - (this.props.path === "/" ? "" : this.props.path) + - "/" + - this.state.newFileName, - }) - .then(() => { - this.onClose(); - this.props.refreshFileList(); - this.props.setModalsLoading(false); - }) - .catch((error) => { - this.props.setModalsLoading(false); - - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - } - //this.props.toggleSnackbar(); - }; - - setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - this.setState({ - selectedPath: path, - selectedPathName: folder.name, - }); - }; - - onClose = () => { - this.setState({ - newFolderName: "", - newFileName: "", - newName: "", - selectedPath: "", - selectedPathName: "", - secretShare: false, - sharePwd: "", - shareUrl: "", - }); - this.newNameSuffix = ""; - this.props.closeAllModals(); - }; - - handleChange = (name) => (event) => { - this.setState({ [name]: event.target.checked }); - }; - - copySource = () => { - if (navigator.clipboard) { - navigator.clipboard.writeText(this.props.modalsStatus.getSource); - this.props.toggleSnackbar( - "top", - "right", - this.props.t("modals.linkCopied"), - "info" - ); - } - }; - - render() { - const { classes, t } = this.props; - - return ( -
- - - - - {t("modals.getSourceLinkTitle")} - - - - - - - - - - - - - {t("fileManager.newFolder")} - - - -
- this.handleInputChange(e)} - fullWidth - /> - -
- - -
- -
-
-
- - - - {t("fileManager.newFile")} - - - -
- this.handleInputChange(e)} - fullWidth - /> - -
- - -
- -
-
-
- - - - {t("fileManager.rename")} - - - - ]} - /> - -
- this.handleInputChange(e)} - fullWidth - /> - -
- - -
- -
-
-
- - - - - {t("modals.moveToTitle")} - - - - {this.state.selectedPath !== "" && ( - - - ]} - /> - - - )} - - -
- -
-
-
- - - - - - -
- ); - } -} - -ModalsCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const Modals = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(ModalsCompoment)))); - -export default Modals; diff --git a/src/component/FileManager/MusicPlayer.js b/src/component/FileManager/MusicPlayer.js deleted file mode 100644 index 1e46afc..0000000 --- a/src/component/FileManager/MusicPlayer.js +++ /dev/null @@ -1,467 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - List, - Slider, - withStyles, -} from "@material-ui/core"; -import IconButton from "@material-ui/core/IconButton"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import ListItemText from "@material-ui/core/ListItemText"; -import MusicNote from "@material-ui/icons/MusicNote"; -import PlayArrow from "@material-ui/icons/PlayArrow"; -import PlayNext from "@material-ui/icons/SkipNext"; -import PlayPrev from "@material-ui/icons/SkipPrevious"; -import Pause from "@material-ui/icons/Pause"; -import { Repeat, RepeatOne, Shuffle } from "@material-ui/icons"; -import PropTypes from "prop-types"; -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { withRouter } from "react-router"; -import { audioPreviewSuffix } from "../../config"; -import { baseURL } from "../../middleware/Api"; -import * as explorer from "../../redux/explorer/reducer"; -import pathHelper from "../../utils/page"; -import { - audioPreviewSetIsOpen, - audioPreviewSetPlaying, - showAudioPreview, -} from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const styles = (theme) => ({ - list: { - //maxWidth: 360, - backgroundColor: theme.palette.background.paper, - position: "relative", - overflow: "auto", - maxHeight: 300, - }, - slider_root: { - "vertical-align": "middle", - }, -}); - -const mapStateToProps = (state) => { - return { - first: state.explorer.audioPreview.first, - other: state.explorer.audioPreview.other, - isOpen: state.explorer.audioPreview.isOpen, - playingName: state.explorer.audioPreview.playingName, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - showAudioPreview: (first) => { - dispatch(showAudioPreview(first)); - }, - audioPreviewSetIsOpen: (first) => { - dispatch(audioPreviewSetIsOpen(first)); - }, - audioPreviewSetPlaying: (playingName, paused) => { - dispatch(audioPreviewSetPlaying(playingName, paused)); - }, - }; -}; - -class MusicPlayerComponent extends Component { - state = { - items: [], - currentIndex: 0, - //isOpen: false, - //isPlay:false, - currentTime: 0, - duration: 0, - progressText: "00:00/00:00", - looptype: 0, - }; - myAudioRef = React.createRef(); - - UNSAFE_componentWillReceiveProps = (nextProps) => { - const items = []; - let firstOne = 0; - if (nextProps.first.id !== "") { - if ( - pathHelper.isSharePage(this.props.location.pathname) && - !nextProps.first.path - ) { - const newItem = { - intro: nextProps.first.name, - src: baseURL + "/share/preview/" + nextProps.first.key, - }; - firstOne = 0; - items.push(newItem); - this.setState({ - currentIndex: firstOne, - items: items, - //isOpen: true, - }); - this.props.audioPreviewSetIsOpen(true); - this.props.showAudioPreview( - explorer.initState.audioPreview.first - ); - return; - } - // eslint-disable-next-line - nextProps.other.map((value) => { - const fileType = value.name.split(".").pop().toLowerCase(); - if (audioPreviewSuffix.indexOf(fileType) !== -1) { - let src = ""; - if (pathHelper.isSharePage(this.props.location.pathname)) { - src = baseURL + "/share/preview/" + value.key; - src = - src + - "?path=" + - encodeURIComponent( - value.path === "/" - ? value.path + value.name - : value.path + "/" + value.name - ); - } else { - src = baseURL + "/file/preview/" + value.id; - } - const newItem = { - intro: value.name, - src: src, - }; - if ( - value.path === nextProps.first.path && - value.name === nextProps.first.name - ) { - firstOne = items.length; - } - items.push(newItem); - } - }); - this.setState({ - currentIndex: firstOne, - items: items, - //isOpen: true, - }); - this.props.audioPreviewSetIsOpen(true); - this.props.showAudioPreview(explorer.initState.audioPreview.first); - } - }; - - handleItemClick = (currentIndex) => () => { - this.setState({ - currentIndex: currentIndex, - }); - }; - - handleClose = () => { - /*this.setState({ - isOpen: false, - });*/ - this.setState({ - currentIndex: -1, - }); - this.pause(); - this.props.audioPreviewSetPlaying(null, false); - this.props.audioPreviewSetIsOpen(false); - }; - backgroundPlay = () => { - this.props.audioPreviewSetIsOpen(false); - }; - - componentDidMount() { - if (this.myAudioRef.current) { - this.bindEvents(this.myAudioRef.current); - } - } - componentDidUpdate() { - if (this.myAudioRef.current) { - this.bindEvents(this.myAudioRef.current); - } - } - componentWillUnmount() { - this.unbindEvents(this.myAudioRef.current); - } - - bindEvents = (ele) => { - if (ele) { - ele.addEventListener("canplay", this.readyPlay); - ele.addEventListener("ended", this.loopnext); - ele.addEventListener("timeupdate", this.timeUpdate); - } - }; - - unbindEvents = (ele) => { - if (ele) { - ele.removeEventListener("canplay", this.readyPlay); - ele.removeEventListener("ended", this.loopnext); - ele.removeEventListener("timeupdate", this.timeUpdate); - } - }; - - readyPlay = () => { - this.play(); - }; - - formatTime = (s) => { - if (isNaN(s)) return "00:00"; - const minute = Math.floor(s / 60); - const second = Math.floor(s % 60); - return ( - `${minute}`.padStart(2, "0") + ":" + `${second}`.padStart(2, "0") - ); - }; - - timeUpdate = () => { - const currentTime = Math.floor(this.myAudioRef.current.currentTime); //this.myAudioRef.current.currentTime;// - this.setState({ - currentTime: currentTime, - duration: this.myAudioRef.current.duration, - progressText: - this.formatTime(currentTime) + - "/" + - this.formatTime(this.myAudioRef.current.duration), - }); - }; - - play = () => { - this.myAudioRef.current.play(); - /*this.setState({ - isPlay: true - });*/ - this.props.audioPreviewSetPlaying( - this.state.items[this.state.currentIndex].intro, - false - ); - }; - - pause = () => { - if (this.myAudioRef.current) { - this.myAudioRef.current.pause(); - } - /*this.setState({ - isPlay: false - })*/ - this.props.audioPreviewSetPlaying( - this.state.items[this.state.currentIndex]?.intro, - true - ); - }; - - playOrPaues = () => { - if (this.state.isPlay) { - this.pause(); - } else { - this.play(); - } - }; - changeLoopType = () => { - let lt = this.state.looptype + 1; - if (lt >= 3) { - lt = 0; - } - this.setState({ - looptype: lt, - }); - }; - loopnext = () => { - let index = this.state.currentIndex; - if (this.state.looptype == 0) { - //all - index = index + 1; - if (index >= this.state.items.length) { - index = 0; - } - } else if (this.state.looptype == 1) { - //single - //index=index; - } else if (this.state.looptype == 2) { - //random - if (this.state.items.length <= 2) { - index = index + 1; - if (index >= this.state.items.length) { - index = 0; - } - } else { - while (index == this.state.currentIndex) { - index = Math.floor(Math.random() * this.state.items.length); - } - } - } - if (this.state.currentIndex == index) { - this.myAudioRef.current.currentTime = 0; - this.play(); - } - this.setState({ - currentIndex: index, - }); - }; - - prev = () => { - let index = this.state.currentIndex - 1; - if (index < 0) { - index = this.state.items.length - 1; - } - this.setState({ - currentIndex: index, - }); - }; - - next = () => { - let index = this.state.currentIndex + 1; - if (index >= this.state.items.length) { - index = 0; - } - this.setState({ - currentIndex: index, - }); - }; - - handleProgress = (e, newValue) => { - this.myAudioRef.current.currentTime = newValue; - }; - - render() { - const { currentIndex, items } = this.state; - const { isOpen, classes, t } = this.props; - return ( - - - {t("fileManager.musicPlayer")} - - - - {items.map((value, idx) => { - const labelId = `label-${value.intro}`; - return ( - - - {idx === currentIndex ? ( - - ) : ( - - )} - - - - ); - })} - - - ); - } -} - -MusicPlayerComponent.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const MusicPlayer = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(MusicPlayerComponent)))); - -export default MusicPlayer; diff --git a/src/component/FileManager/Navigator/DropDown.js b/src/component/FileManager/Navigator/DropDown.js deleted file mode 100644 index d7ae5fa..0000000 --- a/src/component/FileManager/Navigator/DropDown.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import DropDownItem from "./DropDownItem"; - -export default function DropDown(props) { - let timer; - let first = props.folders.length; - const status = []; - for (let index = 0; index < props.folders.length; index++) { - status[index] = false; - } - - const setActiveStatus = (id, value) => { - status[id] = value; - if (value) { - clearTimeout(timer); - } else { - let shouldClose = true; - status.forEach((element) => { - if (element) { - shouldClose = false; - } - }); - if (shouldClose) { - if (first <= 0) { - timer = setTimeout(() => { - props.onClose(); - }, 100); - } else { - first--; - } - } - } - console.log(status); - }; - - return ( - <> - {props.folders.map((folder, id) => ( - - ))} - - ); -} diff --git a/src/component/FileManager/Navigator/DropDownItem.js b/src/component/FileManager/Navigator/DropDownItem.js deleted file mode 100644 index 6df0a10..0000000 --- a/src/component/FileManager/Navigator/DropDownItem.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useEffect } from "react"; -import { makeStyles } from "@material-ui/core"; -import FolderIcon from "@material-ui/icons/Folder"; -import { MenuItem, ListItemIcon, ListItemText } from "@material-ui/core"; -import { useDrop } from "react-dnd"; -import classNames from "classnames"; - -const useStyles = makeStyles((theme) => ({ - active: { - border: "2px solid " + theme.palette.primary.light, - }, -})); - -export default function DropDownItem(props) { - const [{ canDrop, isOver }, drop] = useDrop({ - accept: "object", - drop: () => { - console.log({ - folder: { - id: -1, - path: props.path, - name: props.folder === "/" ? "" : props.folder, - }, - }); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); - - const isActive = canDrop && isOver; - - useEffect(() => { - props.setActiveStatus(props.id, isActive); - // eslint-disable-next-line - }, [isActive]); - - const classes = useStyles(); - return ( - props.navigateTo(e, props.id)} - > - - - - - - ); -} diff --git a/src/component/FileManager/Navigator/Navigator.js b/src/component/FileManager/Navigator/Navigator.js deleted file mode 100644 index 7d02b1a..0000000 --- a/src/component/FileManager/Navigator/Navigator.js +++ /dev/null @@ -1,499 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; -import RightIcon from "@material-ui/icons/KeyboardArrowRight"; -import ShareIcon from "@material-ui/icons/Share"; -import NewFolderIcon from "@material-ui/icons/CreateNewFolder"; -import RefreshIcon from "@material-ui/icons/Refresh"; -import explorer, { - drawerToggleAction, - navigateTo, - navigateUp, - openCompressDialog, - openCreateFileDialog, - openCreateFolderDialog, - openShareDialog, - refreshFileList, - setNavigatorError, - setNavigatorLoadingStatus, - setSelectedTarget, -} from "../../../redux/explorer"; -import { fixUrlHash, setGetParameter } from "../../../utils/index"; -import { - Divider, - ListItemIcon, - Menu, - MenuItem, - withStyles, -} from "@material-ui/core"; -import PathButton from "./PathButton"; -import DropDown from "./DropDown"; -import pathHelper from "../../../utils/page"; -import classNames from "classnames"; -import Auth from "../../../middleware/Auth"; -import Avatar from "@material-ui/core/Avatar"; -import { Archive } from "@material-ui/icons"; -import { FilePlus } from "mdi-material-ui"; -import SubActions from "./SubActions"; -import { setCurrentPolicy } from "../../../redux/explorer/action"; -import { list } from "../../../services/navigate"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - path: state.navigator.path, - refresh: state.navigator.refresh, - drawerDesktopOpen: state.viewUpdate.open, - viewMethod: state.viewUpdate.explorerViewMethod, - search: state.explorer.search, - sortMethod: state.viewUpdate.sortMethod, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - navigateToPath: (path) => { - dispatch(navigateTo(path)); - }, - navigateUp: () => { - dispatch(navigateUp()); - }, - setNavigatorError: (status, msg) => { - dispatch(setNavigatorError(status, msg)); - }, - updateFileList: (list) => { - dispatch(explorer.actions.updateFileList(list)); - }, - setNavigatorLoadingStatus: (status) => { - dispatch(setNavigatorLoadingStatus(status)); - }, - refreshFileList: () => { - dispatch(refreshFileList()); - }, - setSelectedTarget: (target) => { - dispatch(setSelectedTarget(target)); - }, - openCreateFolderDialog: () => { - dispatch(openCreateFolderDialog()); - }, - openCreateFileDialog: () => { - dispatch(openCreateFileDialog()); - }, - openShareDialog: () => { - dispatch(openShareDialog()); - }, - handleDesktopToggle: (open) => { - dispatch(drawerToggleAction(open)); - }, - openCompressDialog: () => { - dispatch(openCompressDialog()); - }, - setCurrentPolicy: (policy) => { - dispatch(setCurrentPolicy(policy)); - }, - }; -}; - -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -const styles = (theme) => ({ - container: { - [theme.breakpoints.down("xs")]: { - display: "none", - }, - backgroundColor: theme.palette.background.paper, - }, - navigatorContainer: { - display: "flex", - justifyContent: "space-between", - }, - nav: { - height: "48px", - padding: "5px 15px", - display: "flex", - }, - optionContainer: { - paddingTop: "6px", - marginRight: "10px", - }, - rightIcon: { - marginTop: "6px", - verticalAlign: "top", - color: "#868686", - }, - expandMore: { - color: "#8d8d8d", - }, - roundBorder: { - borderRadius: "4px 4px 0 0", - }, -}); - -class NavigatorComponent extends Component { - search = undefined; - currentID = 0; - - state = { - hidden: false, - hiddenFolders: [], - folders: [], - anchorEl: null, - hiddenMode: false, - anchorHidden: null, - }; - - constructor(props) { - super(props); - this.element = React.createRef(); - } - - componentDidMount = () => { - const url = new URL(fixUrlHash(window.location.href)); - const c = url.searchParams.get("path"); - this.renderPath(c === null ? "/" : c); - - if (!this.props.isShare) { - // 如果是在个人文件管理页,首次加载时打开侧边栏 - this.props.handleDesktopToggle(true); - } - - // 后退操作时重新导航 - window.onpopstate = () => { - const url = new URL(fixUrlHash(window.location.href)); - const c = url.searchParams.get("path"); - if (c !== null) { - this.props.navigateToPath(c); - } - }; - }; - - renderPath = (path = null) => { - this.props.setNavigatorError(false, null); - this.setState({ - folders: - path !== null - ? path.substr(1).split("/") - : this.props.path.substr(1).split("/"), - }); - const newPath = path !== null ? path : this.props.path; - list( - newPath, - this.props.share, - this.search ? this.search.keywords : "", - this.search ? this.search.searchPath : "" - ) - .then((response) => { - this.currentID = response.data.parent; - this.props.updateFileList(response.data.objects); - this.props.setNavigatorLoadingStatus(false); - if (!this.search) { - setGetParameter("path", encodeURIComponent(newPath)); - } - if (response.data.policy) { - this.props.setCurrentPolicy({ - id: response.data.policy.id, - name: response.data.policy.name, - type: response.data.policy.type, - maxSize: response.data.policy.max_size, - allowedSuffix: response.data.policy.file_type, - }); - } - }) - .catch((error) => { - this.props.setNavigatorError(true, error); - }); - - this.checkOverFlow(true); - }; - - redresh = (path) => { - this.props.setNavigatorLoadingStatus(true); - this.props.setNavigatorError(false, "error"); - this.renderPath(path); - }; - - UNSAFE_componentWillReceiveProps = (nextProps) => { - if (this.props.search !== nextProps.search) { - this.search = nextProps.search; - } - if (this.props.path !== nextProps.path) { - this.renderPath(nextProps.path); - } - if (this.props.refresh !== nextProps.refresh) { - this.redresh(nextProps.path); - } - }; - - componentWillUnmount() { - this.props.updateFileList([]); - } - - componentDidUpdate = (prevProps, prevStates) => { - if (this.state.folders !== prevStates.folders) { - this.checkOverFlow(true); - } - if (this.props.drawerDesktopOpen !== prevProps.drawerDesktopOpen) { - delay(500).then(() => this.checkOverFlow()); - } - }; - - checkOverFlow = (force) => { - if (this.overflowInitLock && !force) { - return; - } - if (this.element.current !== null) { - const hasOverflowingChildren = - this.element.current.offsetHeight < - this.element.current.scrollHeight || - this.element.current.offsetWidth < - this.element.current.scrollWidth; - if (hasOverflowingChildren) { - this.overflowInitLock = true; - this.setState({ hiddenMode: true }); - } - if (!hasOverflowingChildren && this.state.hiddenMode) { - this.setState({ hiddenMode: false }); - } - } - }; - - navigateTo = (event, id) => { - if (id === this.state.folders.length - 1) { - //最后一个路径 - this.setState({ anchorEl: event.currentTarget }); - } else if ( - id === -1 && - this.state.folders.length === 1 && - this.state.folders[0] === "" - ) { - this.props.refreshFileList(); - this.handleClose(); - } else if (id === -1) { - this.props.navigateToPath("/"); - this.handleClose(); - } else { - this.props.navigateToPath( - "/" + this.state.folders.slice(0, id + 1).join("/") - ); - this.handleClose(); - } - }; - - handleClose = () => { - this.setState({ anchorEl: null, anchorHidden: null, anchorSort: null }); - }; - - showHiddenPath = (e) => { - this.setState({ anchorHidden: e.currentTarget }); - }; - - performAction = (e) => { - this.handleClose(); - if (e === "refresh") { - this.redresh(); - return; - } - const presentPath = this.props.path.split("/"); - const newTarget = [ - { - id: this.currentID, - type: "dir", - name: presentPath.pop(), - path: presentPath.length === 1 ? "/" : presentPath.join("/"), - }, - ]; - //this.props.navitateUp(); - switch (e) { - case "share": - this.props.setSelectedTarget(newTarget); - this.props.openShareDialog(); - break; - case "newfolder": - this.props.openCreateFolderDialog(); - break; - case "compress": - this.props.setSelectedTarget(newTarget); - this.props.openCompressDialog(); - break; - case "newFile": - this.props.openCreateFileDialog(); - break; - default: - break; - } - }; - - render() { - const { classes, t } = this.props; - const isHomePage = pathHelper.isHomePage(this.props.location.pathname); - const user = Auth.GetUser(); - - const presentFolderMenu = ( - - this.performAction("refresh")}> - - - - {t("fileManager.refresh")} - - {!this.props.search && isHomePage && ( -
- - this.performAction("share")}> - - - - {t("fileManager.share")} - - {user.group.compress && ( - this.performAction("compress")} - > - - - - {t("fileManager.compress")} - - )} - - this.performAction("newfolder")} - > - - - - {t("fileManager.newFolder")} - - this.performAction("newFile")}> - - - - {t("fileManager.newFile")} - -
- )} -
- ); - - return ( -
-
-
- - this.navigateTo(e, -1)} - /> - - - {this.state.hiddenMode && ( - - - - - - - - this.navigateTo( - e, - this.state.folders.length - 1 - ) - } - /> - {presentFolderMenu} - - )} - {!this.state.hiddenMode && - this.state.folders.map((folder, id, folders) => ( - - {folder !== "" && ( - - - this.navigateTo(e, id) - } - /> - {id === folders.length - 1 && - presentFolderMenu} - {id !== folders.length - 1 && ( - - )} - - )} - - ))} -
-
- -
-
- -
- ); - } -} - -NavigatorComponent.propTypes = { - classes: PropTypes.object.isRequired, - path: PropTypes.string.isRequired, -}; - -const Navigator = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(NavigatorComponent)))); - -export default Navigator; diff --git a/src/component/FileManager/Navigator/PathButton.js b/src/component/FileManager/Navigator/PathButton.js deleted file mode 100644 index 9998b47..0000000 --- a/src/component/FileManager/Navigator/PathButton.js +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useEffect } from "react"; -import ExpandMore from "@material-ui/icons/ExpandMore"; -import { Button } from "@material-ui/core"; -import { makeStyles } from "@material-ui/core"; -import { useDrop } from "react-dnd"; -import classNames from "classnames"; -import MoreIcon from "@material-ui/icons/MoreHoriz"; - -const useStyles = makeStyles((theme) => ({ - expandMore: { - color: "#8d8d8d", - }, - active: { - boxShadow: "0 0 0 2px " + theme.palette.primary.light, - }, - button: { - textTransform: "none", - }, -})); - -export default function PathButton(props) { - const inputRef = React.useRef(null); - - const [{ canDrop, isOver }, drop] = useDrop({ - accept: "object", - drop: () => { - if (props.more) { - inputRef.current.click(); - } else { - return { - folder: { - id: -1, - path: props.path, - name: props.folder === "/" ? "" : props.folder, - }, - }; - } - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); - - const isActive = canDrop && isOver; - - useEffect(() => { - if (props.more && isActive) { - inputRef.current.click(); - } - // eslint-disable-next-line - }, [isActive]); - - const classes = useStyles(); - return ( - - - - ); -} diff --git a/src/component/FileManager/Navigator/SubActions.js b/src/component/FileManager/Navigator/SubActions.js deleted file mode 100644 index 5890e91..0000000 --- a/src/component/FileManager/Navigator/SubActions.js +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { IconButton, makeStyles, Menu, MenuItem } from "@material-ui/core"; -import ViewListIcon from "@material-ui/icons/ViewList"; -import ViewSmallIcon from "@material-ui/icons/ViewComfy"; -import ViewModuleIcon from "@material-ui/icons/ViewModule"; -import DownloadIcon from "@material-ui/icons/CloudDownload"; -import Avatar from "@material-ui/core/Avatar"; -import { useDispatch, useSelector } from "react-redux"; -import Auth from "../../../middleware/Auth"; -import { changeViewMethod, setShareUserPopover } from "../../../redux/explorer"; -import { changeSortMethod, startBatchDownload } from "../../../redux/explorer/action"; -import { FormatPageBreak } from "mdi-material-ui"; -import pathHelper from "../../../utils/page"; -import { changePageSize } from "../../../redux/viewUpdate/action"; -import { useTranslation } from "react-i18next"; -import Sort from "../Sort"; - -const useStyles = makeStyles((theme) => ({ - sideButton: { - padding: "8px", - marginRight: "5px", - }, -})); - -const paginationOption = ["50", "100", "200", "500", "1000"]; - -export default function SubActions({ isSmall, inherit }) { - const { t } = useTranslation("application", { keyPrefix: "fileManager" }); - const { t: vasT } = useTranslation("application", { keyPrefix: "vas" }); - const dispatch = useDispatch(); - const viewMethod = useSelector( - (state) => state.viewUpdate.explorerViewMethod - ); - const share = useSelector((state) => state.viewUpdate.shareInfo); - const pageSize = useSelector((state) => state.viewUpdate.pagination.size); - const OpenLoadingDialog = useCallback( - (method) => dispatch(changeViewMethod(method)), - [dispatch] - ); - const ChangeSortMethod = useCallback( - (method) => dispatch(changeSortMethod(method)), - [dispatch] - ); - const SetShareUserPopover = useCallback( - (e) => dispatch(setShareUserPopover(e)), - [dispatch] - ); - const StartBatchDownloadAll = useCallback( - () => dispatch(startBatchDownload(share)), - [dispatch, share] - ); - const ChangePageSize = useCallback((e) => dispatch(changePageSize(e)), [ - dispatch, - ]); - const [anchorPagination, setAnchorPagination] = useState(null); - const showPaginationOptions = (e) => { - setAnchorPagination(e.currentTarget); - }; - - /** change sort */ - const onChangeSort = (value) => { - ChangeSortMethod(value); - }; - const handlePaginationChange = (s) => { - ChangePageSize(s); - setAnchorPagination(null); - }; - - const toggleViewMethod = () => { - const newMethod = - viewMethod === "icon" - ? "list" - : viewMethod === "list" - ? "smallIcon" - : "icon"; - Auth.SetPreference("view_method", newMethod); - OpenLoadingDialog(newMethod); - }; - const isMobile = pathHelper.isMobile(); - - const classes = useStyles(); - return ( - <> - - - - - {viewMethod === "icon" && ( - - - - )} - {viewMethod === "list" && ( - - - - )} - - {viewMethod === "smallIcon" && ( - - - - )} - - {!isMobile && ( - - - - )} - setAnchorPagination(null)} - > - {paginationOption.map((option, index) => ( - handlePaginationChange(parseInt(option))} - > - {t("paginationOption", { option })} - - ))} - handlePaginationChange(-1)} - > - {t("noPagination")} - - - - - {share && ( - SetShareUserPopover(e.currentTarget)} - style={{ padding: 5 }} - > - - - )} - - ); -} diff --git a/src/component/FileManager/NewButton.tsx b/src/component/FileManager/NewButton.tsx new file mode 100644 index 0000000..0b23b18 --- /dev/null +++ b/src/component/FileManager/NewButton.tsx @@ -0,0 +1,36 @@ +import Add from "../Icons/Add.tsx"; +import { Button, IconButton, useMediaQuery, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../redux/hooks.ts"; +import { openNewContextMenu } from "../../redux/thunks/filemanager.ts"; +import { FileManagerIndex } from "./FileManager.tsx"; + +const NewButton = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + if (isMobile) { + return ( + dispatch(openNewContextMenu(FileManagerIndex.main, e))} + > + + + ); + } + + return ( + + ); +}; + +export default NewButton; diff --git a/src/component/FileManager/ObjectIcon.js b/src/component/FileManager/ObjectIcon.js deleted file mode 100644 index c9a1384..0000000 --- a/src/component/FileManager/ObjectIcon.js +++ /dev/null @@ -1,261 +0,0 @@ -import React, { useCallback, useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import statusHelper from "../../utils/page"; -import FileIcon from "./FileIcon"; -import SmallIcon from "./SmallIcon"; -import TableItem from "./TableRow"; -import classNames from "classnames"; -import { makeStyles } from "@material-ui/core"; -import { useDrag } from "react-dnd"; -import { getEmptyImage } from "react-dnd-html5-backend"; -import DropWarpper from "./DnD/DropWarpper"; -import { useLocation } from "react-router-dom"; -import { pathBack } from "../../utils"; -import { - changeContextMenu, - dragAndDrop, - navigateTo, - openLoadingDialog, - openPreview, - selectFile, - setSelectedTarget, - toggleSnackbar, -} from "../../redux/explorer"; -import useDragScrolling from "./DnD/Scrolling"; - -const useStyles = makeStyles(() => ({ - container: { - padding: "7px", - }, - fixFlex: { - minWidth: 0, - }, - dragging: { - opacity: 0.4, - }, -})); - -export default function ObjectIcon(props) { - const path = useSelector((state) => state.navigator.path); - const shareInfo = useSelector((state) => state.viewUpdate.shareInfo); - const selected = useSelector((state) => state.explorer.selected); - const viewMethod = useSelector( - (state) => state.viewUpdate.explorerViewMethod - ); - const navigatorPath = useSelector((state) => state.navigator.path); - const location = useLocation(); - - const dispatch = useDispatch(); - const ContextMenu = useCallback( - (type, open) => dispatch(changeContextMenu(type, open)), - [dispatch] - ); - const SetSelectedTarget = useCallback( - (targets) => dispatch(setSelectedTarget(targets)), - [dispatch] - ); - - const NavitateTo = useCallback((targets) => dispatch(navigateTo(targets)), [ - dispatch, - ]); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const DragAndDrop = useCallback( - (source, target) => dispatch(dragAndDrop(source, target)), - [dispatch] - ); - const OpenLoadingDialog = useCallback( - (text) => dispatch(openLoadingDialog(text)), - [dispatch] - ); - const OpenPreview = useCallback((share) => dispatch(openPreview(share)), [ - dispatch, - ]); - const StartDownload = useCallback( - (share, file) => dispatch(StartDownload(share, file)), - [dispatch] - ); - - const classes = useStyles(); - - const contextMenu = (e) => { - if (props.file.type === "up") { - return; - } - e.preventDefault(); - if ( - selected.findIndex((value) => { - return value === props.file; - }) === -1 - ) { - SetSelectedTarget([props.file]); - } - ContextMenu("file", true); - }; - - const SelectFile = (e) => { - dispatch(selectFile(props.file, e, props.index)); - }; - const enterFolder = () => { - NavitateTo( - path === "/" ? path + props.file.name : path + "/" + props.file.name - ); - }; - const handleClick = (e) => { - if (props.file.type === "up") { - NavitateTo(pathBack(navigatorPath)); - return; - } - - SelectFile(e); - if ( - props.file.type === "dir" && - !e.ctrlKey && - !e.metaKey && - !e.shiftKey - ) { - enterFolder(); - } - }; - - const handleDoubleClick = () => { - if (props.file.type === "up") { - return; - } - if (props.file.type === "dir") { - enterFolder(); - return; - } - - OpenPreview(shareInfo); - }; - - const handleIconClick = (e) => { - e.stopPropagation(); - if (!e.shiftKey) { - e.ctrlKey = true; - } - SelectFile(e); - return false; - }; - - const { - addEventListenerForWindow, - removeEventListenerForWindow, - } = useDragScrolling(); - - const [{ isDragging }, drag, preview] = useDrag({ - item: { - object: props.file, - type: "object", - selected: [...selected], - viewMethod: viewMethod, - }, - begin: () => { - addEventListenerForWindow(); - }, - end: (item, monitor) => { - removeEventListenerForWindow(); - const dropResult = monitor.getDropResult(); - if (item && dropResult) { - if (dropResult.folder) { - if ( - item.object.id !== dropResult.folder.id || - item.object.type !== dropResult.folder.type - ) { - DragAndDrop(item.object, dropResult.folder); - } - } - } - }, - canDrag: () => { - return ( - !statusHelper.isMobile() && - statusHelper.isHomePage(location.pathname) - ); - }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - useEffect(() => { - preview(getEmptyImage(), { captureDraggingState: true }); - // eslint-disable-next-line - }, []); - - if (viewMethod === "list") { - return ( - <> - {props.file.type === "dir" && ( - - )} - {props.file.type !== "dir" && ( - - )} - - ); - } - - return ( -
-
- {props.file.type === "dir" && viewMethod !== "list" && ( - - )} - {props.file.type === "file" && viewMethod === "icon" && ( - - )} - {props.file.type === "file" && viewMethod === "smallIcon" && ( - - )} -
-
- ); -} diff --git a/src/component/FileManager/Pagination.js b/src/component/FileManager/Pagination.js deleted file mode 100644 index b5adcf4..0000000 --- a/src/component/FileManager/Pagination.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import { useDispatch, useSelector } from "react-redux"; -import { Pagination } from "@material-ui/lab"; -import CustomPaginationItem from "./PaginationItem"; -import { setPagination } from "../../redux/viewUpdate/action"; -import AutoHidden from "../Dial/AutoHidden"; -import statusHelper from "../../utils/page"; -import { useLocation } from "react-router-dom"; - -const useStyles = makeStyles((theme) => ({ - root: { - position: "fixed", - bottom: 23, - /* left: 8px; */ - background: theme.palette.background.paper, - borderRadius: 24, - boxShadow: - " 0px 3px 5px -1px rgb(0 0 0 / 20%), 0px 6px 10px 0px rgb(0 0 0 / 14%), 0px 1px 18px 0px rgb(0 0 0 / 12%)", - padding: "8px 4px 8px 4px", - marginLeft: 20, - }, - placeholder: { - marginTop: 80, - }, -})); - -export default function PaginationFooter() { - const classes = useStyles(); - const dispatch = useDispatch(); - const files = useSelector((state) => state.explorer.fileList); - const folders = useSelector((state) => state.explorer.dirList); - const pagination = useSelector((state) => state.viewUpdate.pagination); - const loading = useSelector((state) => state.viewUpdate.navigatorLoading); - const location = useLocation(); - - const SetPagination = useCallback((p) => dispatch(setPagination(p)), [ - dispatch, - ]); - - const handleChange = (event, value) => { - SetPagination({ ...pagination, page: value }); - }; - - const count = useMemo( - () => Math.ceil((files.length + folders.length) / pagination.size), - [files, folders, pagination.size] - ); - - const isMobile = statusHelper.isMobile(); - const isSharePage = statusHelper.isSharePage(location.pathname); - - if (count > 1 && !loading) { - return ( - <> - {!isMobile && !isSharePage && ( -
- )} - -
- ( - - )} - color="secondary" - count={count} - page={pagination.page} - onChange={handleChange} - /> -
-
- - ); - } - return
; -} diff --git a/src/component/FileManager/Pagination/PaginationFooter.tsx b/src/component/FileManager/Pagination/PaginationFooter.tsx new file mode 100644 index 0000000..d0015a8 --- /dev/null +++ b/src/component/FileManager/Pagination/PaginationFooter.tsx @@ -0,0 +1,86 @@ +import { RadiusFrame } from "../../Frame/RadiusFrame.tsx"; +import { + Box, + Pagination, + Slide, + styled, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { forwardRef, useContext } from "react"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { MinPageSize } from "../TopBar/ViewOptionPopover.tsx"; +import { changePage } from "../../../redux/thunks/filemanager.ts"; +import PaginationItem from "./PaginationItem.tsx"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; + +const PaginationFrame = styled(RadiusFrame)(({ theme }) => ({ + padding: theme.spacing(0.5), +})); + +export interface PaginationState { + currentPage: number; + totalPages: number; + usePagination: boolean; + moreItems: boolean; + useEndlessLoading: boolean; + nextToken?: string; +} + +export const usePaginationState = (fmIndex: number) => { + const pagination = useAppSelector( + (state) => state.fileManager[fmIndex].list?.pagination, + ); + const totalItems = pagination?.total_items; + const page = pagination?.page; + const pageSize = pagination?.page_size; + + const currentPage = (page ?? 0) + 1; + const totalPages = Math.ceil( + (totalItems ?? 1) / (pageSize && pageSize > 0 ? pageSize : MinPageSize), + ); + const usePagination = totalPages > 1; + return { + currentPage, + totalPages, + usePagination, + useEndlessLoading: !usePagination, + moreItems: + pagination?.next_token || (usePagination && currentPage < totalPages), + nextToken: pagination?.next_token, + } as PaginationState; +}; + +const PaginationFooter = forwardRef((_props, ref) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const paginationState = usePaginationState(fmIndex); + const onPageChange = (_event: unknown, page: number) => { + dispatch(changePage(fmIndex, page - 1)); + }; + + return ( + + + + } + shape="rounded" + color="primary" + count={paginationState.totalPages} + page={paginationState.currentPage} + onChange={onPageChange} + /> + + + + ); +}); + +export default PaginationFooter; diff --git a/src/component/FileManager/Pagination/PaginationItem.tsx b/src/component/FileManager/Pagination/PaginationItem.tsx new file mode 100644 index 0000000..e57c4d1 --- /dev/null +++ b/src/component/FileManager/Pagination/PaginationItem.tsx @@ -0,0 +1,55 @@ +import { PaginationItem, PaginationItemProps, styled } from "@mui/material"; +import { NoOpDropUri, useFileDrag } from "../Dnd/DndWrappedFile.tsx"; +import { useCallback, useEffect, useRef } from "react"; +import { mergeRefs } from "../../../util"; + +let timeOut: ReturnType | undefined = undefined; + +const StyledPaginationItem = styled(PaginationItem)<{ isDropOver?: boolean }>( + ({ theme, isDropOver }) => ({ + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms !important", + transitionProperty: "background-color,opacity,box-shadow", + boxShadow: isDropOver + ? `inset 0 0 0 2px ${theme.palette.primary.light}` + : "none", + }), +); + +const CustomPaginationItem = (props: PaginationItemProps) => { + const [drag, drop, isOver, isDragging] = useFileDrag({ + dropUri: + props.type !== "start-ellipsis" && props.type !== "end-ellipsis" + ? NoOpDropUri + : undefined, + }); + const buttonRef = useRef(); + + useEffect(() => { + if ( + isOver && + props.onClick && + props.type !== "start-ellipsis" && + props.type !== "end-ellipsis" && + buttonRef.current && + !props.selected + ) { + if (timeOut) { + clearTimeout(timeOut); + } + timeOut = setTimeout(() => buttonRef.current?.click(), 500); + } + }, [isOver]); + + const mergedRef = useCallback( + (val: any) => { + mergeRefs(drop, buttonRef)(val); + }, + [drop, buttonRef], + ); + + return ( + + ); +}; + +export default CustomPaginationItem; diff --git a/src/component/FileManager/PaginationItem.js b/src/component/FileManager/PaginationItem.js deleted file mode 100644 index 9b76a9a..0000000 --- a/src/component/FileManager/PaginationItem.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { useDrop } from "react-dnd"; -import { PaginationItem } from "@material-ui/lab"; - -export default function CustomPaginationItem(props) { - const inputRef = useRef(null); - - const [{ canDrop, isOver }, drop] = useDrop({ - accept: "object", - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); - - const isActive = canDrop && isOver; - - useEffect(() => { - if ( - isActive && - props.onClick && - props.type !== "start-ellipsis" && - props.type !== "end-ellipsis" - ) { - console.log("ss"); - props.onClick(); - } - }, [isActive, inputRef]); - - if ( - props.isMobile && - (props.type === "start-ellipsis" || - props.type === "end-ellipsis" || - props.type === "page") - ) { - if (props.selected) { - return ( -
- {props.page} / {props.count} -
- ); - } - return <>; - } - return ( -
- -
- ); -} diff --git a/src/component/FileManager/PathSelector.js b/src/component/FileManager/PathSelector.js deleted file mode 100644 index 595681d..0000000 --- a/src/component/FileManager/PathSelector.js +++ /dev/null @@ -1,266 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import FolderIcon from "@material-ui/icons/Folder"; -import RightIcon from "@material-ui/icons/KeyboardArrowRight"; -import UpIcon from "@material-ui/icons/ArrowUpward"; -import { connect } from "react-redux"; -import classNames from "classnames"; - -import { - IconButton, - ListItemIcon, - ListItemSecondaryAction, - ListItemText, - MenuItem, - MenuList, - withStyles, -} from "@material-ui/core"; -import Sort, { sortMethodFuncs } from './Sort'; -import API from "../../middleware/Api"; -import { toggleSnackbar } from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - search: state.explorer.search, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -const styles = (theme) => ({ - iconWhite: { - color: theme.palette.common.white, - }, - selected: { - backgroundColor: theme.palette.primary.main + "!important", - "& $primary, & $icon": { - color: theme.palette.common.white, - }, - }, - primary: {}, - icon: {}, - buttonIcon: {}, - selector: { - minWidth: "300px", - }, - container: { - maxHeight: "330px", - overflowY: " auto", - }, - sortWrapper: { - textAlign: "right", - paddingRight: "30px", - }, - sortButton: { - padding: "0", - }, -}); -class PathSelectorCompoment extends Component { - state = { - presentPath: "/", - sortBy: '', - dirList: [], - selectedTarget: null, - }; - /** - * the source dir list from api `/directory` - * - * `state.dirList` is a sorted copy of it - */ - sourceDirList = [] - - componentDidMount = () => { - const toBeLoad = this.props.presentPath; - this.enterFolder(!this.props.search ? toBeLoad : "/"); - }; - - back = () => { - const paths = this.state.presentPath.split("/"); - paths.pop(); - const toBeLoad = paths.join("/"); - this.enterFolder(toBeLoad === "" ? "/" : toBeLoad); - }; - - enterFolder = (toBeLoad) => { - API.get( - (this.props.api ? this.props.api : "/directory") + - encodeURIComponent(toBeLoad) - ) - .then((response) => { - const dirList = response.data.objects.filter((x) => { - return ( - x.type === "dir" && - this.props.selected.findIndex((value) => { - return ( - value.name === x.name && value.path === x.path - ); - }) === -1 - ); - }); - dirList.forEach((value) => { - value.displayName = value.name; - }); - this.sourceDirList = dirList - this.setState({ - presentPath: toBeLoad, - selectedTarget: null, - }, this.updateDirList); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "warning" - ); - }); - }; - - handleSelect = (index) => { - this.setState({ selectedTarget: index }); - this.props.onSelect(this.state.dirList[index]); - }; - - - /** - * change sort type - * @param {Event} event - */ - onChangeSort = (sortBy) => { - this.setState({ sortBy }, this.updateDirList) - }; - - /** - * sort dir list, and handle parent dirs - */ - updateDirList = () => { - const { state, sourceDirList } = this - const { sortBy, presentPath } = state - - // copy - const dirList = [...sourceDirList] - // sort - const sortMethod = sortMethodFuncs[sortBy] - if (sortMethod) dirList.sort(sortMethod) - - // add root/parent dirs to top - if (presentPath === "/") { - dirList.unshift({ name: "/", path: "", displayName: "/" }); - } else { - let path = presentPath; - let name = presentPath; - const displayNames = ["fileManager.currentFolder", "fileManager.backToParentFolder"]; - for (let i = 0; i < 2; i++) { - const paths = path.split("/"); - name = paths.pop(); - name = name === "" ? "/" : name; - path = paths.join("/"); - dirList.unshift({ - name: name, - path: path, - displayName: this.props.t( - displayNames[i] - ), - }); - } - } - this.setState({ dirList }) - } - render() { - const { classes, t } = this.props; - - const showActionIcon = (index) => { - if (this.state.presentPath === "/") { - return index !== 0; - } - return index !== 1; - }; - - const actionIcon = (index) => { - if (this.state.presentPath === "/") { - return ; - } - - if (index === 0) { - return ; - } - return ; - }; - - return ( -
-
- -
- - {this.state.dirList.map((value, index) => ( - this.handleSelect(index)} - > - - - - - {showActionIcon(index) && ( - - - index === 0 - ? this.back() - : this.enterFolder( - value.path === "/" - ? value.path + - value.name - : value.path + - "/" + - value.name - ) - } - > - {actionIcon(index)} - - - )} - - ))} - -
- ); - } -} - -PathSelectorCompoment.propTypes = { - classes: PropTypes.object.isRequired, - presentPath: PropTypes.string.isRequired, - selected: PropTypes.array.isRequired, -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withTranslation()(PathSelectorCompoment))); diff --git a/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx new file mode 100644 index 0000000..cb806b1 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx @@ -0,0 +1,219 @@ +import { useTranslation } from "react-i18next"; + +import { SecondaryButton } from "../../../Common/StyledComponents.tsx"; +import Add from "../../../Icons/Add.tsx"; +import { + bindMenu, + bindTrigger, + usePopupState, +} from "material-ui-popup-state/hooks"; +import { ListItemIcon, ListItemText, Menu } from "@mui/material"; +import { Condition, ConditionType } from "./ConditionBox.tsx"; +import React from "react"; +import TextCaseTitle from "../../../Icons/TextCaseTitle.tsx"; +import FolderOutlined from "../../../Icons/FolderOutlined.tsx"; +import { SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; +import { FileType, Metadata } from "../../../../api/explorer.ts"; +import Numbers from "../../../Icons/Numbers.tsx"; +import Tag from "../../../Icons/Tag.tsx"; +import { CascadingSubmenu } from "../../ContextMenu/CascadingMenu.tsx"; +import Info from "../../../Icons/Info.tsx"; +import HardDriveOutlined from "../../../Icons/HardDriveOutlined.tsx"; +import dayjs from "dayjs"; +import CalendarClock from "../../../Icons/CalendarClock.tsx"; + +export interface AddConditionProps { + onConditionAdd: (condition: Condition) => void; +} + +interface ConditionOption { + name: string; + icon?: JSX.Element; + condition: Condition; +} + +const options: ConditionOption[] = [ + { + name: "application:modals.fileName", + icon: , + condition: { type: ConditionType.name, case_folding: true }, + }, + { + name: "application:navbar.fileType", + icon: , + condition: { type: ConditionType.type, file_type: FileType.file }, + }, + { + name: "application:fileManager.tags", + icon: , + condition: { type: ConditionType.tag }, + }, + { + name: "application:fileManager.metadata", + icon: , + condition: { type: ConditionType.metadata }, + }, + { + name: "application:navbar.fileSize", + icon: , + condition: { type: ConditionType.size, size_lte: 0, size_gte: 0 }, + }, + { + name: "application:fileManager.createDate", + icon: , + condition: { + type: ConditionType.created, + created_gte: dayjs().subtract(7, "d").unix(), + created_lte: dayjs().unix(), + }, + }, + { + name: "application:fileManager.updatedDate", + icon: , + condition: { + type: ConditionType.modified, + updated_gte: dayjs().subtract(7, "d").unix(), + updated_lte: dayjs().unix(), + }, + }, +]; + +const mediaMetaOptions: ConditionOption[] = [ + { + name: "application:fileManager.title", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.music_title, + id: Metadata.music_title, + }, + }, + { + name: "application:fileManager.artist", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.music_artist, + id: Metadata.music_artist, + }, + }, + { + name: "application:fileManager.album", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.music_album, + id: Metadata.music_album, + }, + }, + { + name: "application:fileManager.cameraMake", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.camera_make, + id: Metadata.camera_make, + }, + }, + { + name: "application:fileManager.cameraModel", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.camera_model, + id: Metadata.camera_model, + }, + }, + { + name: "application:fileManager.lensMake", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.lens_make, + id: Metadata.lens_make, + }, + }, + { + name: "application:fileManager.lensModel", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.lens_model, + id: Metadata.lens_model, + }, + }, +]; + +const AddCondition = (props: AddConditionProps) => { + const { t } = useTranslation(); + const conditionPopupState = usePopupState({ + variant: "popover", + popupId: "conditions", + }); + const { onClose, ...menuProps } = bindMenu(conditionPopupState); + const onConditionAdd = (condition: Condition) => { + props.onConditionAdd({ + ...condition, + id: + condition.type == ConditionType.metadata && !condition.id + ? Math.random().toString() + : condition.id, + }); + onClose(); + }; + return ( + <> + } + sx={{ px: "15px" }} + > + {t("navbar.addCondition")} + + + {options.map((option, index) => ( + onConditionAdd(option.condition)} + > + {option.icon} + {t(option.name)} + + ))} + } + popupId={"mediaInfo"} + title={t("application:fileManager.mediaInfo")} + > + {mediaMetaOptions.map((option, index) => ( + onConditionAdd(option.condition)} + > + + {t(option.name)} + + + ))} + + + + ); +}; + +export default AddCondition; diff --git a/src/component/FileManager/Search/AdvanceSearch/AdvanceSearch.tsx b/src/component/FileManager/Search/AdvanceSearch/AdvanceSearch.tsx new file mode 100644 index 0000000..3e91d8b --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/AdvanceSearch.tsx @@ -0,0 +1,217 @@ +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import { useCallback, useEffect, useState } from "react"; +import { closeAdvanceSearch } from "../../../../redux/globalStateSlice.ts"; +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { Collapse, DialogContent } from "@mui/material"; +import ConditionBox, { Condition, ConditionType } from "./ConditionBox.tsx"; +import { SearchParam } from "../../../../util/uri.ts"; +import { TransitionGroup } from "react-transition-group"; +import AddCondition from "./AddCondition.tsx"; +import { useSnackbar } from "notistack"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { FileManagerIndex } from "../../FileManager.tsx"; +import { defaultPath } from "../../../../hooks/useNavigation.tsx"; +import { advancedSearch } from "../../../../redux/thunks/filemanager.ts"; +import { Metadata } from "../../../../api/explorer.ts"; + +const searchParamToConditions = ( + search_params: SearchParam, + base: string, +): Condition[] => { + const applied: Condition[] = [ + { + type: ConditionType.base, + base_uri: base, + }, + ]; + if (search_params.name) { + applied.push({ + type: ConditionType.name, + names: search_params.name, + name_op_or: search_params.name_op_or, + case_folding: search_params.case_folding, + }); + } + + if (search_params.type != undefined) { + applied.push({ + type: ConditionType.type, + file_type: search_params.type, + }); + } + + if ( + search_params.size_gte != undefined || + search_params.size_lte != undefined + ) { + applied.push({ + type: ConditionType.size, + size_gte: search_params.size_gte, + size_lte: search_params.size_lte, + }); + } + + if ( + search_params.created_at_gte != undefined || + search_params.created_at_lte != undefined + ) { + applied.push({ + type: ConditionType.created, + created_gte: search_params.created_at_gte, + created_lte: search_params.created_at_lte, + }); + } + + if ( + search_params.updated_at_gte != undefined || + search_params.updated_at_lte != undefined + ) { + applied.push({ + type: ConditionType.modified, + updated_gte: search_params.updated_at_gte, + updated_lte: search_params.updated_at_lte, + }); + } + + const tags: string[] = []; + if (search_params.metadata) { + Object.entries(search_params.metadata).forEach(([key, value]) => { + if (key.startsWith(Metadata.tag_prefix)) { + tags.push(key.slice(Metadata.tag_prefix.length)); + } else { + applied.push({ + type: ConditionType.metadata, + metadata_key: key, + metadata_value: value, + }); + } + }); + } + + if (tags.length > 0) { + applied.push({ + type: ConditionType.tag, + tags: tags, + }); + } + + return applied; +}; + +const AdvanceSearch = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const [conditions, setConditions] = useState([]); + const open = useAppSelector((state) => state.globalState.advanceSearchOpen); + const base = useAppSelector( + (state) => state.globalState.advanceSearchBasePath, + ); + const initialNames = useAppSelector( + (state) => state.globalState.advanceSearchInitialNameCondition, + ); + const search_params = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main].search_params, + ); + const current_base = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main].pure_path, + ); + + const onClose = useCallback(() => { + dispatch(closeAdvanceSearch()); + }, [dispatch]); + + useEffect(() => { + if (open) { + if (initialNames && base) { + setConditions([ + { + type: ConditionType.base, + base_uri: base, + }, + { + type: ConditionType.name, + names: initialNames, + case_folding: true, + }, + ]); + return; + } + + if (search_params) { + const existedConditions = searchParamToConditions( + search_params, + current_base ?? defaultPath, + ); + if (existedConditions.length > 0) { + setConditions(existedConditions); + } + } + } + }, [open]); + + const onConditionRemove = (condition: Condition) => { + setConditions(conditions.filter((c) => c !== condition)); + }; + + const onConditionAdd = (condition: Condition) => { + if ( + conditions.find((c) => c.type === condition.type && c.id === condition.id) + ) { + enqueueSnackbar(t("application:navbar.conditionDuplicate"), { + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + setConditions([...conditions, condition]); + }; + + const submitSearch = useCallback(() => { + dispatch(advancedSearch(FileManagerIndex.main, conditions)); + }, [dispatch, conditions]); + + return ( + } + dialogProps={{ + open: open ?? false, + onClose: onClose, + fullWidth: true, + maxWidth: "xs", + }} + > + + + {conditions.map((condition, index) => ( + + 2 && condition.type != ConditionType.base + ? onConditionRemove + : undefined + } + condition={condition} + onChange={(condition) => { + const new_conditions = [...conditions]; + new_conditions[index] = condition; + setConditions(new_conditions); + }} + /> + + ))} + + + + ); +}; + +export default AdvanceSearch; diff --git a/src/component/FileManager/Search/AdvanceSearch/ConditionBox.tsx b/src/component/FileManager/Search/AdvanceSearch/ConditionBox.tsx new file mode 100644 index 0000000..e8a1d0e --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/ConditionBox.tsx @@ -0,0 +1,189 @@ +import { Box, Grow, IconButton, Typography } from "@mui/material"; +import { forwardRef, useCallback, useMemo, useState } from "react"; +import TextCaseTitle from "../../../Icons/TextCaseTitle.tsx"; +import { useTranslation } from "react-i18next"; +import Dismiss from "../../../Icons/Dismiss.tsx"; +import FolderOutlined from "../../../Icons/FolderOutlined.tsx"; +import Search from "../../../Icons/Search.tsx"; +import { SearchBaseCondition } from "./SearchBaseCondition.tsx"; +import { FileTypeCondition } from "./FileTypeCondition.tsx"; +import { FileNameCondition, StyledBox } from "./FileNameCondition.tsx"; +import Tag from "../../../Icons/Tag.tsx"; +import { TagCondition } from "./TagCondition.tsx"; +import Numbers from "../../../Icons/Numbers.tsx"; +import { MetadataCondition } from "./MetadataCondition.tsx"; +import HardDriveOutlined from "../../../Icons/HardDriveOutlined.tsx"; +import { SizeCondition } from "./SizeCondition.tsx"; +import CalendarClock from "../../../Icons/CalendarClock.tsx"; +import { DateTimeCondition } from "./DateTimeCondition.tsx"; + +export interface Condition { + type: ConditionType; + case_folding?: boolean; + names?: string[]; + name_op_or?: boolean; + file_type?: number; + size_gte?: number; + size_lte?: number; + time?: number; + metadata_key?: string; + metadata_value?: string; + base_uri?: string; + tags?: string[]; + id?: string; + metadata_key_readonly?: boolean; + created_gte?: number; + created_lte?: number; + updated_gte?: number; + updated_lte?: number; +} + +export enum ConditionType { + name, + size, + created, + modified, + type, + metadata, + base, + tag, +} + +export interface ConditionProps { + condition: Condition; + onChange: (condition: Condition) => void; + onRemove?: (condition: Condition) => void; + index: number; +} + +const ConditionBox = forwardRef((props: ConditionProps, ref) => { + const { condition, index, onRemove, onChange } = props; + const { t } = useTranslation(); + const [hovered, setHovered] = useState(false); + + const Icon = useMemo(() => { + switch (condition.type) { + case ConditionType.base: + return Search; + case ConditionType.type: + return FolderOutlined; + case ConditionType.tag: + return Tag; + case ConditionType.metadata: + return Numbers; + case ConditionType.size: + return HardDriveOutlined; + case ConditionType.modified: + case ConditionType.created: + return CalendarClock; + + default: + return TextCaseTitle; + } + }, [condition.type]); + + const title = useMemo(() => { + switch (condition.type) { + case ConditionType.base: + return t("application:navbar.searchBase"); + case ConditionType.name: + return t("application:modals.fileName"); + case ConditionType.type: + return t("application:navbar.fileType"); + case ConditionType.tag: + return t("application:fileManager.tags"); + case ConditionType.metadata: + return t("application:fileManager.metadata"); + case ConditionType.size: + return t("application:navbar.fileSize"); + case ConditionType.modified: + return t("application:fileManager.updatedDate"); + case ConditionType.created: + return t("application:fileManager.createDate"); + default: + return "Unknown"; + } + }, [t, condition]); + + const onNameConditionAdded = useCallback( + (_e: any, newValue: string[]) => { + onChange({ + ...condition, + names: newValue, + }); + }, + [onChange], + ); + + return ( + 0 ? 1 : 0, + }} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + + {title} + + onRemove(condition) : undefined} + > + + + + + + {condition.type == ConditionType.name && ( + + )} + {condition.type == ConditionType.type && ( + + )} + {condition.type == ConditionType.base && ( + + )} + {condition.type == ConditionType.tag && ( + + )} + {condition.type == ConditionType.metadata && ( + + )} + {condition.type == ConditionType.size && ( + + )} + {condition.type == ConditionType.created && ( + + )} + {condition.type == ConditionType.modified && ( + + )} + + + ); +}); + +export default ConditionBox; diff --git a/src/component/FileManager/Search/AdvanceSearch/DateTimeCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/DateTimeCondition.tsx new file mode 100644 index 0000000..6aad624 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/DateTimeCondition.tsx @@ -0,0 +1,54 @@ +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Condition } from "./ConditionBox.tsx"; +import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs from "dayjs"; + +export const DateTimeCondition = ({ + condition, + onChange, + field, +}: { + onChange: (condition: Condition) => void; + condition: Condition; + field: string; +}) => { + const { t } = useTranslation(); + return ( + + + + onChange({ + ...condition, + [field + "_gte"]: newValue ? newValue.unix() : 0, + }) + } + /> + + onChange({ + ...condition, + [field + "_lte"]: newValue ? newValue.unix() : 0, + }) + } + /> + + + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/FileNameCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/FileNameCondition.tsx new file mode 100644 index 0000000..5a5e0b9 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/FileNameCondition.tsx @@ -0,0 +1,117 @@ +import { + Autocomplete, + Box, + Chip, + FormControlLabel, + styled, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { + FilledTextField, + StyledCheckbox, +} from "../../../Common/StyledComponents.tsx"; +import { Condition } from "./ConditionBox.tsx"; + +export const StyledBox = styled(Box)(({ theme }) => ({ + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + paddingBottom: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, +})); +export const FileNameCondition = ({ + condition, + onChange, + onNameConditionAdded, +}: { + onChange: (condition: Condition) => void; + condition: Condition; + onNameConditionAdded: (_e: any, newValue: string[]) => void; +}) => { + const { t } = useTranslation(); + return ( + <> + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderInput={(params) => ( + + )} + /> + + { + onChange({ + ...condition, + case_folding: e.target.checked, + }); + }} + disableRipple + checked={condition.case_folding} + size="small" + /> + } + label={t("application:navbar.caseFolding")} + /> + { + onChange({ + ...condition, + name_op_or: !e.target.checked, + }); + }} + disableRipple + checked={!condition.name_op_or} + size="small" + /> + } + label={t("application:navbar.notNameOpOr")} + /> + + + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/FileTypeCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/FileTypeCondition.tsx new file mode 100644 index 0000000..332c5a1 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/FileTypeCondition.tsx @@ -0,0 +1,53 @@ +import { FormControl, ListItemIcon, ListItemText } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; +import { FileType } from "../../../../api/explorer.ts"; +import Document from "../../../Icons/Document.tsx"; +import Folder from "../../../Icons/Folder.tsx"; +import { Condition } from "./ConditionBox.tsx"; +import { DenseSelect } from "../../../Common/StyledComponents.tsx"; + +export const FileTypeCondition = ({ + condition, + onChange, +}: { + onChange: (condition: Condition) => void; + condition: Condition; +}) => { + const { t } = useTranslation(); + return ( + + + onChange({ + ...condition, + file_type: e.target.value as number, + }) + } + > + + + + + + {t("application:fileManager.file")} + + + + + + + + {t("application:fileManager.folder")} + + + + + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/MetadataCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/MetadataCondition.tsx new file mode 100644 index 0000000..1b51566 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/MetadataCondition.tsx @@ -0,0 +1,44 @@ +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Condition } from "./ConditionBox.tsx"; +import { FilledTextField } from "../../../Common/StyledComponents.tsx"; + +export const MetadataCondition = ({ + condition, + onChange, +}: { + onChange: (condition: Condition) => void; + condition: Condition; +}) => { + const { t } = useTranslation(); + return ( + + + onChange({ ...condition, metadata_key: e.target.value }) + } + disabled={condition.metadata_key_readonly} + type="text" + fullWidth + /> + + onChange({ ...condition, metadata_value: e.target.value }) + } + type="text" + fullWidth + /> + + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/SearchBaseCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/SearchBaseCondition.tsx new file mode 100644 index 0000000..2a7a1e2 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/SearchBaseCondition.tsx @@ -0,0 +1,27 @@ +import { PathSelectorForm } from "../../../Common/Form/PathSelectorForm.tsx"; +import { defaultPath } from "../../../../hooks/useNavigation.tsx"; +import { Condition } from "./ConditionBox.tsx"; + +export const SearchBaseCondition = ({ + condition, + onChange, +}: { + onChange: (condition: Condition) => void; + condition: Condition; +}) => { + return ( + onChange({ ...condition, base_uri: path })} + path={condition.base_uri ?? defaultPath} + variant={"searchIn"} + textFieldProps={{ + sx: { + "& .MuiOutlinedInput-input": { + paddingTop: "15.5px", + paddingBottom: "15.5px", + }, + }, + }} + /> + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/SizeCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/SizeCondition.tsx new file mode 100644 index 0000000..ecdaa42 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/SizeCondition.tsx @@ -0,0 +1,39 @@ +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Condition } from "./ConditionBox.tsx"; +import SizeInput from "../../../Common/SizeInput.tsx"; + +export const SizeCondition = ({ + condition, + onChange, +}: { + onChange: (condition: Condition) => void; + condition: Condition; +}) => { + const { t } = useTranslation(); + return ( + + onChange({ ...condition, size_gte: e })} + inputProps={{ + fullWidth: true, + }} + /> + onChange({ ...condition, size_lte: e })} + inputProps={{ + fullWidth: true, + }} + /> + + ); +}; diff --git a/src/component/FileManager/Search/AdvanceSearch/TagCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/TagCondition.tsx new file mode 100644 index 0000000..656cc35 --- /dev/null +++ b/src/component/FileManager/Search/AdvanceSearch/TagCondition.tsx @@ -0,0 +1,50 @@ +import { Autocomplete, Chip } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { FilledTextField } from "../../../Common/StyledComponents.tsx"; +import { Condition } from "./ConditionBox.tsx"; + +export const TagCondition = ({ + condition, + onChange, +}: { + onChange: (condition: Condition) => void; + condition: Condition; +}) => { + const { t } = useTranslation(); + return ( + <> + onChange({ ...condition, tags: value })} + renderTags={(value: readonly string[], getTagProps) => + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderInput={(params) => ( + + )} + /> + + ); +}; diff --git a/src/component/FileManager/Search/FullSearchOptions.tsx b/src/component/FileManager/Search/FullSearchOptions.tsx new file mode 100644 index 0000000..49b9fdf --- /dev/null +++ b/src/component/FileManager/Search/FullSearchOptions.tsx @@ -0,0 +1,86 @@ +import { + Box, + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { Trans, useTranslation } from "react-i18next"; +import React, { useCallback } from "react"; +// @ts-ignore +import Highlighter from "react-highlight-words"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { SearchOutlined } from "@mui/icons-material"; +import { FileType } from "../../../api/explorer.ts"; +import FileBadge from "../FileBadge.tsx"; +import { quickSearch } from "../../../redux/thunks/filemanager.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; + +export interface FullSearchOptionProps { + options: string[]; + keyword: string; +} + +const FullSearchOption = ({ options, keyword }: FullSearchOptionProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onClick = useCallback( + (base: string) => () => + dispatch(quickSearch(FileManagerIndex.main, base, keyword)), + [keyword, dispatch], + ); + + return ( + + {options.map((option) => ( + + + + theme.palette.action.active, + width: 24, + height: 24, + mt: "7px", + ml: "5px", + }} + /> + + , + ]} + /> + } + slotProps={{ + primary: { + variant: "body2", + } + }} + /> + + + + ))} + + ); +}; + +export default FullSearchOption; diff --git a/src/component/FileManager/Search/FuzzySearchResult.tsx b/src/component/FileManager/Search/FuzzySearchResult.tsx new file mode 100644 index 0000000..0d8644d --- /dev/null +++ b/src/component/FileManager/Search/FuzzySearchResult.tsx @@ -0,0 +1,112 @@ +import { FileResponse, FileType, Metadata } from "../../../api/explorer.ts"; +import { + List, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { sizeToString } from "../../../util"; +import FileIcon from "../Explorer/FileIcon.tsx"; +import React, { useCallback } from "react"; +import FileBadge from "../FileBadge.tsx"; +import CrUri from "../../../util/uri.ts"; +// @ts-ignore +import Highlighter from "react-highlight-words"; + +import { openFileContextMenu } from "../../../redux/thunks/file.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { ContextMenuTypes } from "../../../redux/fileManagerSlice.ts"; + +export interface FuzzySearchResultProps { + files: FileResponse[]; + keyword: string; +} + +const FuzzySearchResult = ({ files, keyword }: FuzzySearchResultProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const getFileTypeText = useCallback( + (file: FileResponse) => { + if (file.metadata?.[Metadata.share_redirect]) { + return t("fileManager.symbolicFile"); + } + + if (file.type == FileType.folder) { + return t("application:fileManager.folder"); + } + return sizeToString(file.size); + }, + [t], + ); + + return ( + + {files.map((file) => ( + + + dispatch( + openFileContextMenu( + FileManagerIndex.main, + file, + true, + e, + ContextMenuTypes.searchResult, + ), + ) + } + > + + + + + } + secondary={getFileTypeText(file)} + slotProps={{ + primary: { + variant: "body2", + }, + + secondary: { + variant: "body2", + } + }} /> + + + + ))} + + ); +}; + +export default FuzzySearchResult; diff --git a/src/component/FileManager/Search/SearchIndicator.tsx b/src/component/FileManager/Search/SearchIndicator.tsx new file mode 100644 index 0000000..a1e86da --- /dev/null +++ b/src/component/FileManager/Search/SearchIndicator.tsx @@ -0,0 +1,102 @@ +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useContext, useMemo } from "react"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { + alpha, + Button, + ButtonGroup, + Grow, + styled, + useMediaQuery, + useTheme, +} from "@mui/material"; +import Search from "../../Icons/Search.tsx"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import { + clearSearch, + openAdvancedSearch, +} from "../../../redux/thunks/filemanager.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; + +export const StyledButtonGroup = styled(ButtonGroup)(({ theme }) => ({ + "& .MuiButtonGroup-firstButton, .MuiButtonGroup-lastButton": { + "&:hover": { + border: "none", + }, + }, +})); +export const StyledButton = styled(Button)(({ theme }) => ({ + border: "none", + backgroundColor: alpha(theme.palette.primary.main, 0.1), + "&:hover": { + backgroundColor: alpha(theme.palette.primary.main, 0.2), + }, + fontSize: theme.typography.caption.fontSize, + minWidth: 0, + "& .MuiButton-startIcon": {}, +})); + +export const SearchIndicator = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + + const search_params = useAppSelector( + (state) => state.fileManager[fmIndex].search_params, + ); + + const searchConditionsCount = useMemo(() => { + if (!search_params) { + return 0; + } + + let count = 0; + if (search_params.name) { + count++; + } + if (search_params.metadata) { + count += Object.keys(search_params.metadata).length; + } + if (search_params.type != undefined) { + count++; + } + if (search_params.size_gte || search_params.size_lte) { + count++; + } + if (search_params.created_at_gte || search_params.created_at_lte) { + count++; + } + if (search_params.updated_at_gte || search_params.updated_at_lte) { + count++; + } + return count; + }, [search_params]); + + return ( + 0}> + + } + onClick={() => dispatch(openAdvancedSearch(fmIndex))} + > + {isMobile + ? searchConditionsCount + : t("fileManager.searchConditions", { + num: searchConditionsCount, + })} + + dispatch(clearSearch(fmIndex))} + > + + + + + ); +}; diff --git a/src/component/FileManager/Search/SearchPopup.tsx b/src/component/FileManager/Search/SearchPopup.tsx new file mode 100644 index 0000000..94ef060 --- /dev/null +++ b/src/component/FileManager/Search/SearchPopup.tsx @@ -0,0 +1,279 @@ +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { setSearchPopup } from "../../../redux/globalStateSlice.ts"; +import { + Box, + debounce, + Dialog, + Divider, + Grow, + IconButton, + styled, + Tooltip, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { forwardRef, useCallback, useEffect, useMemo, useState } from "react"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import { SearchOutlined } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FileResponse } from "../../../api/explorer.ts"; +import Fuse from "fuse.js"; +import AutoHeight from "../../Common/AutoHeight.tsx"; +import FuzzySearchResult from "./FuzzySearchResult.tsx"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import SessionManager from "../../../session"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import FullSearchOption from "./FullSearchOptions.tsx"; +import { TransitionProps } from "@mui/material/transitions"; +import { + openAdvancedSearch, + quickSearch, +} from "../../../redux/thunks/filemanager.ts"; +import Options from "../../Icons/Options.tsx"; + +const StyledDialog = styled(Dialog)<{ + expanded?: boolean; +}>(({ theme, expanded }) => ({ + "& .MuiDialog-container": { + alignItems: "flex-start", + height: expanded ? "100%" : "initial", + }, + zIndex: theme.zIndex.modal - 1, +})); + +const StyledOutlinedIconTextFiled = styled(OutlineIconTextField)(() => ({ + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, +})); + +export const GrowDialogTransition = forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); + +const SearchPopup = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + const [keywords, setKeywords] = useState(""); + const [searchedKeyword, setSearchedKeyword] = useState(""); + const [treeSearchResults, setTreeSearchResults] = useState( + [], + ); + + const onClose = () => { + dispatch(setSearchPopup(false)); + setKeywords(""); + setSearchedKeyword(""); + }; + + const open = useAppSelector((state) => state.globalState.searchPopupOpen); + const tree = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main]?.tree, + ); + const path = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main]?.path, + ); + const single_file_view = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main]?.list?.single_file_view, + ); + + const searchTree = useMemo( + () => + debounce( + ( + request: { input: string }, + callback: (results?: FileResponse[]) => void, + ) => { + const options = { + includeScore: true, + // Search in `author` and in `tags` array + keys: ["file.name"], + }; + const fuse = new Fuse(Object.values(tree), options); + const result = fuse.search( + request.input + .split(" ") + .filter((k) => k != "") + .join(" "), + { limit: 50 }, + ); + const res: FileResponse[] = []; + result + .filter((r) => r.item.file != undefined) + .forEach((r) => { + if (r.item.file) { + res.push(r.item.file); + } + }); + callback(res); + }, + 400, + ), + [tree], + ); + + useEffect(() => { + let active = true; + + if (keywords === "" || keywords.length < 2) { + setTreeSearchResults([]); + setSearchedKeyword(""); + return undefined; + } + + searchTree({ input: keywords }, (results?: FileResponse[]) => { + if (active) { + setTreeSearchResults(results ?? []); + setSearchedKeyword(keywords); + } + }); + return () => { + active = false; + }; + }, [keywords, setSearchedKeyword, searchTree]); + + const fullSearchOptions = useMemo(() => { + if (!open || !keywords) { + return []; + } + + const res: string[] = []; + const current = new CrUri(path ?? defaultPath); + // current folder - not currently in root + if (!current.is_root()) { + res.push(current.toString()); + } + // current root - not in single file view + if (!single_file_view) { + res.push(current.base()); + } + // my files - user login and not my fs + if ( + SessionManager.currentLoginOrNull() && + !(current.fs() == Filesystem.my) + ) { + res.push(defaultPath); + } + return res; + }, [open, path, single_file_view, keywords]); + + const onEnter = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + e.preventDefault(); + if (fullSearchOptions.length > 0) { + dispatch( + quickSearch(FileManagerIndex.main, fullSearchOptions[0], keywords), + ); + } + } + }, + [fullSearchOptions, keywords], + ); + + return ( + + + } + variant="outlined" + autoFocus + onKeyDown={onEnter} + value={keywords} + onChange={(e) => setKeywords(e.target.value)} + placeholder={t("navbar.searchFiles")} + fullWidth + /> + + + dispatch(openAdvancedSearch(FileManagerIndex.main, keywords)) + } + > + + + + + + {keywords && } + + + {fullSearchOptions.length > 0 && ( + <> + + {t("navbar.searchFilesTitle")} + + + {treeSearchResults.length > 0 && } + + )} + {treeSearchResults.length > 0 && ( + <> + + {t("navbar.recentlyViewed")} + + + + )} + + + + ); +}; + +export default SearchPopup; diff --git a/src/component/FileManager/Sidebar/BasicInfo.tsx b/src/component/FileManager/Sidebar/BasicInfo.tsx new file mode 100644 index 0000000..1a19cfc --- /dev/null +++ b/src/component/FileManager/Sidebar/BasicInfo.tsx @@ -0,0 +1,222 @@ +import { FileResponse, FileType, FolderSummary, Metadata } from "../../../api/explorer.ts"; +import { useTranslation } from "react-i18next"; +import { Link, Skeleton, Typography } from "@mui/material"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import InfoRow from "./InfoRow.tsx"; +import { sizeToString } from "../../../util"; +import FileBadge from "../FileBadge.tsx"; +import CrUri from "../../../util/uri.ts"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { getFileInfo } from "../../../api/api.ts"; +import dayjs from "dayjs"; + +export interface BasicInfoProps { + target: FileResponse; +} + +const BasicInfo = ({ target }: BasicInfoProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + // null: not valid, undefined: not loaded, FolderSummary: loaded + const [folderSummary, setFolderSummary] = useState(null); + useEffect(() => { + setFolderSummary(null); + }, [target]); + + const isSymbolicLink = useMemo(() => { + return !!(target.metadata && target.metadata[Metadata.share_redirect]); + }, [target.metadata]); + const fileType = useMemo(() => { + let srcType = ""; + switch (target.type) { + case FileType.file: + srcType = t("fileManager.file"); + break; + case FileType.folder: + srcType = t("fileManager.folder"); + break; + default: + srcType = t("fileManager.file"); + } + + if (isSymbolicLink) { + return t("fileManager.symbolicLink", { srcType }); + } + + return srcType; + }, [target, isSymbolicLink, t]); + + const displaySize = useCallback( + (size: number): string => sizeToString(size) + t("fileManager.bytes", { bytes: size.toLocaleString() }), + [t], + ); + + const storagePolicy = useMemo(() => { + if (target.extended_info) { + if (!target.extended_info.storage_policy) { + return t("fileManager.unset"); + } + + return target.extended_info.storage_policy.name; + } + return ; + }, [target.extended_info, t]); + + const targetCrUri = useMemo(() => { + return new CrUri(target.path); + }, [target]); + + const restoreParent = useMemo(() => { + if (!target.metadata || !target.metadata[Metadata.restore_uri]) { + return null; + } + return new CrUri(target.metadata[Metadata.restore_uri]); + }, [target]); + + const getFolderSummary = useCallback(() => { + setFolderSummary(undefined); + dispatch(getFileInfo({ uri: target.path, folder_summary: true })) + .then((res) => { + setFolderSummary(res.folder_summary ?? null); + }) + .catch(() => { + setFolderSummary(null); + }); + }, [target, setFolderSummary, dispatch]); + + const folderSize = useMemo(() => { + if (!folderSummary) { + return ""; + } + + const sizeText = displaySize(folderSummary.size); + if (!folderSummary.completed) { + return t("fileManager.moreThan", { text: sizeText }); + } + return sizeText; + }, [folderSummary, t]); + + const folderChildren = useMemo(() => { + if (!folderSummary) { + return ""; + } + + let files = folderSummary.files.toLocaleString(); + let folders = folderSummary.folders.toLocaleString(); + + if (!folderSummary.completed) { + files += "+"; + folders += "+"; + } + + return t("application:fileManager.folderChildren", { + files, + folders, + }); + }, [folderSummary, t]); + + return ( + <> + + {t("application:fileManager.basicInfo")} + + + + } + /> + {restoreParent && ( + + } + /> + )} + {target.metadata && target.metadata[Metadata.expected_collect_time] && ( + + } + /> + )} + {target.type == FileType.folder && !isSymbolicLink && ( + <> + {!folderSummary && ( + + ) : ( + + {t("fileManager.calculate")} + + ) + } + /> + )} + {folderSummary && ( + <> + + + } + /> + + )} + + )} + {target.type == FileType.file && ( + <> + + + ) + } + /> + + )} + } + /> + } + /> + + ); +}; + +export default BasicInfo; diff --git a/src/component/FileManager/Sidebar/Data.tsx b/src/component/FileManager/Sidebar/Data.tsx new file mode 100644 index 0000000..57c2961 --- /dev/null +++ b/src/component/FileManager/Sidebar/Data.tsx @@ -0,0 +1,112 @@ +import { Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { EntityType, FileResponse } from "../../../api/explorer.ts"; +import { setVersionControlDialog } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { downloadSingleFile } from "../../../redux/thunks/download.ts"; +import { sizeToString } from "../../../util"; +import { NoWrapTableCell, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx"; +import TimeBadge from "../../Common/TimeBadge.tsx"; + +export interface DataProps { + target: FileResponse; +} + +export const EntityTypeText: Record = { + [EntityType.thumbnail]: "application:fileManager.thumbnails", + [EntityType.live_photo]: "application:fileManager.livePhoto", + [EntityType.version]: "application:fileManager.version", +}; + +const Data = ({ target }: DataProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const downloadEntity = useCallback( + (entityId: string) => { + dispatch(downloadSingleFile(target, entityId)); + }, + [target, dispatch], + ); + + const versionSizes = useMemo(() => { + let size = 0; + let notFound = true; + target.extended_info?.entities?.forEach((entity) => { + if (entity.type === EntityType.version) { + size += entity.size; + notFound = false; + } + }); + + return notFound ? undefined : size; + }, [target.extended_info?.entities]); + + if (!target.extended_info?.entities) { + return null; + } + + return ( + <> + + {t("application:fileManager.data")} + + + + + + {t("fileManager.type")} + {t("fileManager.size")} + {t("fileManager.createdAt")} + {t("fileManager.storagePolicy")} + {t("fileManager.actions")} + + + + {versionSizes != undefined && ( + + + {t("fileManager.versionEntity")} + + {sizeToString(versionSizes)} + - + - + + dispatch(setVersionControlDialog({ open: true, file: target }))} + underline={"hover"} + > + {t("fileManager.manageVersions")} + + + + )} + {target.extended_info?.entities + ?.filter((e) => e.type != EntityType.version) + .map((e) => ( + + + {t(EntityTypeText[e.type as EntityType])} + + {sizeToString(e.size)} + + + + {e.storage_policy?.name} + + downloadEntity(e.id)}> + {t("fileManager.download")} + + + + ))} + +
+
+ + ); +}; + +export default Data; diff --git a/src/component/FileManager/Sidebar/Details.tsx b/src/component/FileManager/Sidebar/Details.tsx new file mode 100644 index 0000000..16bd813 --- /dev/null +++ b/src/component/FileManager/Sidebar/Details.tsx @@ -0,0 +1,73 @@ +import { FileResponse, FileType, Metadata } from "../../../api/explorer.ts"; +import { useEffect, useState } from "react"; +import { loadFileThumb } from "../../../redux/thunks/file.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { Box, Stack, Typography } from "@mui/material"; +import MediaInfo from "./MediaInfo.tsx"; +import BasicInfo from "./BasicInfo.tsx"; +import Tags from "./Tags.tsx"; +import Data from "./Data.tsx"; + +export interface DetailsProps { + inPhotoViewer?: boolean; + target: FileResponse; +} + +const InfoBlock = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => { + return ( + + {title} + + {children} + + + ); +}; + +const Details = ({ target, inPhotoViewer }: DetailsProps) => { + const dispatch = useAppDispatch(); + const [thumbSrc, setThumbSrc] = useState(null); + useEffect(() => { + if ( + target.type == FileType.file && + (!target.metadata || + target.metadata[Metadata.thumbDisabled] === undefined) + ) { + dispatch(loadFileThumb(FileManagerIndex.main, target)).then((src) => { + setThumbSrc(src); + }); + } + + setThumbSrc(null); + }, [target]); + + return ( + + {thumbSrc && !inPhotoViewer && ( + { + setThumbSrc(null); + }} + src={thumbSrc} + sx={{ + borderRadius: "8px", + }} + component={"img"} + /> + )} + + + + + + ); +}; + +export default Details; diff --git a/src/component/FileManager/Sidebar/Header.tsx b/src/component/FileManager/Sidebar/Header.tsx new file mode 100644 index 0000000..99ef228 --- /dev/null +++ b/src/component/FileManager/Sidebar/Header.tsx @@ -0,0 +1,53 @@ +import { FileResponse } from "../../../api/explorer.ts"; +import { Box, IconButton, Skeleton, Typography } from "@mui/material"; +import FileIcon from "../Explorer/FileIcon.tsx"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { closeSidebar } from "../../../redux/globalStateSlice.ts"; + +export interface HeaderProps { + target: FileResponse | undefined | null; +} +const Header = ({ target }: HeaderProps) => { + const dispatch = useAppDispatch(); + return ( + + {target !== null && ( + + )} + {target !== null && ( + + + {target && target.name} + {!target && } + + + )} + { + dispatch(closeSidebar()); + }} + sx={{ + ml: 1, + placeSelf: "flex-start", + position: "relative", + top: "-4px", + }} + size={"small"} + > + + + + ); +}; + +export default Header; diff --git a/src/component/FileManager/Sidebar/InfoRow.tsx b/src/component/FileManager/Sidebar/InfoRow.tsx new file mode 100644 index 0000000..33b835f --- /dev/null +++ b/src/component/FileManager/Sidebar/InfoRow.tsx @@ -0,0 +1,22 @@ +import { Box, Typography } from "@mui/material"; +import React from "react"; + +export interface InfoRowProps { + title: string; + content: React.ReactNode | string; +} + +const InfoRow = ({ title, content }: InfoRowProps) => { + return ( + + + {title} + + + {content} + + + ); +}; + +export default InfoRow; diff --git a/src/component/FileManager/Sidebar/Map/LeafletMapBox.tsx b/src/component/FileManager/Sidebar/Map/LeafletMapBox.tsx new file mode 100644 index 0000000..a68c7d4 --- /dev/null +++ b/src/component/FileManager/Sidebar/Map/LeafletMapBox.tsx @@ -0,0 +1,79 @@ +import "leaflet/dist/leaflet.css"; +import { MapContainer, Marker, TileLayer, useMap } from "react-leaflet"; +import { Box, useTheme } from "@mui/material"; +import { MapLoaderProps } from "./MapLoader.tsx"; +import { useMemo } from "react"; + +/* + Majority users of Cloudreve in China prefer not to include Ukraine flag. Feel free to remove it if you want to display it. + */ +const includeUkraineFlag = false; +const FlagRemoval = () => { + const map = useMap(); + if (!includeUkraineFlag) { + map.attributionControl.setPrefix( + 'Leaflet', + ); + } + return null; +}; + +const LeafletMapBox = ({ + center, + height, + mapProvider, + googleTileType, + sx, + ...rest +}: MapLoaderProps) => { + const theme = useTheme(); + const googleTileUrl = useMemo(() => { + switch (googleTileType) { + case "terrain": + return "http://{s}.google.com/vt/lyrs=p&x={x}&y={y}&z={z}"; + case "satellite": + return "http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}"; + default: + return "http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}"; + } + }, [googleTileType]); + return ( + + + + {/* OPEN STREEN MAPS TILES */} + {mapProvider == "openstreetmap" && ( + + )} + {/* GOOGLE MAPS TILES */} + {mapProvider == "google" && ( + + )} + + + + ); +}; + +export default LeafletMapBox; diff --git a/src/component/FileManager/Sidebar/Map/MapLoader.tsx b/src/component/FileManager/Sidebar/Map/MapLoader.tsx new file mode 100644 index 0000000..bcc9e4b --- /dev/null +++ b/src/component/FileManager/Sidebar/Map/MapLoader.tsx @@ -0,0 +1,36 @@ +import { BoxProps, Skeleton } from "@mui/material"; +import { lazy, Suspense } from "react"; +import { useAppSelector } from "../../../../redux/hooks.ts"; + +const MapBox = lazy(() => import("./LeafletMapBox.tsx")); + +export interface MapLoaderProps extends BoxProps { + height: number; + center: [number, number]; + mapProvider?: string; + googleTileType?: string; +} + +const MapLoader = (props: MapLoaderProps) => { + const mapProvider = useAppSelector( + (state) => state.siteConfig.explorer.config.map_provider, + ); + const googleTileType = useAppSelector( + (state) => state.siteConfig.explorer.config.google_map_tile_type, + ); + return ( + + } + > + + + ); +}; + +export default MapLoader; diff --git a/src/component/FileManager/Sidebar/MediaInfo.tsx b/src/component/FileManager/Sidebar/MediaInfo.tsx new file mode 100644 index 0000000..0893a41 --- /dev/null +++ b/src/component/FileManager/Sidebar/MediaInfo.tsx @@ -0,0 +1,605 @@ +import { Typography } from "@mui/material"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import { TFunction } from "i18next"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { FileResponse, Metadata } from "../../../api/explorer.ts"; +import { formatLocalTime } from "../../../util/datetime.ts"; +import CameraFilled from "../../Icons/CameraFilled.tsx"; +import CameraRounded from "../../Icons/CameraRounded.tsx"; +import ClockFilled from "../../Icons/ClockFilled.tsx"; +import Copyright from "../../Icons/Copyright.tsx"; +import DarkTheme from "../../Icons/DarkTheme.tsx"; +import Image from "../../Icons/Image.tsx"; +import InfoFilled from "../../Icons/InfoFilled.tsx"; +import MusicNote1 from "../../Icons/MusicNote1.tsx"; +import Notepad from "../../Icons/Notepad.tsx"; +import Person from "../../Icons/Person.tsx"; +import WindowApps from "../../Icons/WindowApps.tsx"; +import MapLoader from "./Map/MapLoader.tsx"; +import MediaMetaCard, { MediaMetaContent, MediaMetaElements } from "./MediaMetaCard.tsx"; + +dayjs.extend(duration); + +export interface MediaInfoProps { + target: FileResponse; +} + +const formatBitrate = (bits: string): string => { + if (!bits) return ""; + + const bitrate = parseFloat(bits); + if (isNaN(bitrate)) return bits; + + if (bitrate < 1000) { + return `${bitrate.toFixed(0)} bps`; + } else if (bitrate < 1000000) { + return `${(bitrate / 1000).toFixed(1)} kbps`; + } else { + return `${(bitrate / 1000000).toFixed(2)} mbps`; + } +}; + +export const getAperture = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.f_number]) { + const fInt = parseFloat(target.metadata[Metadata.f_number]); + if (fInt) { + return { + display: `ƒ/${fInt.toFixed(1)}`, + searchKey: Metadata.f_number, + searchValue: target.metadata[Metadata.f_number], + }; + } + } +}; + +export const getExposure = (target: FileResponse, t: TFunction): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.exposure_time]) { + return { + display: t("application:fileManager.exposureValue", { + num: target.metadata[Metadata.exposure_time], + }), + searchKey: Metadata.exposure_time, + searchValue: target.metadata[Metadata.exposure_time], + }; + } +}; + +export const getIso = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.iso_speed_ratings]) { + return { + display: target.metadata[Metadata.iso_speed_ratings], + searchKey: Metadata.iso_speed_ratings, + searchValue: target.metadata[Metadata.iso_speed_ratings], + }; + } +}; + +export const getCameraMake = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.camera_make]) { + return { + display: target.metadata[Metadata.camera_make], + searchKey: Metadata.camera_make, + searchValue: target.metadata[Metadata.camera_make], + }; + } +}; + +export const getCameraModel = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.camera_model]) { + return { + display: target.metadata[Metadata.camera_model], + searchKey: Metadata.camera_model, + searchValue: target.metadata[Metadata.camera_model], + }; + } +}; + +export const getLensMake = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.lens_make]) { + return { + display: target.metadata[Metadata.lens_make], + searchKey: Metadata.lens_make, + searchValue: target.metadata[Metadata.lens_make], + }; + } +}; + +export const getLensModel = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.lens_model]) { + return { + display: target.metadata[Metadata.lens_model], + searchKey: Metadata.lens_model, + searchValue: target.metadata[Metadata.lens_model], + }; + } +}; + +export const getFocalLength = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.focal_length]) { + return { + display: `${target.metadata[Metadata.focal_length]}mm`, + searchKey: Metadata.focal_length, + searchValue: target.metadata?.[Metadata.focal_length], + }; + } +}; + +export const getExposureBias = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.exposure_bias_value]) { + const evFloat = parseFloat(target.metadata[Metadata.exposure_bias_value]); + return { + display: `${evFloat.toFixed(1)} ev`, + searchKey: Metadata.exposure_bias_value, + searchValue: target.metadata[Metadata.exposure_bias_value], + }; + } +}; + +export const getFlash = (target: FileResponse, t: TFunction): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.flash]) { + return { + display: target.metadata[Metadata.flash] == "1" ? t("fileManager.on") : t("fileManager.off"), + searchKey: Metadata.flash, + searchValue: target.metadata[Metadata.flash], + }; + } +}; + +export const getSoftware = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.software]) { + return { + display: target.metadata[Metadata.software], + searchKey: Metadata.software, + searchValue: target.metadata[Metadata.software], + }; + } +}; + +export const takenAt = (target: FileResponse): string | undefined => { + if (target.metadata?.[Metadata.taken_at]) { + return formatLocalTime(target.metadata[Metadata.taken_at], true); + } +}; + +export const getImageSize = (target: FileResponse): (MediaMetaElements | string)[] | undefined => { + if (!target.metadata?.[Metadata.pixel_x_dimension] || !target.metadata?.[Metadata.pixel_y_dimension]) { + return undefined; + } + + const holder: (MediaMetaElements | string)[] = []; + const x = parseInt(target.metadata[Metadata.pixel_x_dimension]); + const y = parseInt(target.metadata[Metadata.pixel_y_dimension]); + const mp = (x * y) / 1000000; + if (mp > 0.1) { + holder.push(`${mp.toFixed(1)} MP · `); + } + holder.push({ + display: `${x}`, + searchKey: Metadata.pixel_x_dimension, + searchValue: target.metadata[Metadata.pixel_x_dimension], + }); + holder.push(" x "); + holder.push({ + display: `${y}`, + searchKey: Metadata.pixel_y_dimension, + searchValue: target.metadata[Metadata.pixel_y_dimension], + }); + + return holder; +}; + +export const getMediaTitle = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.music_title]) { + return { + display: target.metadata[Metadata.music_title], + searchKey: Metadata.music_title, + searchValue: target.metadata[Metadata.music_title], + }; + } else if (target.metadata?.[Metadata.stream_title]) { + return { + display: target.metadata[Metadata.stream_title], + searchKey: Metadata.stream_title, + searchValue: target.metadata[Metadata.stream_title], + }; + } +}; + +export const getArtist = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.music_artist]) { + return { + display: target.metadata[Metadata.music_artist], + searchKey: Metadata.music_artist, + searchValue: target.metadata[Metadata.music_artist], + }; + } +}; + +export const getAlbum = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.music_album]) { + return { + display: target.metadata[Metadata.music_album], + searchKey: Metadata.music_album, + searchValue: target.metadata[Metadata.music_album], + }; + } +}; + +export const getDuration = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.stream_duration]) { + return { + display: dayjs.duration(parseFloat(target.metadata[Metadata.stream_duration]), "seconds").format("HH:mm:ss"), + searchKey: Metadata.stream_duration, + searchValue: target.metadata[Metadata.stream_duration], + }; + } +}; + +const MediaInfo = ({ target }: MediaInfoProps) => { + if (!target.metadata) { + return undefined; + } + + const { t } = useTranslation(); + + const exifContents = useMemo(() => { + let res: MediaMetaContent[] = []; + const aperture = getAperture(target); + if (aperture) { + res.push({ + title: [t("fileManager.aperture")], + content: [aperture], + }); + } + + const exposure = getExposure(target, t); + if (exposure) { + res.push({ + title: [t("fileManager.exposure")], + content: [exposure], + }); + } + + const iso = getIso(target); + if (iso) { + res.push({ + title: [t("fileManager.iso")], + content: [iso], + }); + } + + return res; + }, [target, t]); + + const cameraModelContent = useMemo(() => { + let title: (MediaMetaElements | string)[] = []; + let content: (MediaMetaElements | string)[] = []; + + const cameraMake = getCameraMake(target); + if (cameraMake) { + title.push(cameraMake); + } + + const cameraModel = getCameraModel(target); + if (cameraModel) { + if (title.length > 0) { + title.push(" "); + } + title.push(cameraModel); + } + + const lensMake = getLensMake(target); + if (lensMake) { + content.push(lensMake); + } + + const lensModel = getLensModel(target); + if (lensModel) { + if (content.length > 0) { + content.push(" "); + } + content.push(lensModel); + } + + const focalLength = getFocalLength(target); + if (focalLength) { + // Push ( to the start of the content + const contentEmpty = content.length == 0; + if (!contentEmpty) { + content.unshift(" ("); + } + content.unshift(focalLength); + if (!contentEmpty) { + content.push(")"); + } + } + if (title.length == 0) { + title = content; + } + if (title.length > 0 || content.length > 0) { + return { title, content }; + } + return undefined; + }, [target, t]); + + const lightInfoContent = useMemo(() => { + let res: MediaMetaContent[] = []; + + const exposureBias = getExposureBias(target); + if (exposureBias) { + res.push({ + title: [t("fileManager.exposureBias")], + content: [exposureBias], + }); + } + + const flash = getFlash(target, t); + if (flash) { + res.push({ + title: [t("fileManager.flash")], + content: [flash], + }); + } + return res; + }, [target, t]); + + const copyRightContent = useMemo(() => { + const holder: (MediaMetaElements | string)[] = []; + if (target.metadata?.[Metadata.copy_right]) { + holder.push({ + display: target.metadata[Metadata.copy_right], + searchKey: Metadata.copy_right, + searchValue: target.metadata[Metadata.copy_right], + }); + } + + if (target.metadata?.[Metadata.artist]) { + if (holder.length > 0) { + holder.push(" "); + } + holder.push({ + display: target.metadata[Metadata.artist], + searchKey: Metadata.artist, + searchValue: target.metadata[Metadata.artist], + }); + } + + if (holder.length == 0) { + return undefined; + } + + return { + title: [t("fileManager.copyright")], + content: holder, + } as MediaMetaContent; + }, [target, t]); + + const softwareContent = useMemo(() => { + const software = getSoftware(target); + if (!software) { + return undefined; + } + + return { + title: [t("fileManager.software")], + content: [software], + } as MediaMetaContent; + }, [target, t]); + + const mapBoxGps = useMemo(() => { + if (!target.metadata?.[Metadata.gps_lat] || !target.metadata?.[Metadata.gps_lng]) { + return undefined; + } + + return [parseFloat(target.metadata[Metadata.gps_lat]), parseFloat(target.metadata[Metadata.gps_lng])] as [ + number, + number, + ]; + }, [target, t]); + + const takenTimeContent = useMemo(() => { + const takenTime = takenAt(target); + if (!takenTime) { + return undefined; + } + + return { + title: [t("application:fileManager.takenAt")], + content: [takenTime], + } as MediaMetaContent; + }, [target, t]); + + const imageSizeContent = useMemo(() => { + const imageSize = getImageSize(target); + if (!imageSize) { + return undefined; + } + + return { + title: [t("fileManager.resolution")], + content: imageSize, + } as MediaMetaContent; + }, [target, t]); + + const mediaTitleContent = useMemo(() => { + const res = { + title: [t("fileManager.title")], + content: [], + } as MediaMetaContent; + + const mediaTitle = getMediaTitle(target); + if (mediaTitle) { + res.content.push(mediaTitle); + return res; + } else { + return undefined; + } + }, [target, t]); + + const musicArtistContent = useMemo(() => { + const artist = getArtist(target); + if (!artist) { + return undefined; + } + return { + title: [t("fileManager.artist")], + content: [artist], + } as MediaMetaContent; + }, [target, t]); + + const albumContent = useMemo(() => { + const album = getAlbum(target); + if (!album) { + return undefined; + } + + return { + title: [t("fileManager.album")], + content: [album], + } as MediaMetaContent; + }, [target, t]); + + const durationContent = useMemo(() => { + const duration = getDuration(target); + if (!duration) { + return undefined; + } + + return { + title: [t("fileManager.duration")], + content: [duration], + } as MediaMetaContent; + }, [target, t]); + + const streamFormatContent = useMemo(() => { + let res: MediaMetaContent[] = []; + + if (target.metadata?.[Metadata.stream_format_long]) { + res.push({ + title: [t("fileManager.format")], + content: [ + { + display: target.metadata[Metadata.stream_format_long], + searchKey: Metadata.stream_format_long, + searchValue: target.metadata[Metadata.stream_format_long], + }, + ], + }); + + if (target.metadata?.[Metadata.stream_bit_rate]) { + res[0].content.push(" · "); + res[0].content.push({ + display: formatBitrate(target.metadata[Metadata.stream_bit_rate]), + searchKey: Metadata.stream_bit_rate, + searchValue: target.metadata[Metadata.stream_bit_rate], + }); + } + } + + if (res.length == 0) { + return undefined; + } + + return res; + }, [target, t]); + + const singleStreamContents = useMemo(() => { + let res: MediaMetaContent[] = []; + const allMetas = Object.keys(target.metadata ?? {}); + const streamGrouped: { + [key: string]: { type: string; [key: string]: string }; + } = {}; + allMetas.forEach((meta) => { + if (!meta.startsWith("stream:stream_")) { + return; + } + + const [prefix, group, type, ...other] = meta.split("_"); + if (!streamGrouped[group]) { + streamGrouped[group] = { + type: type, + }; + } + + if (type != "video" && type != "audio") { + return; + } + + streamGrouped[group][other.join("_")] = target.metadata?.[meta] ?? ""; + }); + + for (const [group, item] of Object.entries(streamGrouped)) { + let content: string[] = []; + if (item[Metadata.stream_indexed_codec]) { + content.push(item[Metadata.stream_indexed_codec]); + } + if (item[Metadata.stream_indexed_bitrate]) { + content.push(formatBitrate(item[Metadata.stream_indexed_bitrate])); + } + if (item[Metadata.stream_indexed_width] && item[Metadata.stream_indexed_height]) { + content.push(`${item[Metadata.stream_indexed_width]}x${item[Metadata.stream_indexed_height]}`); + } + if (content.length == 0) { + continue; + } + res.push({ + title: [`Stream #${group} (${item.type})`], + content: [content.join(" · ")], + }); + } + + if (res.length > 0) { + return res; + } + + return undefined; + }, [target, t]); + + const showExifBasic = exifContents.length > 0; + const showLightInfo = lightInfoContent.length > 0; + const showCopyRight = !!copyRightContent; + const showCameraModel = !!cameraModelContent; + const showMediaInfo = + showExifBasic || + showCameraModel || + showLightInfo || + showCopyRight || + softwareContent || + takenTimeContent || + imageSizeContent || + mediaTitleContent || + musicArtistContent || + durationContent || + streamFormatContent || + singleStreamContents || + mapBoxGps; + + if (!showMediaInfo) { + return undefined; + } + + return ( + <> + + {t("fileManager.mediaInfo")} + + {showExifBasic && } + {showLightInfo && } + {showCameraModel && } + {takenTimeContent && } + {imageSizeContent && } + {showCopyRight && } + {softwareContent && } + {mapBoxGps && } + {mediaTitleContent && } + {musicArtistContent && } + {albumContent && } + {durationContent && } + {streamFormatContent && } + {singleStreamContents && singleStreamContents.map((content) => )} + + ); +}; + +export default MediaInfo; diff --git a/src/component/FileManager/Sidebar/MediaMetaCard.tsx b/src/component/FileManager/Sidebar/MediaMetaCard.tsx new file mode 100644 index 0000000..6ddf161 --- /dev/null +++ b/src/component/FileManager/Sidebar/MediaMetaCard.tsx @@ -0,0 +1,179 @@ +import { useTranslation } from "react-i18next"; +import { + Box, + Link, + LinkProps, + ListItemIcon, + ListItemText, + Menu, + styled, + SvgIconProps, + Typography, +} from "@mui/material"; +import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; +import React, { useState } from "react"; +import { SquareMenuItem } from "../ContextMenu/ContextMenu.tsx"; +import Search from "../../Icons/Search.tsx"; +import Clipboard from "../../Icons/Clipboard.tsx"; +import { searchMetadata } from "../../../redux/thunks/filemanager.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { copyToClipboard } from "../../../util"; + +export interface MediaMetaElements { + display: string; + searchKey: string; + searchValue: string; +} + +export interface MediaMetaContent { + title: (MediaMetaElements | string)[]; + content: (MediaMetaElements | string)[]; +} + +export interface MediaMetaCardProps { + contents: MediaMetaContent[]; + icon?: typeof SvgIcon | ((props: SvgIconProps) => JSX.Element); +} + +const StyledButtonBase = styled(Box)(({ theme }) => { + let bgColor = + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900]; + let bgColorHover = + theme.palette.mode === "light" + ? theme.palette.grey[300] + : theme.palette.grey[700]; + return { + borderRadius: theme.shape.borderRadius, + backgroundColor: bgColor, + display: "flex", + width: "100%", + wordBreak: "break-all", + alignItems: "center", + justifyContent: "flex-start", + padding: "8px 16px", + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + transitionProperty: "background-color,opacity,box-shadow", + gap: 15, + + textAlign: "left", + height: "100%", + userSelect: "text", + overflow: "hidden", + }; +}); + +export interface MediaMetaElementsProps extends LinkProps { + element: MediaMetaElements; +} + +export const MediaMetaElements = ({ + element, + ...rest +}: MediaMetaElementsProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const handleSearch = () => { + dispatch( + searchMetadata( + FileManagerIndex.main, + element.searchKey, + element.searchValue, + ), + ); + handleClose(); + }; + + const handleCopy = () => { + copyToClipboard(element.display); + handleClose(); + }; + + const [anchorEl, setAnchorEl] = useState(null); + const handleClose = () => { + setAnchorEl(null); + }; + return ( + <> + + + + + + + {t("application:fileManager.searchSomething", { + text: element?.display ?? "", + })} + + + + + + + + {t("application:fileManager.copyToClipboard")} + + + + setAnchorEl(e.currentTarget)} + underline="hover" + {...rest} + > + {element.display} + + + ); +}; + +const MediaMetaCard = ({ contents, icon }: MediaMetaCardProps) => { + const Icon = icon; + return ( + <> + + {Icon && } + + {contents.map(({ title, content }) => ( + + + {title.map((element) => + typeof element === "string" ? ( + element + ) : ( + + ), + )} + + + {content.map((element) => + typeof element === "string" ? ( + element + ) : ( + + ), + )} + + + ))} + + + + ); +}; +export default MediaMetaCard; diff --git a/src/component/FileManager/Sidebar/SideDrawer.js b/src/component/FileManager/Sidebar/SideDrawer.js deleted file mode 100644 index a7eff5d..0000000 --- a/src/component/FileManager/Sidebar/SideDrawer.js +++ /dev/null @@ -1,331 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles } from "@material-ui/core"; -import { useDispatch, useSelector } from "react-redux"; -import Drawer from "@material-ui/core/Drawer"; -import Toolbar from "@material-ui/core/Toolbar"; -import { Clear, Folder } from "@material-ui/icons"; -import Divider from "@material-ui/core/Divider"; -import { setSideBar } from "../../../redux/explorer/action"; -import TypeIcon from "../TypeIcon"; -import Typography from "@material-ui/core/Typography"; -import IconButton from "@material-ui/core/IconButton"; -import Grid from "@material-ui/core/Grid"; -import API from "../../../middleware/Api"; -import { filename, sizeToString } from "../../../utils"; -import Link from "@material-ui/core/Link"; -import Tooltip from "@material-ui/core/Tooltip"; -import TimeAgo from "timeago-react"; -import ListLoading from "../../Placeholder/ListLoading"; -import Hidden from "@material-ui/core/Hidden"; -import Dialog from "@material-ui/core/Dialog"; -import Slide from "@material-ui/core/Slide"; -import AppBar from "@material-ui/core/AppBar"; -import { formatLocalTime } from "../../../utils/datetime"; -import { navigateTo, toggleSnackbar } from "../../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; - -const drawerWidth = 350; - -const useStyles = makeStyles((theme) => ({ - drawer: { - width: drawerWidth, - flexShrink: 0, - }, - drawerPaper: { - width: drawerWidth, - boxShadow: - "0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)", - }, - drawerContainer: { - overflow: "auto", - }, - header: { - display: "flex", - padding: theme.spacing(3), - placeContent: "space-between", - }, - fileIcon: { width: 33, height: 33 }, - fileIconSVG: { fontSize: 20 }, - folderIcon: { - color: theme.palette.text.secondary, - width: 33, - height: 33, - }, - fileName: { - marginLeft: theme.spacing(2), - marginRight: theme.spacing(2), - wordBreak: "break-all", - flexGrow: 2, - }, - closeIcon: { - placeSelf: "flex-start", - marginTop: 2, - }, - propsContainer: { - padding: theme.spacing(3), - }, - propsLabel: { - color: theme.palette.text.secondary, - padding: theme.spacing(1), - }, - propsTime: { - color: theme.palette.text.disabled, - padding: theme.spacing(1), - }, - propsValue: { - padding: theme.spacing(1), - wordBreak: "break-all", - }, - appBar: { - position: "relative", - }, - title: { - marginLeft: theme.spacing(2), - flex: 1, - }, -})); - -const Transition = React.forwardRef(function Transition(props, ref) { - return ; -}); - -export default function SideDrawer() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const sideBarOpen = useSelector((state) => state.explorer.sideBarOpen); - const selected = useSelector((state) => state.explorer.selected); - const SetSideBar = useCallback((open) => dispatch(setSideBar(open)), [ - dispatch, - ]); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]); - const search = useSelector((state) => state.explorer.search); - const [target, setTarget] = useState(null); - const [details, setDetails] = useState(null); - const loadProps = (object) => { - API.get( - "/object/property/" + - object.id + - "?trace_root=" + - (search ? "true" : "false") + - "&is_folder=" + - (object.type === "dir").toString() - ) - .then((response) => { - setDetails(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - setDetails(null); - if (sideBarOpen) { - if (selected.length !== 1) { - SetSideBar(false); - } else { - setTarget(selected[0]); - loadProps(selected[0]); - } - } - }, [selected, sideBarOpen]); - - const classes = useStyles(); - const propsItem = [ - { - label: t("fileManager.size"), - value: (d, target) => - sizeToString(d.size) + - t("fileManager.bytes", { bytes: d.size.toLocaleString() }), - show: (d) => true, - }, - { - label: t("fileManager.storagePolicy"), - value: (d, target) => d.policy, - show: (d) => d.type === "file", - }, - { - label: t("fileManager.childFolders"), - value: (d, target) => - t("fileManager.childCount", { - num: d.child_folder_num.toLocaleString(), - }), - show: (d) => d.type === "dir", - }, - { - label: t("fileManager.childFiles"), - value: (d, target) => - t("fileManager.childCount", { - num: d.child_file_num.toLocaleString(), - }), - show: (d) => d.type === "dir", - }, - { - label: t("fileManager.parentFolder"), - // eslint-disable-next-line react/display-name - value: (d, target) => { - const path = d.path === "" ? target.path : d.path; - const name = filename(path); - return ( - - NavigateTo(path)} - > - {name === "" ? t("fileManager.rootFolder") : name} - - - ); - }, - show: (d) => true, - }, - { - label: t("fileManager.modifiedAt"), - value: (d, target) => formatLocalTime(d.updated_at), - show: (d) => true, - }, - { - label: t("fileManager.createdAt"), - value: (d) => formatLocalTime(d.created_at), - show: (d) => true, - }, - ]; - const content = ( - - {!details && } - {details && ( - <> - {propsItem.map((item) => { - if (item.show(target)) { - return ( - <> - - {item.label} - - - {item.value(details, target)} - - - ); - } - })} - {target.type === "dir" && ( - - , - , - , - ]} - /> - - )} - - )} - - ); - return ( - <> - - - {target && ( - <> - - - SetSideBar(false)} - aria-label="close" - > - - - - {target.name} - - - - {content} - - )} - - - - - -
- {target && ( - <> -
- {target.type === "dir" && ( - - )} - {target.type !== "dir" && ( - - )} -
- - {target.name} - -
- SetSideBar(false)} - className={classes.closeIcon} - aria-label="close" - size={"small"} - > - - -
- - )} - - {content} -
-
-
- - ); -} diff --git a/src/component/FileManager/Sidebar/Sidebar.tsx b/src/component/FileManager/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..55687c1 --- /dev/null +++ b/src/component/FileManager/Sidebar/Sidebar.tsx @@ -0,0 +1,87 @@ +import { RadiusFrame } from "../../Frame/RadiusFrame.tsx"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { Box, Collapse } from "@mui/material"; +import SidebarContent from "./SidebarContent.tsx"; +import { useCallback, useEffect, useState } from "react"; +import { FileResponse } from "../../../api/explorer.ts"; +import { getFileInfo } from "../../../api/api.ts"; + +export interface SideBarProps { + inPhotoViewer?: boolean; +} + +const Sidebar = ({ inPhotoViewer }: SideBarProps) => { + const dispatch = useAppDispatch(); + const sidebarOpen = useAppSelector((state) => state.globalState.sidebarOpen); + const sidebarTarget = useAppSelector( + (state) => state.globalState.sidebarTarget, + ); + // null: not valid, undefined: not loaded, FileResponse: loaded + const [target, setTarget] = useState( + undefined, + ); + + const loadExtendedInfo = useCallback( + (path: string) => { + dispatch( + getFileInfo({ + uri: path, + extended: true, + }), + ).then((res) => { + setTarget(res); + }); + }, + [target, dispatch, setTarget], + ); + + useEffect(() => { + if (sidebarTarget && sidebarOpen) { + if (typeof sidebarTarget === "string") { + } else { + setTarget(sidebarTarget); + loadExtendedInfo(sidebarTarget.path); + } + } else { + setTarget(null); + } + }, [sidebarTarget, setTarget]); + + return ( + + + + inPhotoViewer ? 0 : theme.shape.borderRadius / 8, + }} + withBorder={!inPhotoViewer} + > + + + + + ); +}; + +export default Sidebar; diff --git a/src/component/FileManager/Sidebar/SidebarContent.tsx b/src/component/FileManager/Sidebar/SidebarContent.tsx new file mode 100644 index 0000000..ca62b2d --- /dev/null +++ b/src/component/FileManager/Sidebar/SidebarContent.tsx @@ -0,0 +1,60 @@ +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { FileResponse } from "../../../api/explorer.ts"; +import Details from "./Details.tsx"; +import Header from "./Header.tsx"; + +export interface SidebarContentProps { + target: FileResponse | undefined | null; + inPhotoViewer?: boolean; +} + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +const SidebarContent = ({ target, inPhotoViewer }: SidebarContentProps) => { + const { t } = useTranslation(); + return ( + +
+ {target != null && ( + <> + + + +
+ + + + )} + + ); +}; + +export default SidebarContent; diff --git a/src/component/FileManager/Sidebar/SidebarDialog.tsx b/src/component/FileManager/Sidebar/SidebarDialog.tsx new file mode 100644 index 0000000..48c2fd7 --- /dev/null +++ b/src/component/FileManager/Sidebar/SidebarDialog.tsx @@ -0,0 +1,71 @@ +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import SidebarContent from "./SidebarContent.tsx"; +import { forwardRef, useCallback, useEffect, useState } from "react"; +import { FileResponse } from "../../../api/explorer.ts"; +import { getFileInfo } from "../../../api/api.ts"; +import { SideBarProps } from "./Sidebar.tsx"; +import { Dialog, Slide } from "@mui/material"; +import { closeSidebar } from "../../../redux/globalStateSlice.ts"; +import { TransitionProps } from "@mui/material/transitions"; + +const Transition = forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); + +const SidebarDialog = ({ inPhotoViewer }: SideBarProps) => { + const dispatch = useAppDispatch(); + const sidebarOpen = useAppSelector((state) => state.globalState.sidebarOpen); + const sidebarTarget = useAppSelector( + (state) => state.globalState.sidebarTarget, + ); + // null: not valid, undefined: not loaded, FileResponse: loaded + const [target, setTarget] = useState( + undefined, + ); + + const loadExtendedInfo = useCallback( + (path: string) => { + dispatch( + getFileInfo({ + uri: path, + extended: true, + }), + ).then((res) => { + setTarget(res); + }); + }, + [target, dispatch, setTarget], + ); + + useEffect(() => { + if (sidebarTarget && sidebarOpen) { + if (typeof sidebarTarget === "string") { + } else { + setTarget(sidebarTarget); + loadExtendedInfo(sidebarTarget.path); + } + } else { + setTarget(null); + } + }, [sidebarTarget, setTarget]); + + return ( + { + dispatch(closeSidebar()); + }} + > + + + ); +}; + +export default SidebarDialog; diff --git a/src/component/FileManager/Sidebar/Tags.tsx b/src/component/FileManager/Sidebar/Tags.tsx new file mode 100644 index 0000000..b696ae6 --- /dev/null +++ b/src/component/FileManager/Sidebar/Tags.tsx @@ -0,0 +1,62 @@ +import { FileResponse, Metadata } from "../../../api/explorer.ts"; +import { useTranslation } from "react-i18next"; +import { Box, Typography } from "@mui/material"; +import React, { useMemo } from "react"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import FileTag from "../Explorer/FileTag.tsx"; + +export interface TagsProps { + target: FileResponse; +} + +export const getFileTags = (file: FileResponse) => { + if (file.metadata) { + const tags: { key: string; value: string }[] = []; + Object.keys(file.metadata).forEach((key: string) => { + if (key.startsWith(Metadata.tag_prefix)) { + // trim prefix for key + tags.push({ + key: key.slice(Metadata.tag_prefix.length), + value: file.metadata?.[key] ?? "", + }); + } + }); + return tags; + } +}; + +const Tags = ({ target }: TagsProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const fileTag = useMemo(() => getFileTags(target), [target]); + + if (!fileTag || fileTag.length === 0) { + return null; + } + + return ( + <> + + {t("application:fileManager.tags")} + + + {fileTag.map((tag, i) => ( + + ))} + + + ); +}; + +export default Tags; diff --git a/src/component/FileManager/SmallIcon.js b/src/component/FileManager/SmallIcon.js deleted file mode 100644 index 10b4e55..0000000 --- a/src/component/FileManager/SmallIcon.js +++ /dev/null @@ -1,183 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import classNames from "classnames"; -import { - ButtonBase, - fade, - Tooltip, - Typography, - withStyles, -} from "@material-ui/core"; -import TypeIcon from "./TypeIcon"; -import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; -import Grow from "@material-ui/core/Grow"; -import { Folder } from "@material-ui/icons"; -import FileName from "./FileName"; - -const styles = (theme) => ({ - container: { - padding: "7px", - }, - - selected: { - "&:hover": { - border: "1px solid #d0d0d0", - }, - backgroundColor: fade( - theme.palette.primary.main, - theme.palette.type === "dark" ? 0.3 : 0.18 - ), - }, - notSelected: { - "&:hover": { - backgroundColor: theme.palette.background.default, - border: "1px solid #d0d0d0", - }, - backgroundColor: theme.palette.background.paper, - }, - - button: { - height: "50px", - border: "1px solid " + theme.palette.divider, - width: "100%", - borderRadius: theme.shape.borderRadius, - boxSizing: "border-box", - transition: - "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", - display: "flex", - justifyContent: "left", - alignItems: "initial", - }, - icon: { - margin: "10px 10px 10px 16px", - height: "30px", - minWidth: "30px", - backgroundColor: theme.palette.background.paper, - borderRadius: "90%", - paddingTop: "3px", - color: theme.palette.text.secondary, - }, - folderNameSelected: { - color: - theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, - fontWeight: "500", - }, - folderNameNotSelected: { - color: theme.palette.text.secondary, - }, - folderName: { - marginTop: "15px", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - marginRight: "20px", - }, - checkIcon: { - color: theme.palette.primary.main, - }, -}); - -const mapStateToProps = (state) => { - return { - selected: state.explorer.selected, - }; -}; - -const mapDispatchToProps = () => { - return {}; -}; - -class SmallIconCompoment extends Component { - state = {}; - - shouldComponentUpdate(nextProps, nextState, nextContext) { - const isSelectedCurrent = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - const isSelectedNext = - nextProps.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - if ( - nextProps.selected !== this.props.selected && - isSelectedCurrent === isSelectedNext - ) { - return false; - } - - return true; - } - - render() { - const { classes } = this.props; - const isSelected = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - - return ( - -
- {!isSelected && ( - <> - {this.props.isFolder && } - {!this.props.isFolder && ( - - )} - - )} - {isSelected && ( - - - - )} -
- - - - - -
- ); - } -} - -SmallIconCompoment.propTypes = { - classes: PropTypes.object.isRequired, - file: PropTypes.object.isRequired, -}; - -const SmallIcon = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(SmallIconCompoment)); - -export default SmallIcon; diff --git a/src/component/FileManager/Sort.tsx b/src/component/FileManager/Sort.tsx deleted file mode 100644 index 017687d..0000000 --- a/src/component/FileManager/Sort.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { MouseEventHandler, useState } from "react"; -import { IconButton, Menu, MenuItem } from "@material-ui/core"; -import TextTotateVerticalIcon from "@material-ui/icons/TextRotateVertical"; -import { useTranslation } from "react-i18next"; -import { CloudreveFile, SortMethod } from "./../../types/index"; - -const SORT_OPTIONS: { - value: SortMethod; - label: string; -}[] = [ - { value: "namePos", label: "A-Z" }, - { value: "nameRev", label: "Z-A" }, - { value: "timePos", label: "oldestUploaded" }, - { value: "timeRev", label: "newestUploaded" }, - { value: "modifyTimePos", label: "oldestModified" }, - { value: "modifyTimeRev", label: "newestModified" }, - { value: "sizePos", label: "smallest" }, - { value: "sizeRes", label: "largest" }, -] - -export default function Sort({ value, onChange, isSmall, inherit, className }) { - const { t } = useTranslation("application", { keyPrefix: "fileManager.sortMethods" }); - - const [anchorSort, setAnchorSort] = useState(null); - const showSortOptions: MouseEventHandler = (e) => { - setAnchorSort(e.currentTarget); - } - - const [sortBy, setSortBy] = useState(value || '') - function onChangeSort(value: SortMethod) { - setSortBy(value) - onChange(value) - setAnchorSort(null); - } - return ( - <> - - - - setAnchorSort(null)} - > - { - SORT_OPTIONS.map((option, index) => ( - onChangeSort(option.value)} - > - {t(option.label)} - - )) - } - - - ) -} - - -type SortFunc = (a: CloudreveFile, b: CloudreveFile) => number; - -export const sortMethodFuncs: Record = { - sizePos: (a: CloudreveFile, b: CloudreveFile) => { - return a.size - b.size; - }, - sizeRes: (a: CloudreveFile, b: CloudreveFile) => { - return b.size - a.size; - }, - namePos: (a: CloudreveFile, b: CloudreveFile) => { - return a.name.localeCompare( - b.name, - navigator.languages[0] || navigator.language, - { numeric: true, ignorePunctuation: true } - ); - }, - nameRev: (a: CloudreveFile, b: CloudreveFile) => { - return b.name.localeCompare( - a.name, - navigator.languages[0] || navigator.language, - { numeric: true, ignorePunctuation: true } - ); - }, - timePos: (a: CloudreveFile, b: CloudreveFile) => { - return Date.parse(a.create_date) - Date.parse(b.create_date); - }, - timeRev: (a: CloudreveFile, b: CloudreveFile) => { - return Date.parse(b.create_date) - Date.parse(a.create_date); - }, - modifyTimePos: (a: CloudreveFile, b: CloudreveFile) => { - return Date.parse(a.date) - Date.parse(b.date); - }, - modifyTimeRev: (a: CloudreveFile, b: CloudreveFile) => { - return Date.parse(b.date) - Date.parse(a.date); - }, -}; diff --git a/src/component/FileManager/TableRow.js b/src/component/FileManager/TableRow.js deleted file mode 100644 index f562dc7..0000000 --- a/src/component/FileManager/TableRow.js +++ /dev/null @@ -1,229 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; - -import FolderIcon from "@material-ui/icons/Folder"; -import classNames from "classnames"; -import { sizeToString } from "../../utils/index"; -import { - fade, - TableCell, - TableRow, - Typography, - withStyles, -} from "@material-ui/core"; -import TypeIcon from "./TypeIcon"; -import pathHelper from "../../utils/page"; -import statusHelper from "../../utils/page"; -import { withRouter } from "react-router"; -import KeyboardReturnIcon from "@material-ui/icons/KeyboardReturn"; -import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded"; -import Grow from "@material-ui/core/Grow"; -import { formatLocalTime } from "../../utils/datetime"; -import FileName from "./FileName"; - -const styles = (theme) => ({ - selected: { - "&:hover": {}, - backgroundColor: fade(theme.palette.primary.main, 0.18), - }, - - selectedShared: { - "&:hover": {}, - backgroundColor: fade(theme.palette.primary.main, 0.18), - }, - - notSelected: { - "&:hover": { - backgroundColor: theme.palette.action.hover, - }, - }, - icon: { - verticalAlign: "middle", - marginRight: "20px", - color: theme.palette.text.secondary, - }, - tableIcon: { - marginRight: "20px", - verticalAlign: "middle", - }, - folderNameSelected: { - color: - theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark, - fontWeight: "500", - userSelect: "none", - }, - folderNameNotSelected: { - color: theme.palette.text.secondary, - userSelect: "none", - }, - folderName: { - marginRight: "20px", - display: "flex", - alignItems: "center", - }, - hideAuto: { - [theme.breakpoints.down("sm")]: { - display: "none", - }, - }, - tableRow: { - padding: "10px 16px", - }, - checkIcon: { - color: theme.palette.primary.main, - }, - active: { - backgroundColor: fade(theme.palette.primary.main, 0.1), - }, -}); - -const mapStateToProps = (state) => { - return { - selected: state.explorer.selected, - }; -}; - -const mapDispatchToProps = () => { - return {}; -}; - -class TableRowCompoment extends Component { - state = {}; - - shouldComponentUpdate(nextProps, nextState, nextContext) { - const isSelectedCurrent = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - const isSelectedNext = - nextProps.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - if ( - nextProps.selected !== this.props.selected && - isSelectedCurrent === isSelectedNext - ) { - return false; - } - - return true; - } - - render() { - const { classes } = this.props; - const isShare = pathHelper.isSharePage(this.props.location.pathname); - - let icon; - if (this.props.file.type === "dir") { - icon = ; - } else if (this.props.file.type === "up") { - icon = ; - } else { - icon = ( - - ); - } - const isSelected = - this.props.selected.findIndex((value) => { - return value === this.props.file; - }) !== -1; - const isMobile = statusHelper.isMobile(); - - return ( - - - -
- {!isSelected && icon} - {isSelected && ( - - - - )} -
- -
-
- - - {" "} - {this.props.file.type !== "dir" && - this.props.file.type !== "up" && - sizeToString(this.props.file.size)} - - - - - {" "} - {formatLocalTime(this.props.file.date)} - - -
- ); - } -} - -TableRowCompoment.propTypes = { - classes: PropTypes.object.isRequired, - file: PropTypes.object.isRequired, -}; - -const TableItem = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(TableRowCompoment))); - -export default TableItem; diff --git a/src/component/FileManager/TopBar/Breadcrumb.tsx b/src/component/FileManager/TopBar/Breadcrumb.tsx new file mode 100644 index 0000000..fcdd0c7 --- /dev/null +++ b/src/component/FileManager/TopBar/Breadcrumb.tsx @@ -0,0 +1,273 @@ +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { + Box, + ClickAwayListener, + Menu, + styled, + TextField, + useMediaQuery, + useTheme, +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import BreadcrumbButton, { + BreadcrumbButtonBase, + BreadcrumbButtonProps, +} from "./BreadcrumbButton.tsx"; +import CrUri from "../../../util/uri.ts"; +import ChevronRight from "../../Icons/ChevronRight.tsx"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import { useIsOverflow } from "../../../hooks/useOverflow.tsx"; +import { mergeRefs } from "../../../util"; +import BreadcrumbHiddenItem from "./BreadcrumbHiddenItem.tsx"; +import { NoOpDropUri, useFileDrag } from "../Dnd/DndWrappedFile.tsx"; +import { navigateToPath } from "../../../redux/thunks/filemanager.ts"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +const PathTextField = styled(TextField)(() => ({ + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + "& .MuiOutlinedInput-input": { + padding: "6px 4px", + fontSize: "0.8125rem", + lineHeight: "1.5", + fontFamily: "monospace", + }, + height: "32px", + overflow: "hidden", + verticalAlign: "middle", +})); + +const RightIcon = styled(ChevronRight)(({ theme }) => ({ + fontSize: 15, + mx: 0.5, + verticalAlign: "middle", + color: theme.palette.text.disabled, +})); + +interface pathElements extends BreadcrumbButtonProps {} + +const useBreadcrumb = (targetPath?: string) => { + if (targetPath) { + const uri = new CrUri(targetPath); + const elements = uri.elements(); + return [targetPath, elements, uri.base(true)] as const; + } + + const index = useContext(FmIndexContext); + + const base = useAppSelector( + (s) => s.fileManager[index].path_root_with_category, + ); + const path = useAppSelector((s) => s.fileManager[index].path); + const elements = useAppSelector((s) => s.fileManager[index].path_elements); + + return [path, elements, base] as const; +}; + +export interface BreadcrumbProps { + targetPath?: string; + displayOnly?: boolean; +} + +const Breadcrumb = (props: BreadcrumbProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const previousElements = useRef(0); + const [editing, setEditing] = useState(false); + const [editedPath, setEditedPath] = useState(""); + const [maxHiding, setMaxHiding] = useState(0); + const [anchorEl, setAnchorEl] = useState(null); + const hiddenOpen = Boolean(anchorEl); + const hiddenExpandButtonRef = useRef(); + const chainRef = useRef(null); + + const openHiddenMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + }; + + const [path, elements, base] = useBreadcrumb(props.targetPath); + + const onEdit = useCallback(() => { + if (props.displayOnly) { + return; + } + if (path) { + const uri = new CrUri(path); + setEditedPath(uri.path()); + } + setEditing(true); + }, [path, props.displayOnly]); + + const buttons = useMemo(() => { + if (!base || !elements) { + return []; + } + + const res: pathElements[] = []; + + // First, inject root button + res.push({ + path: base, + is_root: true, + displayOnly: props.displayOnly, + is_latest: elements.length === 0, + }); + + const currentBase = new CrUri(base); + res.push( + ...elements.map((e, i) => { + return { + path: currentBase.join(e).toString(), + name: e, + displayOnly: props.displayOnly, + is_latest: i === elements.length - 1, + }; + }), + ); + + return res; + }, [elements, base, props.displayOnly]); + + const hidedButtons = useMemo(() => { + return buttons.slice(0, maxHiding); + }, [buttons, maxHiding]); + + const displayedButtons = useMemo(() => { + return buttons.slice(maxHiding); + }, [buttons, maxHiding]); + + const isOverflow = useIsOverflow(chainRef, (_isOverflow) => {}); + + useEffect(() => { + if (isOverflow && !isMobile) { + setMaxHiding(buttons.length - 1); + } + }, [isOverflow, isMobile]); + + // Cancel collapse when elements are less than previous + useEffect(() => { + const current = elements?.length ?? 0; + if (previousElements.current > current) { + setTimeout(() => { + setMaxHiding(0); + }, theme.transitions.duration.standard); + } + previousElements.current = current; + }, [elements]); + + const submitNewPath = useCallback(() => { + setEditing(false); + if (!path || !editedPath) { + return; + } + const uri = new CrUri(path); + if (uri.path() == editedPath || props.displayOnly) { + // No change + return; + } + + // Apply new path and navigate + dispatch(navigateToPath(fmIndex, uri.setPath(editedPath).toString())); + }, [path, editedPath, props.displayOnly, fmIndex, dispatch]); + + const [drag, drop, isOver, isDragging] = useFileDrag({ + dropUri: NoOpDropUri, + }); + + useEffect(() => { + if (isOver && hiddenExpandButtonRef.current) { + hiddenExpandButtonRef.current?.click(); + } + }, [isOver]); + + return ( + <> + + {editing && ( + submitNewPath()} + > + e.target.select()} + fullWidth + InputProps={{ + readOnly: props.displayOnly, + }} + onKeyPress={(e) => { + if (e.key === "Enter") { + submitNewPath(); + } + }} + autoFocus + value={editedPath} + onChange={(e) => setEditedPath(e.target.value)} + /> + + )} + {!editing && ( + <> + {hidedButtons.length > 0 && ( + + + + )} + {...displayedButtons.map((b, i) => ( + <> + {i == 0 && hidedButtons.length > 0 && } + + {i != displayedButtons.length - 1 && } + + ))} + + )} + + setAnchorEl(null)} + slotProps={{ + list: { + dense: true, + } + }} + > + {hidedButtons && + hidedButtons.map((b) => ( + setAnchorEl(null)} + {...b} + /> + ))} + + + ); +}; + +export default Breadcrumb; diff --git a/src/component/FileManager/TopBar/BreadcrumbButton.tsx b/src/component/FileManager/TopBar/BreadcrumbButton.tsx new file mode 100644 index 0000000..2c07820 --- /dev/null +++ b/src/component/FileManager/TopBar/BreadcrumbButton.tsx @@ -0,0 +1,334 @@ +import { Button, Skeleton, styled, SvgIconProps, Tooltip } from "@mui/material"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import CrUri, { + Filesystem, + UriQuery, + UriSearchCategory, +} from "../../../util/uri.ts"; +import { useTranslation } from "react-i18next"; +import Home from "../../Icons/Home.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import CaretDown from "../../Icons/CaretDown.tsx"; +import HomeOutlined from "../../Icons/HomeOutlined.tsx"; +import Delete from "../../Icons/Delete.tsx"; +import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; +import PeopleTeam from "../../Icons/PeopleTeam.tsx"; +import PeopleTeamOutlined from "../../Icons/PeopleTeamOutlined.tsx"; +import UserAvatar from "../../Common/User/UserAvatar.tsx"; +import Image from "../../Icons/Image.tsx"; +import ImageOutlined from "../../Icons/ImageOutlined.tsx"; +import Video from "../../Icons/Video.tsx"; +import VideoOutlined from "../../Icons/VideoOutlined.tsx"; +import MusicNote1 from "../../Icons/MusicNote1.tsx"; +import MusicNote1Outlined from "../../Icons/MusicNote1Outlined.tsx"; +import DocumentTextOutlined from "../../Icons/DocumentTextOutlined.tsx"; +import DocumentText from "../../Icons/DocumentText.tsx"; +import { useFileDrag } from "../Dnd/DndWrappedFile.tsx"; +import { + navigateToPath, + openContextUrlFromUri, +} from "../../../redux/thunks/filemanager.ts"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { queueLoadShareInfo } from "../../../redux/thunks/share.ts"; +import LinkDismiss from "../../Icons/LinkDismiss.tsx"; +import { usePopupState } from "material-ui-popup-state/hooks"; +import { bindHover, bindPopover } from "material-ui-popup-state"; +import ShareInfoPopover from "./ShareInfoPopover.tsx"; +import SessionManager from "../../../session"; +import { NoWrapBox } from "../../Common/StyledComponents.tsx"; +import { Share } from "../../../api/explorer.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import PageTitle from "../../../router/PageTitle.tsx"; + +export const BreadcrumbButtonBase = styled(Button)<{ isDropOver?: boolean }>( + ({ theme, isDropOver }) => ({ + color: theme.palette.text.secondary, + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms !important", + transitionProperty: "background-color,opacity,box-shadow", + boxShadow: isDropOver + ? `inset 0 0 0 2px ${theme.palette.primary.light}` + : "none", + minHeight: theme.spacing(4), + }), +); + +export interface BreadcrumbButtonProps { + path: string; + name?: string; + is_latest?: boolean; + displayOnly?: boolean; + is_root?: boolean; + count_share_views?: boolean; + [key: string]: any; +} + +export interface StartIcon { + Element?: (props: { [key: string]: any }) => JSX.Element; + Icons?: ((props: SvgIconProps) => JSX.Element)[]; +} + +export const useBreadcrumbButtons = ({ + name, + is_latest, + path, + displayOnly, + is_root, + count_share_views, +}: BreadcrumbButtonProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const [loading, setLoading] = useState(false); + const [shareInfo, setShareInfo] = useState( + undefined, + ); + + const uri = useMemo(() => { + return new CrUri(path); + }, [path]); + + useEffect(() => { + if (uri.is_root() && uri.fs() == Filesystem.share) { + setLoading(true); + dispatch(queueLoadShareInfo(uri, count_share_views)) + .then((info) => { + setShareInfo(info); + }) + .catch((_e) => { + setShareInfo(null); + }) + .finally(() => { + setLoading(false); + }); + } else { + setShareInfo(undefined); + } + }, [uri]); + + const onClick = useCallback( + (e?: React.MouseEvent) => { + e && e.stopPropagation(); + if (is_latest && !displayOnly && !is_root && e) { + dispatch(openContextUrlFromUri(fmIndex, path, e)); + return; + } + dispatch(navigateToPath(fmIndex, path, undefined, displayOnly)); + }, + [dispatch, fmIndex, path, is_latest, displayOnly, is_root], + ); + + const displayName = useMemo(() => { + if (uri.is_root()) { + switch (uri.fs()) { + case Filesystem.my: + if (uri.is_search()) { + const searchCategory = uri.query(UriQuery.category); + if (searchCategory.length == 1) { + switch (searchCategory[0]) { + case UriSearchCategory.image: + return t("navbar.photos"); + case UriSearchCategory.video: + return t("navbar.videos"); + case UriSearchCategory.audio: + return t("navbar.music"); + case UriSearchCategory.document: + return t("navbar.documents"); + } + } + } + if (uri.id()) { + const current = SessionManager.currentLoginOrNull(); + if (!current || current.user.id != uri.id()) { + return t("navbar.hisFiles"); + } + } + return t("navbar.myFiles"); + case Filesystem.trash: + return t("navbar.trash"); + case Filesystem.shared_by_me: + return t("navbar.myShare"); + case Filesystem.shared_with_me: + return t("navbar.sharedWithMe"); + case Filesystem.share: + if (shareInfo) { + return shareInfo.name + ? shareInfo.name + : t("application:share.somebodyShare", { + name: shareInfo.owner.nickname, + }); + } else if (shareInfo === null) { + return t("application:share.expiredLink"); + } + + return ""; + default: + "Root"; + } + } + + return name; + }, [name, uri, t, shareInfo]); + + const startIcon = useMemo(() => { + if (uri.is_root()) { + switch (uri.fs()) { + case Filesystem.my: + if (uri.is_search()) { + const searchCategory = uri.query(UriQuery.category); + if (searchCategory.length == 1) { + switch (searchCategory[0]) { + case UriSearchCategory.image: + return { Icons: [Image, ImageOutlined] }; + case UriSearchCategory.video: + return { Icons: [Video, VideoOutlined] }; + case UriSearchCategory.audio: + return { Icons: [MusicNote1, MusicNote1Outlined] }; + case UriSearchCategory.document: + return { Icons: [DocumentText, DocumentTextOutlined] }; + } + } + } + const uid = uri.id(); + if (uid) { + const current = SessionManager.currentLoginOrNull(); + if (!current || current.user.id != uri.id()) { + return { + Element: (props: { [key: string]: any }) => ( + + ), + }; + } + } + return { Icons: [Home, HomeOutlined] }; + case Filesystem.trash: + return { Icons: [Delete, DeleteOutlined] }; + case Filesystem.shared_with_me: + return { Icons: [PeopleTeam, PeopleTeamOutlined] }; + case Filesystem.share: + if (shareInfo) { + return { + Element: (props: { [key: string]: any }) => ( + + ), + }; + } else if (shareInfo === null) { + return { Icons: [LinkDismiss, LinkDismiss] }; + } + return undefined; + default: + return undefined; + } + } + }, [uri, shareInfo]); + + return [loading, displayName, startIcon, onClick, shareInfo] as const; +}; + +const BreadcrumbButton = ({ + name, + is_root, + is_latest, + path, + displayOnly, + ...rest +}: BreadcrumbButtonProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + + const [loading, displayName, startIcon, onClick, shareInfo] = + useBreadcrumbButtons({ + name, + is_latest, + path, + displayOnly, + is_root, + count_share_views: true, + }); + const maxWidth = is_latest ? "300px" : is_root ? "initial" : "100px"; + const StartIcon = useMemo(() => { + if (loading) { + return ; + } + if (startIcon?.Icons?.[0]) { + const Icon = startIcon?.Icons?.[0]; + return ; + } + if (startIcon?.Element) { + return startIcon.Element({ sx: { ml: 0.5, width: 20, height: 20 } }); + } + return null; + }, [startIcon, loading]); + + const [drag, drop, isOver, isDragging] = useFileDrag({ + dropUri: path, + }); + + const popupState = usePopupState({ + variant: "popover", + popupId: "shareInfo", + }); + + return ( + <> + {is_latest && !displayOnly && fmIndex == FileManagerIndex.main && ( + + )} + + theme.transitions.create( + ["max-width", "color", "background-color"], + { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.standard, + }, + ), + color: (theme) => + is_latest && !displayOnly + ? theme.palette.text.primary + : theme.palette.text.secondary, + maxWidth: maxWidth, + }} + startIcon={StartIcon} + endIcon={ + is_latest && !is_root && !displayOnly ? ( + + ) : undefined + } + ref={drop} + {...(shareInfo ? bindHover(popupState) : {})} + {...rest} + > + {loading && } + {!loading && ( + + {displayName} + + )} + + {shareInfo && displayName && ( + + )} + + ); +}; + +export default BreadcrumbButton; diff --git a/src/component/FileManager/TopBar/BreadcrumbHiddenItem.tsx b/src/component/FileManager/TopBar/BreadcrumbHiddenItem.tsx new file mode 100644 index 0000000..ac915e7 --- /dev/null +++ b/src/component/FileManager/TopBar/BreadcrumbHiddenItem.tsx @@ -0,0 +1,101 @@ +import { + BreadcrumbButtonProps, + useBreadcrumbButtons, +} from "./BreadcrumbButton.tsx"; +import { + ListItemIcon, + ListItemText, + MenuItem, + Skeleton, + styled, +} from "@mui/material"; +import { useContext, useMemo } from "react"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import FileIcon from "../Explorer/FileIcon.tsx"; +import { useFileDrag } from "../Dnd/DndWrappedFile.tsx"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; + +export interface BreadcrumbHiddenItem extends BreadcrumbButtonProps { + onClose: () => void; +} + +export const StyledMenuItem = styled(MenuItem)<{ isDropOver?: boolean }>( + ({ theme, isDropOver }) => ({ + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms !important", + transitionProperty: "background-color,opacity,box-shadow", + boxShadow: isDropOver + ? `inset 0 0 0 2px ${theme.palette.primary.light}` + : "none", + }), +); + +const BreadcrumbHiddenItem = ({ + name, + is_root, + is_latest, + path, + onClose, + ...rest +}: BreadcrumbHiddenItem) => { + const [loading, displayName, startIcon, onClick] = useBreadcrumbButtons({ + name, + is_latest, + path, + count_share_views: true, + }); + const onItemClick = async () => { + onClose(); + onClick && onClick(); + }; + + const fmIndex = useContext(FmIndexContext); + const file = useAppSelector((s) => s.fileManager[fmIndex].tree[path]?.file); + + const StartIcon = useMemo(() => { + if (loading) { + return ; + } + if (startIcon?.Icons?.[0]) { + const Icon = startIcon?.Icons?.[0]; + return ; + } + if (startIcon?.Element) { + return startIcon.Element({ sx: { width: 20, height: 20 } }); + } + }, [startIcon, loading]); + + const [drag, drop, isOver, isDragging] = useFileDrag({ + dropUri: path, + }); + + return ( + + + {StartIcon ? ( + StartIcon + ) : ( + + )} + + + {loading ? : displayName} + + + ); +}; + +export default BreadcrumbHiddenItem; diff --git a/src/component/FileManager/TopBar/MoreActionMenu.tsx b/src/component/FileManager/TopBar/MoreActionMenu.tsx new file mode 100644 index 0000000..92b633c --- /dev/null +++ b/src/component/FileManager/TopBar/MoreActionMenu.tsx @@ -0,0 +1,150 @@ +import { ListItemIcon, ListItemText, MenuProps, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useCallback, useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { clearSelected } from "../../../redux/fileManagerSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { createShareShortcut, isMacbook } from "../../../redux/thunks/file.ts"; +import { inverseSelection, pinCurrentView, refreshFileList, selectAll } from "../../../redux/thunks/filemanager.ts"; +import SessionManager from "../../../session"; +import { Filesystem } from "../../../util/uri.ts"; +import { KeyIndicator } from "../../Frame/NavBar/SearchBar.tsx"; +import ArrowSync from "../../Icons/ArrowSync.tsx"; +import Border from "../../Icons/Border.tsx"; +import BorderAll from "../../Icons/BorderAll.tsx"; +import BorderInside from "../../Icons/BorderInside.tsx"; +import FolderLink from "../../Icons/FolderLink.tsx"; +import PinOutlined from "../../Icons/PinOutlined.tsx"; +import StorageOutlined from "../../Icons/StorageOutlined.tsx"; +import { DenseDivider, SquareMenu, SquareMenuItem } from "../ContextMenu/ContextMenu.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +const MoreActionMenu = ({ onClose, ...rest }: MenuProps) => { + const { t } = useTranslation(); + const fmIndex = useContext(FmIndexContext); + const fs = useAppSelector((state) => state.fileManager[fmIndex].current_fs); + const base = useAppSelector((state) => state.fileManager[fmIndex].path_root); + const isSingleFile = useAppSelector((state) => state.fileManager[fmIndex].list?.single_file_view); + const files = useAppSelector((state) => state.fileManager[fmIndex].list?.files); + const dispatch = useAppDispatch(); + const isLogin = !!SessionManager.currentLoginOrNull(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + const mountPopupState = usePopupState({ + variant: "popover", + popupId: "mount", + }); + + const onPinClicked = useCallback(() => { + dispatch(pinCurrentView(fmIndex)); + onClose && onClose({}, "escapeKeyDown"); + }, [dispatch, onClose, fmIndex]); + + const onCreateShortcutClicked = useCallback(() => { + if (isSingleFile && files && files.length > 0) { + dispatch(createShareShortcut(files[0].path)); + } else if (base) { + dispatch(createShareShortcut(base)); + } + onClose && onClose({}, "escapeKeyDown"); + }, [dispatch, onClose, base, isSingleFile, files]); + + const onSelectAllClicked = useCallback(() => { + onClose && onClose({}, "escapeKeyDown"); + dispatch(selectAll(fmIndex)); + }, [dispatch, onClose, fmIndex]); + + const onSelectNoneClicked = useCallback(() => { + onClose && onClose({}, "escapeKeyDown"); + dispatch(clearSelected({ index: fmIndex, value: undefined })); + }, [dispatch, onClose, fmIndex]); + + const onInverseSelectionClicked = useCallback(() => { + onClose && onClose({}, "escapeKeyDown"); + dispatch(inverseSelection(fmIndex)); + }, [dispatch, onClose, fmIndex]); + + const onRefreshClicked = useCallback(() => { + onClose && onClose({}, "escapeKeyDown"); + dispatch(refreshFileList(fmIndex)); + }, [dispatch, onClose, fmIndex]); + + return ( + + {isMobile && ( + <> + + + + + {t("application:fileManager.refresh")} + + {fmIndex == FileManagerIndex.main && ( + + + + + {t("application:vas.switchFolderPolicy")} + + )} + + )} + {isLogin && ( + + + + + {t("application:fileManager.pin")} + + )} + {isLogin && fs == Filesystem.share && ( + + + + + {t("application:fileManager.saveShortcut")} + + )} + {isLogin && } + + + + + {t("application:fileManager.selectAll")} + + {isMacbook ? "⌘" : "Crtl"}+A + + + + + + + {t("application:fileManager.selectNone")} + + + + + + {t("application:fileManager.invertSelection")} + + + ); +}; + +export default MoreActionMenu; diff --git a/src/component/FileManager/TopBar/NavHeader.tsx b/src/component/FileManager/TopBar/NavHeader.tsx new file mode 100644 index 0000000..a86bf58 --- /dev/null +++ b/src/component/FileManager/TopBar/NavHeader.tsx @@ -0,0 +1,43 @@ +import { Stack, useMediaQuery, useTheme } from "@mui/material"; +import Breadcrumb from "./Breadcrumb.tsx"; +import TopActions from "./TopActions.tsx"; +import { RadiusFrame } from "../../Frame/RadiusFrame.tsx"; +import TopActionsSecondary from "./TopActionsSecondary.tsx"; +import { SearchIndicator } from "../Search/SearchIndicator.tsx"; + +const NavHeader = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + return ( + + + + + + {!isMobile && ( + + + + )} + + + + + ); +}; + +export default NavHeader; diff --git a/src/component/FileManager/TopBar/ShareInfoPopover.tsx b/src/component/FileManager/TopBar/ShareInfoPopover.tsx new file mode 100644 index 0000000..4a32cc7 --- /dev/null +++ b/src/component/FileManager/TopBar/ShareInfoPopover.tsx @@ -0,0 +1,127 @@ +import { Box, Divider, PopoverProps, Stack, styled, Typography } from "@mui/material"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import { useCallback } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Share } from "../../../api/explorer.ts"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import UserBadge from "../../Common/User/UserBadge.tsx"; +import Eye from "../../Icons/Eye.tsx"; +import Timer from "../../Icons/Timer.tsx"; + +interface ShareInfoPopoverProps extends PopoverProps { + displayName: string; + shareInfo: Share; +} + +export const PropTypography = styled(Typography)({ + display: "flex", + alignItems: "center", + gap: 8, +}); + +export const separator = " • "; + +export interface ShareExpiresProps { + expires?: string; + remain_downloads?: number; +} +export const ShareExpires = ({ expires, remain_downloads }: ShareExpiresProps) => { + const { t } = useTranslation(); + return ( + <> + {expires && ( + + ]} + /> + + )} + {expires && remain_downloads != undefined && separator} + {remain_downloads != undefined && + t("application:share.expireAfterDownloads", { + downloads: remain_downloads, + })} + + ); +}; + +export const ShareStatistics = ({ shareInfo }: { shareInfo: Share }) => { + const { t } = useTranslation(); + return ( + <> + {t("application:share.statisticsViews", { + views: shareInfo.visited, + })} + {shareInfo.downloaded && + separator + + t("application:share.statisticsDownloads", { + downloads: shareInfo.downloaded, + })} + + ); +}; + +const ShareInfoPopover = ({ displayName, shareInfo, ...rest }: ShareInfoPopoverProps) => { + const { t } = useTranslation(); + const stopPropagation = useCallback((e: any) => e.stopPropagation(), []); + return ( + + + + {displayName} + + + {(shareInfo.remain_downloads || shareInfo.expires) && ( + + + + + )} + {shareInfo.visited > 0 && ( + + + + + )} + + + + + {shareInfo.created_at && ( + + )} + + + + ); +}; + +export default ShareInfoPopover; diff --git a/src/component/FileManager/TopBar/SortMethodMenu.tsx b/src/component/FileManager/TopBar/SortMethodMenu.tsx new file mode 100644 index 0000000..0776aeb --- /dev/null +++ b/src/component/FileManager/TopBar/SortMethodMenu.tsx @@ -0,0 +1,146 @@ +import { + ListItemIcon, + ListItemText, + Menu, + MenuItem, + MenuProps, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import React, { useContext, useMemo } from "react"; +import Checkmark from "../../Icons/Checkmark.tsx"; +import { changeSortOption } from "../../../redux/thunks/filemanager.ts"; +import SessionManager, { UserSettings } from "../../../session"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; + +interface sortOption { + label: string; + order_by: string; + order_direction: string; + selected?: boolean; +} + +const supportOption: { + [key: string]: sortOption; +} = { + name_asc: { + label: "application:fileManager.sortMethods.A-Z", + order_by: "name", + order_direction: "asc", + }, + name_desc: { + label: "application:fileManager.sortMethods.Z-A", + order_by: "name", + order_direction: "desc", + }, + size_asc: { + label: "application:fileManager.sortMethods.smallest", + order_by: "size", + order_direction: "asc", + }, + size_desc: { + label: "application:fileManager.sortMethods.largest", + order_by: "size", + order_direction: "desc", + }, + updated_at_asc: { + label: "application:fileManager.sortMethods.oldestModified", + order_by: "updated_at", + order_direction: "asc", + }, + updated_at_desc: { + label: "application:fileManager.sortMethods.newestModified", + order_by: "updated_at", + order_direction: "desc", + }, + created_at_asc: { + label: "application:fileManager.sortMethods.oldestUploaded", + order_by: "created_at", + order_direction: "asc", + }, + created_at_desc: { + label: "application:fileManager.sortMethods.newestUploaded", + order_by: "created_at", + order_direction: "desc", + }, + _asc: { + label: "application:fileManager.sortMethods.oldestUploaded", + order_by: "created_at", + order_direction: "asc", + }, + _desc: { + label: "application:fileManager.sortMethods.oldestUploaded", + order_by: "created_at", + order_direction: "asc", + }, +}; + +const SortMethodMenu = ({ onClose, ...rest }: MenuProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const orderMethodOptions = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.order_by_options, + ); + const orderDirectionOption = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.order_direction_options, + ); + const sortBy = useAppSelector((state) => state.fileManager[fmIndex].sortBy); + const sortDirection = useAppSelector( + (state) => state.fileManager[fmIndex].sortDirection, + ); + + const options = useMemo(() => { + if (!orderMethodOptions || !orderDirectionOption) return []; + const res: sortOption[] = []; + const selectedVal = + !sortBy || !sortDirection + ? "created_at_asc" + : `${sortBy}_${sortDirection}`; + orderMethodOptions.forEach((method) => { + orderDirectionOption.forEach((direction) => { + const key = `${method}_${direction}`; + if (supportOption[key]) { + res.push({ ...supportOption[key], selected: key == selectedVal }); + } + }); + }); + return res; + }, [orderMethodOptions, orderDirectionOption, sortBy, sortDirection]); + + const selectOption = (option: sortOption) => { + dispatch( + changeSortOption(fmIndex, option.order_by, option.order_direction), + ); + SessionManager.set(UserSettings.SortBy, option.order_by); + SessionManager.set(UserSettings.SortDirection, option.order_direction); + onClose && onClose({}, "escapeKeyDown"); + }; + + return ( + + {options.map((option) => ( + selectOption(option)} + > + {!option.selected && ( + {t(option.label)} + )} + {option.selected && ( + <> + + + + {t(option.label)} + + )} + + ))} + + ); +}; + +export default SortMethodMenu; diff --git a/src/component/FileManager/TopBar/TopActions.tsx b/src/component/FileManager/TopBar/TopActions.tsx new file mode 100644 index 0000000..e3cd5a8 --- /dev/null +++ b/src/component/FileManager/TopBar/TopActions.tsx @@ -0,0 +1,103 @@ +import { + Button, + ButtonGroup, + styled, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover } from "material-ui-popup-state"; +import { + bindMenu, + bindTrigger, + usePopupState, +} from "material-ui-popup-state/hooks"; +import { useContext } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import ArrowSort from "../../Icons/ArrowSort.tsx"; +import TableSettingsOutlined from "../../Icons/TableSettings.tsx"; +import SortMethodMenu from "./SortMethodMenu.tsx"; +import ViewOptionPopover from "./ViewOptionPopover.tsx"; + +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import MoreActionMenu from "./MoreActionMenu.tsx"; + +export const ActionButton = styled(Button)(({ theme }) => ({ + border: `1px solid ${theme.palette.divider}`, + color: theme.palette.text.primary, +})); + +export const ActionButtonGroup = styled(ButtonGroup)(({ theme }) => ({ + "& .MuiButtonGroup-firstButton, .MuiButtonGroup-middleButton, .MuiButtonGroup-lastButton": { + "&:hover": { + "border-color": theme.palette.primary.main, + }, + }, + height: "100%", +})); + +const TopActions = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const fmIndex = useContext(FmIndexContext); + const sortOptions = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.order_by_options, + ); + const isSingleFileView = useAppSelector( + (state) => state.fileManager[fmIndex].list?.single_file_view, + ); + const viewPopupState = usePopupState({ + variant: "popover", + popupId: "viewOption", + }); + const sortPopupState = usePopupState({ + variant: "popover", + popupId: "sortOption", + }); + const morePopupState = usePopupState({ + variant: "popover", + popupId: "moreActions", + }); + return ( + <> + + } + > + {isMobile ? ( + + ) : ( + t("application:fileManager.view") + )} + + {(!(!sortOptions || isSingleFileView) || !isMobile) && ( + } + {...bindTrigger(sortPopupState)} + > + {isMobile ? ( + + ) : ( + t("application:fileManager.sortMethod") + )} + + )} + {isMobile && ( + + + + )} + + {isMobile && } + + + + ); +}; + +export default TopActions; diff --git a/src/component/FileManager/TopBar/TopActionsSecondary.tsx b/src/component/FileManager/TopBar/TopActionsSecondary.tsx new file mode 100644 index 0000000..acb67c5 --- /dev/null +++ b/src/component/FileManager/TopBar/TopActionsSecondary.tsx @@ -0,0 +1,65 @@ +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { ActionButton, ActionButtonGroup } from "./TopActions.tsx"; +import ArrowSync from "../../Icons/ArrowSync.tsx"; +import { styled, Tooltip } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useContext, useState } from "react"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import { refreshFileList } from "../../../redux/thunks/filemanager.ts"; +import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import MoreActionMenu from "./MoreActionMenu.tsx"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +const SpinArrowSync = styled(ArrowSync)(() => ({ + "@keyframes spin": { + from: { + transform: "rotate(0deg)", + }, + to: { + transform: "rotate(360deg)", + }, + }, +})); + +const TopActionsSecondary = () => { + const { t } = useTranslation(); + const fmIndex = useContext(FmIndexContext); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const morePopupState = usePopupState({ + variant: "popover", + popupId: "moreActions", + }); + const refresh = async () => { + setLoading(true); + await dispatch(refreshFileList(fmIndex)); + setLoading(false); + }; + return ( + <> + + + refresh()}> + + + + {fmIndex == FileManagerIndex.main && ( + + + + )} + + + + ); +}; + +export default TopActionsSecondary; diff --git a/src/component/FileManager/TopBar/ViewOptionPopover.tsx b/src/component/FileManager/TopBar/ViewOptionPopover.tsx new file mode 100644 index 0000000..41001be --- /dev/null +++ b/src/component/FileManager/TopBar/ViewOptionPopover.tsx @@ -0,0 +1,304 @@ +import { + Box, + Collapse, + Popover, + PopoverProps, + Slider, + SvgIconProps, + ToggleButton, + ToggleButtonGroup, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import AppsListOutlined from "../../Icons/AppsListOutlined.tsx"; +import GridOutlined from "../../Icons/GridOutlined.tsx"; +import Grid from "../../Icons/Grid.tsx"; +import AppsList from "../../Icons/AppsList.tsx"; +import NavIconTransition from "../../Frame/NavBar/NavIconTransition.tsx"; +import React, { useContext } from "react"; +import SessionManager, { UserSettings } from "../../../session"; +import { + Layouts, + setGalleryWidth, + setLayout, + setShowThumb, +} from "../../../redux/fileManagerSlice.ts"; +import ImageOutlined from "../../Icons/ImageOutlined.tsx"; +import ImageOffOutlined from "../../Icons/ImageOffOutlined.tsx"; +import { changePageSize } from "../../../redux/thunks/filemanager.ts"; +import ImageCopy from "../../Icons/ImageCopy.tsx"; +import ImageCopyOutlined from "../../Icons/ImageCopyOutlined.tsx"; + +import { FmIndexContext } from "../FmIndexContext.tsx"; +import Setting from "../../Icons/Setting.tsx"; +import { setListViewColumnSettingDialog } from "../../../redux/globalStateSlice.ts"; + +const layoutOptions: { + label: string; + value: string; + icon: ((props: SvgIconProps) => JSX.Element)[]; +}[] = [ + { + label: "application:fileManager.gridView", + value: "grid", + icon: [Grid, GridOutlined], + }, + { + label: "application:fileManager.listView", + value: "list", + icon: [AppsList, AppsListOutlined], + }, + { + label: "application:fileManager.galleryView", + value: "gallery", + icon: [ImageCopy, ImageCopyOutlined], + }, +]; + +const thumbOptions: { + label: string; + value: boolean; + icon: (props: SvgIconProps) => JSX.Element; +}[] = [ + { + label: "application:fileManager.on", + value: true, + icon: ImageOutlined, + }, + { + label: "application:fileManager.off", + value: false, + icon: ImageOffOutlined, + }, +]; + +export const MinPageSize = 50; + +const ViewOptionPopover = ({ ...rest }: PopoverProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const layout = useAppSelector((state) => state.fileManager[fmIndex].layout); + const showThumb = useAppSelector( + (state) => state.fileManager[fmIndex].showThumb, + ); + const pageSize = useAppSelector( + (state) => state.fileManager[fmIndex].pageSize, + ); + const pageSizeMax = useAppSelector( + (state) => state.fileManager[fmIndex].list?.props.max_page_size, + ); + const galleryWidth = useAppSelector( + (state) => state.fileManager[fmIndex].galleryWidth, + ); + const [desiredPageSize, setDesiredPageSize] = React.useState(pageSize); + const pageSizeMaxSafe = pageSizeMax ?? desiredPageSize; + const step = pageSizeMaxSafe - MinPageSize <= 100 ? 1 : 10; + const [desiredImageWidth, setDesiredImageWidth] = + React.useState(galleryWidth); + + const handleLayoutChange = ( + _event: React.MouseEvent, + newMode: string, + ) => { + if (newMode) { + dispatch(setLayout({ index: fmIndex, value: newMode })); + SessionManager.set(UserSettings.Layout, newMode); + } + }; + + const handleThumbChange = ( + _event: React.MouseEvent, + newMode: boolean, + ) => { + dispatch(setShowThumb({ index: fmIndex, value: newMode })); + SessionManager.set(UserSettings.ShowThumb, newMode); + }; + + const handlePageSlideChange = ( + _event: Event, + newValue: number | number[], + ) => { + setDesiredPageSize(newValue as number); + }; + + const commitPageSize = ( + _event: React.SyntheticEvent | Event, + newValue: number | number[], + ) => { + const pageSize = Math.max(MinPageSize, newValue as number); + SessionManager.set(UserSettings.PageSize, pageSize); + dispatch(changePageSize(fmIndex, pageSize)); + }; + + const handleImageSizeChange = ( + _event: Event, + newValue: number | number[], + ) => { + setDesiredImageWidth(newValue as number); + }; + + const commitImageSize = ( + _event: React.SyntheticEvent | Event, + newValue: number | number[], + ) => { + SessionManager.set(UserSettings.GalleryWidth, newValue as number); + dispatch(setGalleryWidth({ index: fmIndex, value: newValue as number })); + }; + + return ( + + + + + {t("application:fileManager.layout")} + + + {layoutOptions.map((option) => ( + + + {t(option.label)} + + ))} + + + + + {t("application:fileManager.thumbnails")} + + + {thumbOptions.map((option) => ( + + + {t(option.label)} + + ))} + + + + + {t("application:fileManager.listColumnSetting")} + + + dispatch(setListViewColumnSettingDialog(true))} + > + + {t("application:fileManager.listColumnSetting")} + + + + + + {t("application:fileManager.imageSize")} + + + + + + 50 + + + 500 + + + + + + {t("application:fileManager.paginationSize")} + + + + + + {MinPageSize} + + + {pageSizeMaxSafe} + + + + + + ); +}; + +export default ViewOptionPopover; diff --git a/src/component/FileManager/TreeView/Pinned.tsx b/src/component/FileManager/TreeView/Pinned.tsx new file mode 100644 index 0000000..86a14d3 --- /dev/null +++ b/src/component/FileManager/TreeView/Pinned.tsx @@ -0,0 +1,72 @@ +import React, { memo, useContext, useMemo } from "react"; +import SessionManager from "../../../session"; +import TreeFiles from "./TreeFiles.tsx"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +interface PinnedItem { + uri: string; + name?: string; + crUri: CrUri; + useElements?: boolean; +} + +export const usePinned = () => { + const fmIndex = useContext(FmIndexContext); + const generation = useAppSelector( + (state) => state.globalState.pinedGeneration, + ); + const path_root = useAppSelector( + (state) => state.fileManager[fmIndex].path_root, + ); + const pined = useMemo(() => { + try { + return SessionManager.currentLogin().user.pined?.map((p): PinnedItem => { + return { + uri: p.uri, + name: p.name, + crUri: new CrUri(p.uri), + useElements: p.uri == path_root, + }; + }); + } catch (e) {} + }, [generation, path_root]); + + return pined; +}; + +const Pinned = memo(() => { + const fmIndex = useContext(FmIndexContext); + const elements = useAppSelector( + (state) => state.fileManager[fmIndex].path_elements, + ); + const pined = usePinned(); + + return ( + <> + {pined?.map((p, index) => ( + + ))} + + ); +}); +export default Pinned; diff --git a/src/component/FileManager/TreeView/TreeFile.tsx b/src/component/FileManager/TreeView/TreeFile.tsx new file mode 100644 index 0000000..9890e74 --- /dev/null +++ b/src/component/FileManager/TreeView/TreeFile.tsx @@ -0,0 +1,365 @@ +import React, { useCallback, useContext, useMemo, useState } from "react"; + +import { Fade, IconButton, Skeleton, styled, Tooltip } from "@mui/material"; +import { SideNavItemBase } from "../../Frame/NavBar/SideNavItem.tsx"; +import clsx from "clsx"; +import FileIcon from "../Explorer/FileIcon.tsx"; +import { FileResponse } from "../../../api/explorer.ts"; +import { + TreeItem, + treeItemClasses, + TreeItemContentProps, + TreeItemProps, + useTreeItem, +} from "@mui/x-tree-view"; +import NavIconTransition from "../../Frame/NavBar/NavIconTransition.tsx"; +import { mergeRefs } from "../../../util"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { + loadChild, + navigateToPath, +} from "../../../redux/thunks/filemanager.ts"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import CaretDown from "../../Icons/CaretDown.tsx"; +import { StartIcon } from "../TopBar/BreadcrumbButton.tsx"; +import { openFileContextMenu } from "../../../redux/thunks/file.ts"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import { useTranslation } from "react-i18next"; +import { unPinFromSidebar } from "../../../redux/thunks/settings.ts"; +import { pinedPrefix } from "./TreeFiles.tsx"; +import { useFileDrag } from "../Dnd/DndWrappedFile.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import { NoWrapTypography } from "../../Common/StyledComponents.tsx"; + +const CustomContentRoot = styled(SideNavItemBase)<{ + isDragging?: boolean; + isDropOver?: boolean; +}>(({ theme, isDragging, isDropOver }) => ({ + "& .MuiTreeItem-iconContainer": { + marginLeft: theme.spacing(1), + }, + opacity: isDragging ? 0.5 : 1, + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + transitionProperty: "opacity,box-shadow,background-color", + boxShadow: isDropOver + ? `inset 0 0 0 2px ${theme.palette.primary.light}` + : "none", + height: "32px", +})); + +const StyledTreeItemRoot = styled(TreeItem)(() => ({ + [`& .${treeItemClasses.group}`]: { + marginLeft: 0, + [`& .${treeItemClasses.content}`]: { + marginLeft: 0, + }, + }, +})) as unknown as typeof TreeItem; + +export const CaretDownIcon = styled(CaretDown)<{ expanded: boolean }>( + ({ theme, expanded }) => ({ + fontSize: "12px!important", + transform: `rotate(${expanded ? 0 : -90}deg)`, + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeInOut, + }), + }), +); + +export interface CustomContentProps extends TreeItemContentProps { + parent?: string; + level?: number; + notLoaded?: boolean; + file?: FileResponse; + fileIcon?: StartIcon; + loading?: boolean; + pinned?: boolean; + canDrop?: boolean; +} + +export interface TreeFileProps extends TreeItemProps { + parent?: string; + level?: number; + notLoaded?: boolean; + file?: FileResponse; + fileIcon?: StartIcon; + loading?: boolean; + pinned?: boolean; + canDrop?: boolean; +} + +const SmallIconButton = styled(IconButton)(() => ({ + fontSize: "0.8rem", +})); + +interface UnpinButton { + show: boolean; + uri: string; +} +const UnpinButton = (props: UnpinButton) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); + const onClick = useCallback( + async (e: React.MouseEvent) => { + e.stopPropagation(); + setLoading(true); + try { + await dispatch(unPinFromSidebar(props.uri)); + } catch (e) { + setLoading(false); + } + }, + [setLoading], + ); + + return ( + + + e.stopPropagation()} + onClick={onClick} + size="small" + > + + + + + ); +}; + +const CustomContent = React.memo( + React.forwardRef(function CustomContent(props: CustomContentProps, ref) { + const dispatch = useAppDispatch(); + const fmIndex = useContext(FmIndexContext); + const [loading, setLoading] = useState(false); + const [showDelete, setShowDelete] = useState(false); + const { + classes, + className, + label, + nodeId, + icon: iconProp, + expansionIcon, + displayIcon, + file, + fileIcon, + } = props; + + const { + disabled, + expanded, + selected, + focused, + handleExpansion, + handleSelection, + preventSelection, + } = useTreeItem(nodeId); + + const uri = useMemo(() => { + // Trim 'pinedPrefix' if exist in prefix + if (nodeId.startsWith(pinedPrefix)) { + return nodeId.substring(pinedPrefix.length); + } + + return nodeId; + }, [nodeId]); + + const icon = iconProp || expansionIcon || displayIcon; + + const handleMouseDown = ( + event: React.MouseEvent, + ) => { + preventSelection(event); + }; + + const handleExpansionClick = useCallback( + async (event: React.MouseEvent) => { + event.stopPropagation(); + let timeOutID: NodeJS.Timeout | undefined; + handleExpansion(event); + if (!expanded) { + try { + await dispatch( + loadChild( + fmIndex, + uri, + () => (timeOutID = setTimeout(() => setLoading(true), 300)), + ), + ); + } finally { + if (timeOutID) { + clearTimeout(timeOutID); + } + setLoading(false); + } + } + }, + [handleExpansion, setLoading, dispatch, uri], + ); + + const handleSelectionClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + handleSelection(event); + dispatch(navigateToPath(fmIndex, uri, file)); + }, + [dispatch, handleSelection, fmIndex, uri], + ); + + const FileItemIcon = useMemo(() => { + if (props.loading) { + return ( + + ); + } + if (fileIcon && fileIcon.Icons) { + return ( + + ); + } + if (fileIcon && fileIcon.Element) { + return ; + } + return ( + + ); + }, [file, fileIcon, selected, props.notLoaded, props.loading]); + + const onContextMenu = useCallback( + (e: React.MouseEvent) => { + if (!file || file.name === "") { + return; + } + dispatch(openFileContextMenu(fmIndex, file, true, e)); + }, + [file, dispatch, fmIndex], + ); + + const fileName = useMemo( + () => ( + + + {!props.loading && label} + {props.loading && } + + + ), + [label, handleSelectionClick, props.loading], + ); + + const onMouseEnter = useCallback(() => { + if (props.pinned) setShowDelete(true); + }, [setShowDelete, props.pinned]); + + const onMouseLeave = useCallback(() => { + if (props.pinned) setShowDelete(false); + }, [setShowDelete, props.pinned]); + + const [drag, drop, isOver, isDragging] = useFileDrag({ + file, + dropUri: props.canDrop ? nodeId : undefined, + }); + + const mergedRef = useCallback( + (val: any) => { + mergeRefs(ref as React.Ref, drop, drag)(val); + }, + [ref, drop, drag], + ); + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + `${theme.spacing((props.level ?? 0) * 2)}!important`, + }} + onClick={handleSelectionClick} + ref={mergedRef} + > + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
+ {icon && !loading && } + {icon && loading && ( + + )} +
+ {FileItemIcon} + {fileName} + +
+ ); + }), +); + +const TreeFile = React.memo( + React.forwardRef(function CustomTreeItem( + props: TreeFileProps, + ref: React.Ref, + ) { + const contentProps = useMemo(() => { + const { level, file, notLoaded, fileIcon, loading, pinned, canDrop } = + props; + return { level, file, notLoaded, fileIcon, loading, pinned, canDrop }; + }, [ + props.level, + props.file, + props.notLoaded, + props.fileIcon, + props.loading, + props.canDrop, + props.pinned, + ]); + return ( + + ); + }), +); + +export default TreeFile; diff --git a/src/component/FileManager/TreeView/TreeFiles.tsx b/src/component/FileManager/TreeView/TreeFiles.tsx new file mode 100644 index 0000000..c65a8ba --- /dev/null +++ b/src/component/FileManager/TreeView/TreeFiles.tsx @@ -0,0 +1,165 @@ +import React, { useContext, useMemo } from "react"; +import TreeFile from "./TreeFile.tsx"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { useTranslation } from "react-i18next"; +import { useBreadcrumbButtons } from "../TopBar/BreadcrumbButton.tsx"; +import path from "path-browserify"; +import { Box } from "@mui/material"; +import SideNavItem from "../../Frame/NavBar/SideNavItem.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; + +export interface TreeFilesProps { + path: string; + elements?: string[]; + notLoaded?: boolean; + level: number; + flatten?: boolean; + labelOverwrite?: string; + pinned?: boolean; + canDrop?: boolean; + [key: string]: any; +} + +export const pinedPrefix = "Pined"; + +const TreeFiles = React.memo( + React.forwardRef( + ( + { + path: p, + level, + elements, + labelOverwrite, + notLoaded, + pinned, + flatten, + canDrop, + ...rest + }: TreeFilesProps, + ref: React.Ref, + ) => { + const { t } = useTranslation(); + const fmIndex = useContext(FmIndexContext); + const parentsCache = useAppSelector( + (state) => state.fileManager[fmIndex].tree[p], + ); + const [limit, setLimit] = React.useState(50); + const uri = useMemo(() => new CrUri(p), [p]); + const nodeId = useMemo(() => { + if (pinned && flatten) { + return pinedPrefix + p; + } + + return p; + }, [pinned, p, flatten]); + const [loading, displayName, startIcon, onClick] = useBreadcrumbButtons({ + name: + parentsCache && parentsCache.file + ? parentsCache.file.name + : path.basename(uri.path()), + is_latest: false, + path: p, + }); + + const childTreeFiles = useMemo(() => { + var res: TreeFilesProps[] = []; + + let newParent: string | null = null; + let elementPushed = false; + // Add current if loaded children exist + if (elements && elements.length >= 1) { + newParent = new CrUri(p).join(elements[0]).toString(); + } + + // load from store cache + let currentIndex = 0; + if (parentsCache && parentsCache.children) { + parentsCache.children.forEach((child) => { + let childElements: string[] | undefined = undefined; + if (newParent && newParent == child) { + childElements = elements?.slice(1); + elementPushed = true; + currentIndex = res.length; + } + res.push({ + level: level + 1, + path: child, + elements: childElements, + }); + }); + } + + if (elements && elements.length >= 1 && !elementPushed) { + const childElements = elements.slice(1); + currentIndex = res.length; + res.push({ + level: level + 1, + path: newParent ?? "", + elements: childElements, + notLoaded: childElements.length > 0, + }); + } + + if (currentIndex >= limit) { + [res[currentIndex], res[0]] = [res[0], res[currentIndex]]; + } + + return res; + }, [p, elements, parentsCache, limit]); + + const shadowChild = useMemo(() => { + if ( + flatten || + (parentsCache?.children && parentsCache.children.length == 0) + ) { + return null; + } + return ; + }, [parentsCache, flatten]); + + return ( + <> + + {!flatten && childTreeFiles.length > 0 + ? childTreeFiles + .slice(0, limit) + .map((f) => ( + + )) + : shadowChild} + + {limit < childTreeFiles.length ? ( + setLimit((l) => l + 50)} + /> + ) : null} + + ); + }, + ), +); + +export default TreeFiles; diff --git a/src/component/FileManager/TreeView/TreeNavigation.tsx b/src/component/FileManager/TreeView/TreeNavigation.tsx new file mode 100644 index 0000000..a4bb4d1 --- /dev/null +++ b/src/component/FileManager/TreeView/TreeNavigation.tsx @@ -0,0 +1,145 @@ +import { Box, Collapse, Fade } from "@mui/material"; + +import { ChevronRight, ExpandMore } from "@mui/icons-material"; +import { TreeView } from "@mui/x-tree-view"; +import React, { useEffect } from "react"; +import { TransitionGroup } from "react-transition-group"; +import { defaultPath, defaultSharedWithMePath, defaultTrashPath } from "../../../hooks/useNavigation.tsx"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import SessionManager from "../../../session"; +import CrUri, { Filesystem } from "../../../util/uri.ts"; +import { FileManagerIndex } from "../FileManager.tsx"; +import { FmIndexContext } from "../FmIndexContext.tsx"; +import Pinned, { usePinned } from "./Pinned.tsx"; +import TreeFiles from "./TreeFiles.tsx"; + +export interface TreeNavigationProps { + scrollRef?: React.MutableRefObject; + index?: number; + hideWithDrawer?: boolean; + disableSharedWithMe?: boolean; + disableTrash?: boolean; +} + +const TreeNavigation = React.memo( + ({ index = 0, scrollRef, hideWithDrawer, disableSharedWithMe, disableTrash }: TreeNavigationProps) => { + const base = useAppSelector((s) => s.fileManager[index].path_root); + const path = useAppSelector((s) => s.fileManager[index].pure_path_with_category); + const currentFs = useAppSelector((s) => s.fileManager[index].current_fs); + const elements = useAppSelector((s) => s.fileManager[index].path_elements); + const drawerOpen = useAppSelector((s) => s.globalState.drawerOpen); + const [expanded, setExpanded] = React.useState([]); + + useEffect(() => { + const res: string[] = []; + if (path) { + const p = new CrUri(path); + if (p.is_search()) { + return; + } + } + if (base && elements) { + const b = new CrUri(base); + res.push(base); + elements.forEach((element) => { + b.join(element); + res.push(b.toString()); + }); + } + setExpanded((e) => [...new Set([...e, ...res])]); + }, [path, base, elements]); + + const pinned = usePinned(); + const alreadyPinned = base && pinned && pinned.find((p) => p.uri == base); + const showShareTree = base && currentFs && currentFs == Filesystem.share && !alreadyPinned; + + useEffect(() => { + if (showShareTree && scrollRef && scrollRef.current) { + scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + } + }, [showShareTree]); + + const isLogin = !!SessionManager.currentLoginOrNull(); + + return ( + + + + } + defaultExpandIcon={} + expanded={expanded} + onNodeToggle={(_event, nodeIds: string[]) => { + setExpanded(nodeIds); + }} + > + + {showShareTree && ( + + + + )} + {isLogin && ( + <> + + {index == FileManagerIndex.main && ( + <> + + + + + + )} + {!disableSharedWithMe && ( + + )} + {!disableTrash && ( + + )} + + + )} + + + + + + ); + }, +); + +export default TreeNavigation; diff --git a/src/component/FileManager/TypeIcon.js b/src/component/FileManager/TypeIcon.js deleted file mode 100644 index fb75c84..0000000 --- a/src/component/FileManager/TypeIcon.js +++ /dev/null @@ -1,162 +0,0 @@ -import React from "react"; -import { mediaType } from "../../config"; -import ImageIcon from "@material-ui/icons/PhotoSizeSelectActual"; -import VideoIcon from "@material-ui/icons/Videocam"; -import AudioIcon from "@material-ui/icons/Audiotrack"; -import PdfIcon from "@material-ui/icons/PictureAsPdf"; -import { - Android, - FileExcelBox, - FilePowerpointBox, - FileWordBox, - LanguageC, - LanguageCpp, - LanguageGo, - LanguageJavascript, - LanguagePhp, - LanguagePython, - MagnetOn, - ScriptText, - WindowRestore, - ZipBox, -} from "mdi-material-ui"; -import FileShowIcon from "@material-ui/icons/InsertDriveFile"; -import { lighten } from "@material-ui/core/styles"; -import useTheme from "@material-ui/core/styles/useTheme"; -import { Avatar } from "@material-ui/core"; -import { MenuBook } from "@material-ui/icons"; - -const icons = { - audio: { - color: "#651fff", - icon: AudioIcon, - }, - video: { - color: "#d50000", - icon: VideoIcon, - }, - image: { - color: "#d32f2f", - icon: ImageIcon, - }, - pdf: { - color: "#f44336", - icon: PdfIcon, - }, - word: { - color: "#538ce5", - icon: FileWordBox, - }, - ppt: { - color: "rgb(239, 99, 63)", - icon: FilePowerpointBox, - }, - excel: { - color: "#4caf50", - icon: FileExcelBox, - }, - text: { - color: "#607d8b", - icon: ScriptText, - }, - torrent: { - color: "#5c6bc0", - icon: MagnetOn, - }, - zip: { - color: "#f9a825", - icon: ZipBox, - }, - excute: { - color: "#1a237e", - icon: WindowRestore, - }, - android: { - color: "#8bc34a", - icon: Android, - }, - file: { - color: "#607d8b", - icon: FileShowIcon, - }, - php: { - color: "#777bb3", - icon: LanguagePhp, - }, - go: { - color: "#16b3da", - icon: LanguageGo, - }, - python: { - color: "#3776ab", - icon: LanguagePython, - }, - c: { - color: "#a8b9cc", - icon: LanguageC, - }, - cpp: { - color: "#004482", - icon: LanguageCpp, - }, - js: { - color: "#f4d003", - icon: LanguageJavascript, - }, - epub: { - color: "#81b315", - icon: MenuBook, - }, -}; - -const getColor = (theme, color) => - theme.palette.type === "light" ? color : lighten(color, 0.2); - -let color; - -const TypeIcon = (props) => { - const theme = useTheme(); - - const fileSuffix = props.fileName.split(".").pop().toLowerCase(); - let fileType = "file"; - Object.keys(mediaType).forEach((k) => { - if (mediaType[k].indexOf(fileSuffix) !== -1) { - fileType = k; - } - }); - const IconComponent = icons[fileType].icon; - color = getColor(theme, icons[fileType].color); - if (props.getColorValue) { - props.getColorValue(color); - } - - return ( - <> - {props.isUpload && ( - - - - )} - {!props.isUpload && ( - - )} - - ); -}; - -export default TypeIcon; diff --git a/src/component/Frame/FrameManagerBundle.tsx b/src/component/Frame/FrameManagerBundle.tsx new file mode 100644 index 0000000..27f97ef --- /dev/null +++ b/src/component/Frame/FrameManagerBundle.tsx @@ -0,0 +1,4 @@ +import { FileManager } from "../FileManager/FileManager.tsx"; +import NavBarFrame, { AutoNavbarFrame } from "./NavBarFrame.tsx"; + +export { AutoNavbarFrame, FileManager, NavBarFrame }; diff --git a/src/component/Frame/HeadlessFrame.tsx b/src/component/Frame/HeadlessFrame.tsx new file mode 100644 index 0000000..8d3f82f --- /dev/null +++ b/src/component/Frame/HeadlessFrame.tsx @@ -0,0 +1,88 @@ +import { Box, Container, Grid, Paper } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import CircularProgress from "../Common/CircularProgress.tsx"; +import Logo from "../Common/Logo.tsx"; +import AutoHeight from "../Common/AutoHeight.tsx"; +import { Outlet, useNavigation } from "react-router-dom"; + +const Loading = () => { + return ( + + + + ); +}; + +const HeadlessFrame = () => { + const loading = useAppSelector( + (state) => state.globalState.loading.headlessFrame, + ); + const dispatch = useAppDispatch(); + let navigation = useNavigation(); + + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + flexGrow: 1, + height: "100vh", + overflow: "auto", + }} + > + + + + + `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(3)}`, + }} + > + + +
+ + + + {(loading || navigation.state !== "idle") && } +
+
+
+
+
+
+
+ ); +}; + +export default HeadlessFrame; diff --git a/src/component/Frame/NavBar/AppDrawer.tsx b/src/component/Frame/NavBar/AppDrawer.tsx new file mode 100644 index 0000000..41b41d1 --- /dev/null +++ b/src/component/Frame/NavBar/AppDrawer.tsx @@ -0,0 +1,98 @@ +import { + Box, + Drawer, + Popover, + PopoverProps, + Stack, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import DrawerHeader from "./DrawerHeader.tsx"; +import TreeNavigation from "../../FileManager/TreeView/TreeNavigation.tsx"; +import PageNavigation, { AdminPageNavigation } from "./PageNavigation.tsx"; +import StorageSummary from "./StorageSummary.tsx"; +import { useContext, useRef } from "react"; +import SessionManager from "../../../session"; +import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx"; + +const DrawerContent = () => { + const scrollRef = useRef(); + const user = SessionManager.currentLoginOrNull(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const pageVariant = useContext(PageVariantContext); + const isDashboard = pageVariant === PageVariant.dashboard; + return ( + <> + + + {!isDashboard && ( + <> + + + {user && } + + )} + {isDashboard && } + + + ); +}; + +export const DrawerPopover = (props: PopoverProps) => { + const dispatch = useAppDispatch(); + const open = useAppSelector((state) => state.globalState.drawerOpen); + const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth); + return ( + + + + + + ); +}; + +const AppDrawer = () => { + const theme = useTheme(); + const open = useAppSelector((state) => state.globalState.drawerOpen); + const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth); + const appBarBg = + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900]; + + return ( + + + + ); +}; + +export default AppDrawer; diff --git a/src/component/Frame/NavBar/AppMain.tsx b/src/component/Frame/NavBar/AppMain.tsx new file mode 100644 index 0000000..9854307 --- /dev/null +++ b/src/component/Frame/NavBar/AppMain.tsx @@ -0,0 +1,97 @@ +import { Box, styled, useMediaQuery, useTheme } from "@mui/material"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { Navigate, Outlet, useNavigation } from "react-router-dom"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { GroupPermission } from "../../../api/user.ts"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import SessionManager from "../../../session"; +import { GroupBS } from "../../../session/utils.ts"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx"; +import { DrawerHeaderContainer } from "./DrawerHeader.tsx"; + +const StyledLoadingContainer = styled(Box)(() => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "100%", +})); + +export const PageLoading = () => { + return ( + + + + ); +}; + +const AppMain = () => { + const open = useAppSelector((state) => state.globalState.drawerOpen); + const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth); + const [innerHeight, setInnerHeight] = useState(window.innerHeight); + let navigation = useNavigation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const pageVariant = useContext(PageVariantContext); + const isDashboard = pageVariant == PageVariant.dashboard; + const user = SessionManager.currentLoginOrNull(); + const isAdmin = useMemo(() => { + return GroupBS(user?.user).enabled(GroupPermission.is_admin); + }, [user?.user?.group?.permission]); + + useEffect(() => { + const handleResize = () => { + setInnerHeight(window.innerHeight); + }; + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return ( + ({ + flexGrow: 1, + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginRight: isMobile ? 0 : 2, + marginLeft: isMobile ? 0 : `-${drawerWidth - 16}px`, + ...(open && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: 0, + }), + height: isMobile ? "100%" : window.innerHeight, + minHeight: window.innerHeight, + display: "flex", + flexDirection: "column", + width: "100%", + overflow: "hidden", + })} + component={"main"} + > + + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={navigation.state !== "idle" ? "loading" : "idle"} + > + {navigation.state !== "idle" ? ( + + ) : isDashboard && !isAdmin ? ( + + ) : ( + + )} + + + + ); +}; + +export default AppMain; diff --git a/src/component/Frame/NavBar/DarkThemeSwitcher.tsx b/src/component/Frame/NavBar/DarkThemeSwitcher.tsx new file mode 100644 index 0000000..8dd8b87 --- /dev/null +++ b/src/component/Frame/NavBar/DarkThemeSwitcher.tsx @@ -0,0 +1,119 @@ +import { + IconButton, + Popover, + ToggleButton, + ToggleButtonGroup, + Tooltip, +} from "@mui/material"; +import DarkTheme from "../../Icons/DarkTheme.tsx"; +import { useTranslation } from "react-i18next"; +import { useMemo, useState } from "react"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import Sunny from "../../Icons/Sunny.tsx"; +import Moon from "../../Icons/Moon.tsx"; +import SunWithTime from "../../Icons/SunWithTime.tsx"; +import { setDarkMode } from "../../../redux/globalStateSlice.ts"; +import SessionManager, { UserSettings } from "../../../session"; + +interface SwitchPopoverProps { + open?: boolean; + anchorEl?: HTMLElement | null; + onClose?: () => void; +} + +export const SwitchPopover = ({ + open, + anchorEl, + onClose, +}: SwitchPopoverProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const darkMode = useAppSelector((state) => state.globalState.darkMode); + const currentMode = useMemo(() => { + if (darkMode === undefined) { + return "system"; + } + return darkMode ? "dark" : "light"; + }, [darkMode]); + const handleChange = ( + _event: React.MouseEvent, + newMode: string, + ) => { + let newSetting: boolean | undefined; + if (newMode === "light") { + newSetting = false; + } else if (newMode === "dark") { + newSetting = true; + } + dispatch(setDarkMode(newSetting)); + SessionManager.set(UserSettings.PreferredDarkMode, newSetting); + onClose && onClose(); + }; + + const inner = ( + + + + {t("navbar.toLightMode")} + + + + {t("setting.syncWithSystem")} + + + + {t("navbar.toDarkMode")} + + + ); + + return onClose ? ( + + {inner} + + ) : ( + inner + ); +}; + +const DarkThemeSwitcher = () => { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + return ( + <> + + + + + + setAnchorEl(null)} + /> + + ); +}; + +export default DarkThemeSwitcher; diff --git a/src/component/Frame/NavBar/DrawerHeader.tsx b/src/component/Frame/NavBar/DrawerHeader.tsx new file mode 100644 index 0000000..3402a85 --- /dev/null +++ b/src/component/Frame/NavBar/DrawerHeader.tsx @@ -0,0 +1,61 @@ +import { + Box, + Fade, + IconButton, + styled, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { setDrawerOpen } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { ChevronLeft } from "@mui/icons-material"; +import { useState } from "react"; +import Logo from "../../Common/Logo.tsx"; + +export const DrawerHeaderContainer = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, + justifyContent: "flex-end", +})); + +const DrawerHeader = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const dispatch = useAppDispatch(); + + const [showCollapse, setShowCollapse] = useState(false); + + return ( + setShowCollapse(true)} + onMouseLeave={() => setShowCollapse(false)} + > + + + + {!isMobile && ( + + + dispatch(setDrawerOpen(false))}> + + + + + )} + + ); +}; + +export default DrawerHeader; diff --git a/src/component/Frame/NavBar/FileSelectedActions.tsx b/src/component/Frame/NavBar/FileSelectedActions.tsx new file mode 100644 index 0000000..7be705b --- /dev/null +++ b/src/component/Frame/NavBar/FileSelectedActions.tsx @@ -0,0 +1,165 @@ +import { Badge, Box, IconButton, Stack, styled, Tooltip, useMediaQuery, useTheme } from "@mui/material"; +import React, { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; +import { FileResponse } from "../../../api/explorer.ts"; +import { clearSelected, ContextMenuTypes } from "../../../redux/fileManagerSlice.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { downloadFiles } from "../../../redux/thunks/download.ts"; +import { + deleteFile, + dialogBasedMoveCopy, + openFileContextMenu, + openShareDialog, + renameFile, +} from "../../../redux/thunks/file.ts"; +import { openViewers } from "../../../redux/thunks/viewer.ts"; +import useActionDisplayOpt from "../../FileManager/ContextMenu/useActionDisplayOpt.ts"; +import { FileManagerIndex } from "../../FileManager/FileManager.tsx"; +import { ActionButton, ActionButtonGroup } from "../../FileManager/TopBar/TopActions.tsx"; +import CopyOutlined from "../../Icons/CopyOutlined.tsx"; +import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import Download from "../../Icons/Download.tsx"; +import FolderArrowRightOutlined from "../../Icons/FolderArrowRightOutlined.tsx"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import Open from "../../Icons/Open.tsx"; +import RenameOutlined from "../../Icons/RenameOutlined.tsx"; +import ShareOutlined from "../../Icons/ShareOutlined.tsx"; + +export interface FileSelectedActionsProps { + targets: FileResponse[]; +} + +const StyledActionButton = styled(ActionButton)(({ theme }) => ({ + // disabled + "&.MuiButtonBase-root.Mui-disabled": { + color: theme.palette.text.primary, + fontWeight: theme.typography.fontWeightRegular, + fontSize: theme.typography.body2.fontSize, + }, +})); + +const StyledActionButtonGroup = styled(ActionButtonGroup)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, +})); + +const FileSelectedActions = forwardRef(({ targets }: FileSelectedActionsProps, ref: React.Ref) => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isTablet = useMediaQuery(theme.breakpoints.down("md")); + const { t } = useTranslation(); + const displayOpt = useActionDisplayOpt(targets, ContextMenuTypes.file); + + if (isMobile) { + return ( + + + dispatch( + clearSelected({ + index: FileManagerIndex.main, + value: undefined, + }), + ) + } + > + + + + + dispatch(openFileContextMenu(FileManagerIndex.main, targets[0], false, e))}> + + + + + + ); + } + + return ( + + + + + dispatch( + clearSelected({ + index: FileManagerIndex.main, + value: undefined, + }), + ) + } + > + + + theme.palette.text.primary }}> + {t("application:navbar.objectsSelected", { + num: targets.length, + })} + + + {!isTablet && ( + + {displayOpt.showOpen && ( + + dispatch(openViewers(0, targets[0]))}> + + + + )} + {displayOpt.showDownload && ( + + dispatch(downloadFiles(0, targets))}> + + + + )} + {displayOpt.showCopy && ( + + dispatch(dialogBasedMoveCopy(0, targets, true))}> + + + + )} + {displayOpt.showMove && ( + + dispatch(dialogBasedMoveCopy(0, targets, false))}> + + + + )} + {displayOpt.showRename && ( + + dispatch(renameFile(0, targets[0]))}> + + + + )} + {displayOpt.showShare && ( + + dispatch(openShareDialog(0, targets[0]))}> + + + + )} + {displayOpt.showDelete && ( + + dispatch(deleteFile(0, targets))}> + + + + )} + + )} + + dispatch(openFileContextMenu(FileManagerIndex.main, targets[0], false, e))}> + + + + + + ); +}); + +export default FileSelectedActions; diff --git a/src/component/Frame/NavBar/NavBarMainActions.tsx b/src/component/Frame/NavBar/NavBarMainActions.tsx new file mode 100644 index 0000000..29afa96 --- /dev/null +++ b/src/component/Frame/NavBar/NavBarMainActions.tsx @@ -0,0 +1,36 @@ +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { useMemo } from "react"; +import { Box } from "@mui/material"; +import SearchBar from "./SearchBar.tsx"; +import FileSelectedActions from "./FileSelectedActions.tsx"; +import { FileManagerIndex } from "../../FileManager/FileManager.tsx"; + +const NavBarMainActions = () => { + const selected = useAppSelector( + (state) => state.fileManager[FileManagerIndex.main].selected, + ); + const targets = useMemo(() => { + return Object.keys(selected).map((key) => selected[key]); + }, [selected]); + return ( + <> + + + node.addEventListener("transitionend", done, false) + } + classNames="fade" + key={`${targets.length > 0}`} + > + + {targets.length == 0 && } + {targets.length > 0 && } + + + + + ); +}; + +export default NavBarMainActions; diff --git a/src/component/Frame/NavBar/NavIconTransition.tsx b/src/component/Frame/NavBar/NavIconTransition.tsx new file mode 100644 index 0000000..00e8242 --- /dev/null +++ b/src/component/Frame/NavBar/NavIconTransition.tsx @@ -0,0 +1,47 @@ +import { Box, Fade, SvgIconProps } from "@mui/material"; +import { TransitionGroup } from "react-transition-group"; +import "../../Common/FadeTransition.css"; +import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; + +export interface NavIconTransitionProps { + fileIcon: ((props: SvgIconProps) => JSX.Element)[] | (typeof SvgIcon)[]; + active?: boolean; + [key: string]: any; + iconProps?: SvgIconProps; +} + +const NavIconTransition = ({ + fileIcon, + active, + iconProps, + ...rest +}: NavIconTransitionProps) => { + const [Active, InActive] = fileIcon; + return ( + + + {active && ( + + + + + + )} + {!active && ( + + + + + + )} + + + + ); +}; + +export default NavIconTransition; diff --git a/src/component/Frame/NavBar/PageNavigation.tsx b/src/component/Frame/NavBar/PageNavigation.tsx new file mode 100644 index 0000000..c79003e --- /dev/null +++ b/src/component/Frame/NavBar/PageNavigation.tsx @@ -0,0 +1,253 @@ +import { Box, SvgIconProps } from "@mui/material"; +import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; +import { memo, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router-dom"; +import { GroupPermission } from "../../../api/user.ts"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import SessionManager from "../../../session"; +import { GroupBS } from "../../../session/utils.ts"; +import ProDialog from "../../Admin/Common/ProDialog.tsx"; +import BoxMultiple from "../../Icons/BoxMultiple.tsx"; +import BoxMultipleFilled from "../../Icons/BoxMultipleFilled.tsx"; +import CloudDownload from "../../Icons/CloudDownload.tsx"; +import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx"; +import CubeSync from "../../Icons/CubeSync.tsx"; +import CubeSyncFilled from "../../Icons/CubeSyncFilled.tsx"; +import DataHistogram from "../../Icons/DataHistogram.tsx"; +import DataHistogramFilled from "../../Icons/DataHistogramFilled.tsx"; +import Folder from "../../Icons/Folder.tsx"; +import FolderOutlined from "../../Icons/FolderOutlined.tsx"; +import HomeOutlined from "../../Icons/HomeOutlined.tsx"; +import Payment from "../../Icons/Payment.tsx"; +import PaymentFilled from "../../Icons/PaymentFilled.tsx"; +import People from "../../Icons/People.tsx"; +import PeopleFilled from "../../Icons/PeopleFilled.tsx"; +import Person from "../../Icons/Person.tsx"; +import PersonOutlined from "../../Icons/PersonOutlined.tsx"; +import PhoneLaptop from "../../Icons/PhoneLaptop.tsx"; +import PhoneLaptopOutlined from "../../Icons/PhoneLaptopOutlined.tsx"; +import SendLogging from "../../Icons/SendLogging.tsx"; +import SendLoggingFilled from "../../Icons/SendLoggingFilled.tsx"; +import Server from "../../Icons/Server.tsx"; +import ServerFilled from "../../Icons/ServerFilled.tsx"; +import Setting from "../../Icons/Setting.tsx"; +import SettingsOutlined from "../../Icons/SettingsOutlined.tsx"; +import ShareAndroid from "../../Icons/ShareAndroid.tsx"; +import ShareOutlined from "../../Icons/ShareOutlined.tsx"; +import Storage from "../../Icons/Storage.tsx"; +import StorageOutlined from "../../Icons/StorageOutlined.tsx"; +import WrenchSettings from "../../Icons/WrenchSettings.tsx"; +import { ProChip } from "../../Pages/Setting/SettingForm.tsx"; +import NavIconTransition from "./NavIconTransition.tsx"; +import SideNavItem from "./SideNavItem.tsx"; + +export interface NavigationItem { + label: string; + icon: ((props: SvgIconProps) => JSX.Element)[] | (typeof SvgIcon)[]; + path: string; + pro?: boolean; +} + +let NavigationItems: NavigationItem[]; +NavigationItems = [ + { + label: "navbar.myShare", + icon: [ShareAndroid, ShareOutlined], + path: "/shares", + }, +]; + +const ConnectNavigationItem: NavigationItem = { + label: "navbar.connect", + icon: [PhoneLaptop, PhoneLaptopOutlined], + path: "/connect", +}; + +const TaskNavigationItem: NavigationItem = { + label: "navbar.taskQueue", + icon: [CubeSyncFilled, CubeSync], + path: "/tasks", +}; + +const RemoteDownloadNavigationItem: NavigationItem = { + label: "navbar.remoteDownload", + icon: [CloudDownload, CloudDownloadOutlined], + path: "/downloads", +}; + +export const SideNavItemComponent = ({ item }: { item: NavigationItem }) => { + const { t } = useTranslation("application"); + const navigate = useNavigate(); + const location = useLocation(); + const [proOpen, setProOpen] = useState(false); + const active = useMemo(() => { + return location.pathname == item.path || location.pathname.startsWith(item.path + "/"); + }, [location.pathname, item.path]); + return ( + <> + {item.pro && setProOpen(false)} />} + (item.pro ? setProOpen(true) : navigate(item.path))} + label={ + item.pro ? ( + + {t(item.label)} + t.typography.caption.fontSize, + }} + label="Pro" + color="primary" + size="small" + /> + + ) : ( + t(item.label) + ) + } + active={active} + icon={ + + } + /> + + ); +}; + +let AdminNavigationItems: NavigationItem[]; +AdminNavigationItems = [ + { + label: "dashboard:nav.summary", + icon: [DataHistogramFilled, DataHistogram], + path: "/admin/home", + }, + { + label: "dashboard:nav.settings", + icon: [Setting, SettingsOutlined], + path: "/admin/settings", + }, + { + label: "dashboard:nav.storagePolicy", + icon: [Storage, StorageOutlined], + path: "/admin/policy", + }, + { + label: "dashboard:nav.nodes", + icon: [ServerFilled, Server], + path: "/admin/node", + }, + { + label: "dashboard:nav.groups", + icon: [PeopleFilled, People], + path: "/admin/group", + }, + { + label: "dashboard:nav.users", + icon: [Person, PersonOutlined], + path: "/admin/user", + }, + { + label: "dashboard:nav.files", + icon: [Folder, FolderOutlined], + path: "/admin/file", + }, + { + label: "dashboard:nav.entities", + icon: [BoxMultipleFilled, BoxMultiple], + path: "/admin/blob", + }, + { + label: "dashboard:nav.shares", + icon: [ShareAndroid, ShareOutlined], + path: "/admin/share", + }, + { + label: "dashboard:nav.tasks", + icon: [CubeSyncFilled, CubeSync], + path: "/admin/task", + }, + { + label: "dashboard:vas.orders", + icon: [PaymentFilled, Payment], + path: "/admin/payment", + pro: true, + }, + { + label: "dashboard:nav.events", + icon: [SendLoggingFilled, SendLogging], + path: "/admin/event", + pro: true, + }, +]; + +export const AdminPageNavigation = memo(() => { + return ( + <> + + + {AdminNavigationItems.slice(1).map((item) => ( + + ))} + + + + ); +}); + +const PageNavigation = () => { + const shopNavEnabled = useAppSelector((state) => state.siteConfig.basic.config.shop_nav_enabled); + const appPromotionEnabled = useAppSelector((state) => state.siteConfig.basic.config.app_promotion); + const user = SessionManager.currentLoginOrNull(); + const isAdmin = useMemo(() => { + return GroupBS(user?.user).enabled(GroupPermission.is_admin); + }, [user?.user?.group?.permission]); + const remoteDownloadEnabled = useMemo(() => { + return GroupBS(user?.user).enabled(GroupPermission.remote_download); + }, [user?.user?.group?.permission]); + const connectEnabled = useMemo(() => { + return GroupBS(user?.user).enabled(GroupPermission.webdav) || appPromotionEnabled; + }, [user?.user?.group?.permission, appPromotionEnabled]); + const isLogin = !!user; + + return ( + <> + {isLogin && ( + + <> + {NavigationItems.map((item) => ( + + ))} + {connectEnabled && } + + {remoteDownloadEnabled && } + + + )} + {isLogin && isAdmin && ( + + )} + + ); +}; + +export default PageNavigation; diff --git a/src/component/Frame/NavBar/SearchBar.tsx b/src/component/Frame/NavBar/SearchBar.tsx new file mode 100644 index 0000000..9552044 --- /dev/null +++ b/src/component/Frame/NavBar/SearchBar.tsx @@ -0,0 +1,90 @@ +import { + alpha, + Button, + IconButton, + styled, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Trans, useTranslation } from "react-i18next"; +import { setSearchPopup } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import Search from "../../Icons/Search.tsx"; + +export const KeyIndicator = styled("code")(({ theme }) => ({ + backgroundColor: + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + border: `1px solid ${theme.palette.divider}`, + boxShadow: + theme.palette.mode === "light" + ? "0 1px 1px rgba(0, 0, 0, 0.2), 0 2px 0 0 rgba(255, 255, 255, 0.7) inset" + : "0 1px 1px rgba(0, 0, 0, 0.2), 0 2px 0 0 #3d3e42 inset", + padding: theme.spacing(0, 0.5), + borderRadius: 4, +})); + +const SearchButton = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.background.default, + color: theme.palette.text.disabled, + border: `1px solid ${theme.palette.divider}`, + pl: 2, + pr: 8, + " :hover": { + border: `1px solid ${theme.palette.primary.main}`, + backgroundColor: alpha(theme.palette.primary.main, 0.04), + } +})); + +const SearchBar = () => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const { t } = useTranslation(); + useHotkeys( + "/", + () => { + dispatch(setSearchPopup(true)); + }, + { preventDefault: true }, + ); + + if (isMobile) { + return ( + dispatch(setSearchPopup(true))}> + + + ); + } + + return ( + ({ + backgroundColor: theme.palette.background.default, + color: theme.palette.text.disabled, + border: `1px solid ${theme.palette.divider}`, + pl: 2, + pr: 8, + height: "100%", + })} + onClick={() => dispatch(setSearchPopup(true))} + variant={"outlined"} + startIcon={} + > + + + , + ]} + /> + + ); +}; + +export default SearchBar; diff --git a/src/component/Frame/NavBar/SideNavItem.tsx b/src/component/Frame/NavBar/SideNavItem.tsx new file mode 100644 index 0000000..1c7ee2b --- /dev/null +++ b/src/component/Frame/NavBar/SideNavItem.tsx @@ -0,0 +1,77 @@ +import { Box, ButtonBase, darken, lighten, styled } from "@mui/material"; +import * as React from "react"; +import { NoWrapTypography } from "../../Common/StyledComponents.tsx"; + +const StyledButtonBase = styled(ButtonBase)<{ + active?: boolean; +}>(({ theme, active }) => ({ + borderRadius: "90px", + display: "flex", + justifyContent: "left", + alignItems: "initial", + width: "100%", + backgroundColor: active + ? `${ + theme.palette.mode == "light" + ? lighten(theme.palette.primary.main, 0.7) + : darken(theme.palette.primary.main, 0.7) + }!important` + : "initial", + transition: + "background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", +})); + +export interface SideNavItemBaseProps { + active?: boolean; + [key: string]: any; +} +export const SideNavItemBase = React.forwardRef( + ({ active, ...rest }: SideNavItemBaseProps, ref: React.Ref) => { + return ; + }, +); + +const StyledSideNavItem = styled(SideNavItemBase)<{ level?: number }>(({ theme, level }) => ({ + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + padding: "4px", + paddingLeft: `${28 + (level ?? 0) * 16}px`, + height: "32px", + display: "flex", + alignItems: "center", +})); + +export interface SideNavItemProps extends SideNavItemBaseProps { + icon?: React.ReactNode; + label?: string | React.ReactNode; + level?: number; + [key: string]: any; +} + +const SideNavItem = React.forwardRef( + ({ icon, label, level, sx, ...rest }: SideNavItemProps, ref: React.Ref) => { + return ( + + + {icon} + + {label} + + ); + }, +); + +export default SideNavItem; diff --git a/src/component/Frame/NavBar/SplitHandle.tsx b/src/component/Frame/NavBar/SplitHandle.tsx new file mode 100644 index 0000000..c6076ed --- /dev/null +++ b/src/component/Frame/NavBar/SplitHandle.tsx @@ -0,0 +1,81 @@ +import { Box, Fade } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useEffect, useRef, useState } from "react"; +import { setDrawerWidth } from "../../../redux/globalStateSlice.ts"; +import SessionManager, { UserSettings } from "../../../session"; + +export interface SplitHandleProps {} + +const minDrawerWidth = 236; + +const SplitHandle = (_props: SplitHandleProps) => { + const dispatch = useAppDispatch(); + const [moving, setMoving] = useState(false); + const [cursor, setCursor] = useState(0); + const finalWidth = useRef(0); + + const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth); + const drawerOpen = useAppSelector((state) => state.globalState.drawerOpen); + + useEffect(() => { + setCursor(drawerWidth - 4); + finalWidth.current = drawerWidth - 4; + }, []); + + const handler = () => { + setMoving(true); + document.body.style.userSelect = "none"; + function onMouseMove(e: MouseEvent) { + e.preventDefault(); + const newWidth = e.clientX - document.body.offsetLeft; + const cappedWidth = Math.max( + Math.min(newWidth, window.innerWidth / 2), + minDrawerWidth, + ); + setCursor(cappedWidth); + finalWidth.current = cappedWidth; + } + function onMouseUp() { + document.body.removeEventListener("mousemove", onMouseMove); + setMoving(false); + dispatch(setDrawerWidth(finalWidth.current + 4)); + SessionManager.set(UserSettings.DrawerWidth, finalWidth.current + 4); + document.body.style.userSelect = "initial"; + } + + document.body.addEventListener("mousemove", onMouseMove); + document.body.addEventListener("mouseup", onMouseUp, { once: true }); + }; + + return ( + <> + {drawerOpen && ( + theme.zIndex.drawer + 2, + }} + /> + )} + + theme.zIndex.drawer + 1, + }} + /> + + + ); +}; + +export default SplitHandle; diff --git a/src/component/Frame/NavBar/StorageSummary.tsx b/src/component/Frame/NavBar/StorageSummary.tsx new file mode 100644 index 0000000..e9c601d --- /dev/null +++ b/src/component/Frame/NavBar/StorageSummary.tsx @@ -0,0 +1,67 @@ +import { LinearProgress, linearProgressClasses, Skeleton, styled, Typography } from "@mui/material"; +import { memo, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { updateUserCapacity } from "../../../redux/thunks/filemanager.ts"; +import { sizeToString } from "../../../util"; +import { RadiusFrame } from "../RadiusFrame.tsx"; + +const StyledBox = styled(RadiusFrame)(({ theme }) => ({ + padding: theme.spacing(1, 2, 1, 2), + margin: theme.spacing(0, 2, 0, 2), +})); + +const StorageHeaderContainer = styled("div")(() => ({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", +})); + +const BorderLinearProgress = styled(LinearProgress)<{ warning: boolean }>(({ theme, warning }) => ({ + height: 8, + borderRadius: 5, + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: theme.palette.grey[theme.palette.mode === "light" ? 200 : 800], + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 5, + backgroundColor: warning ? theme.palette.warning.main : theme.palette.primary.main, + }, + marginTop: theme.spacing(1), +})); + +const StorageSummary = memo(() => { + const { t } = useTranslation("application"); + const dispatch = useAppDispatch(); + const capacity = useAppSelector((state) => state.fileManager[0].capacity); + useEffect(() => { + if (!capacity) { + dispatch(updateUserCapacity(0)); + return; + } + }, [capacity]); + return ( + + + {t("application:navbar.storage")} + + {capacity && ( + capacity.total} + variant="determinate" + value={Math.min(100, (capacity.used / capacity.total) * 100)} + /> + )} + {!capacity && } + + {capacity ? ( + `${sizeToString(capacity.used)} / ${sizeToString(capacity.total)}` + ) : ( + + )} + + + ); +}); + +export default StorageSummary; diff --git a/src/component/Frame/NavBar/TopAppBar.tsx b/src/component/Frame/NavBar/TopAppBar.tsx new file mode 100644 index 0000000..9428f99 --- /dev/null +++ b/src/component/Frame/NavBar/TopAppBar.tsx @@ -0,0 +1,180 @@ +import { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; +import { + AppBar, + Box, + Collapse, + IconButton, + Stack, + Toolbar, + Tooltip, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { Menu } from "@mui/icons-material"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { + setDrawerOpen, + setMobileDrawerOpen, +} from "../../../redux/globalStateSlice.ts"; +import NewButton from "../../FileManager/NewButton.tsx"; +import UserAction from "./UserAction.tsx"; +import Setting from "../../Icons/Setting.tsx"; +import DarkThemeSwitcher from "./DarkThemeSwitcher.tsx"; +import NavBarMainActions from "./NavBarMainActions.tsx"; +import MusicPlayer from "../../Viewers/MusicPlayer/MusicPlayer.tsx"; +import { TaskListIconButton } from "../../Uploader/TaskListIconButton.tsx"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import SessionManager from "../../../session"; +import { useContext, useState } from "react"; +import { DrawerPopover } from "./AppDrawer.tsx"; +import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx"; + +interface AppBarProps extends MuiAppBarProps { + open?: boolean; +} + +const TopAppBar = () => { + const dispatch = useAppDispatch(); + const theme = useTheme(); + const pageVariant = useContext(PageVariantContext); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isMainPage = pageVariant == PageVariant.default; + const { t } = useTranslation(); + const navigate = useNavigate(); + const open = useAppSelector((state) => state.globalState.drawerOpen); + const mobileDrawerOpen = useAppSelector( + (state) => state.globalState.mobileDrawerOpen, + ); + const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth); + const musicPlayer = useAppSelector((state) => state.globalState.musicPlayer); + const [mobileMenuAnchor, setMobileMenuAnchor] = useState( + null, + ); + + const appBarBg = + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900]; + const isLogin = !!SessionManager.currentLoginOrNull(); + + const onMobileMenuClicked = (e: React.MouseEvent) => { + setMobileMenuAnchor(e.currentTarget); + dispatch(setMobileDrawerOpen(true)); + }; + + const onCloseMobileMenu = () => { + dispatch(setMobileDrawerOpen(false)); + }; + + // @ts-ignore + return ( + ({ + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + backgroundColor: appBarBg, + color: theme.palette.getContrastText(appBarBg), + ...(open && + !isMobile && { + width: `calc(100% - ${drawerWidth}px)`, + marginLeft: `${drawerWidth}px`, + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }), + })} + position="fixed" + > + + + dispatch(setDrawerOpen(true)) + } + edge="start" + sx={{ + mr: isMobile ? 1 : 2, + ml: isMobile ? -1 : -1.5, + }} + > + + + + {isMobile && ( + <> + + + )} + {!isMobile && isMainPage && ( + + + + + )} + + + {!isMobile && } + {musicPlayer && } + {!isMobile ? ( + <> + + {isLogin && ( + + navigate("/settings")} + > + + + + )} + + + ) : ( + <> + {isMainPage && } + {isMainPage && } + + + )} + + + + ); +}; + +export default TopAppBar; diff --git a/src/component/Frame/NavBar/UserAction.tsx b/src/component/Frame/NavBar/UserAction.tsx new file mode 100644 index 0000000..8b1dbd1 --- /dev/null +++ b/src/component/Frame/NavBar/UserAction.tsx @@ -0,0 +1,175 @@ +import { + Box, + Divider, + IconButton, + ListItemIcon, + ListItemText, + MenuList, + Popover, + PopoverProps, + styled, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { bindPopover } from "material-ui-popup-state"; +import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { GroupPermission } from "../../../api/user.ts"; +import { closeMusicPlayer } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import SessionManager, { Session } from "../../../session"; +import { GroupBS } from "../../../session/utils.ts"; +import UserAvatar from "../../Common/User/UserAvatar.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import HomeOutlined from "../../Icons/HomeOutlined.tsx"; +import Person from "../../Icons/Person.tsx"; +import SettingsOutlined from "../../Icons/SettingsOutlined.tsx"; +import SignOut from "../../Icons/SignOut.tsx"; +import WrenchSettings from "../../Icons/WrenchSettings.tsx"; + +const StyledTypography = styled(Typography)(() => ({ + lineHeight: 1, +})); + +const UserPopover = ({ open, onClose, ...rest }: PopoverProps) => { + const user = SessionManager.currentUser(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + if (!user) { + return null; + } + + const isAdmin = useMemo(() => { + return GroupBS(user).enabled(GroupPermission.is_admin); + }, [user.group?.permission]); + + const signWithHint = (email: string) => { + navigate("/session?phase=email&email=" + encodeURIComponent(email)); + }; + + const signOut = useCallback(() => { + SessionManager.signOutCurrent(); + dispatch(closeMusicPlayer()); + navigate("/session"); + onClose && onClose({}, "backdropClick"); + }, []); + + const openMyProfile = useCallback(() => { + navigate(`/profile/${user?.id}`); + onClose && onClose({}, "backdropClick"); + }, [user?.id]); + + const openSetting = useCallback(() => { + navigate(`/settings`); + onClose && onClose({}, "backdropClick"); + }, [user?.id]); + + const openDashboard = useCallback(() => { + navigate(`/admin/home`); + onClose && onClose({}, "backdropClick"); + }, [user?.id]); + + return ( + + + + + {user.nickname} + + + {user.group?.name} + + + + {user.email} + + + + + {isAdmin && ( + + + + + {t("navbar.dashboard")} + + )} + {isMobile && ( + + + + + {t("navbar.setting")} + + )} + + + + + {t("navbar.myProfile")} + + + + + + {t("login.logout")} + + + + ); +}; + +const UserAction = () => { + const navigate = useNavigate(); + const [current, setCurrent] = useState(); + const popupState = usePopupState({ variant: "popover", popupId: "user" }); + useEffect(() => { + try { + const session = SessionManager.currentLogin(); + if (session) { + setCurrent(session); + } + } catch (e) {} + }, []); + return ( + <> + + {!current && navigate("/session")} />} + {current && } + + + + ); +}; + +export default UserAction; diff --git a/src/component/Frame/NavBarFrame.tsx b/src/component/Frame/NavBarFrame.tsx new file mode 100644 index 0000000..4ba1d2e --- /dev/null +++ b/src/component/Frame/NavBarFrame.tsx @@ -0,0 +1,71 @@ +import { Box, useMediaQuery, useTheme } from "@mui/material"; +import { createContext, useEffect } from "react"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { useLocation } from "react-router-dom"; +import { setMobileDrawerOpen } from "../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../redux/hooks.ts"; +import ContextMenu from "../FileManager/ContextMenu/ContextMenu.tsx"; +import Dialogs from "../FileManager/Dialogs/Dialogs.tsx"; +import DragLayer from "../FileManager/Dnd/DragLayer.tsx"; +import { FileManagerIndex } from "../FileManager/FileManager.tsx"; +import SearchPopup from "../FileManager/Search/SearchPopup.tsx"; +import Uploader from "../Uploader/Uploader.tsx"; +import AppDrawer from "./NavBar/AppDrawer.tsx"; +import Main from "./NavBar/AppMain.tsx"; +import SplitHandle from "./NavBar/SplitHandle.tsx"; +import TopAppBar from "./NavBar/TopAppBar.tsx"; + +export enum PageVariant { + default, + dashboard, +} + +export interface NavBarFrameProps { + variant?: PageVariant; +} + +export const PageVariantContext = createContext(PageVariant.default); + +export const AutoNavbarFrame = () => { + const path = useLocation().pathname; + return ; +}; + +const NavBarFrame = ({ variant }: NavBarFrameProps) => { + const theme = useTheme(); + const dispatch = useAppDispatch(); + const isTouch = useMediaQuery("(pointer: coarse)"); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const location = useLocation(); + + useEffect(() => { + if (isMobile) { + dispatch(setMobileDrawerOpen(false)); + } + }, [location]); + return ( + + (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), + display: "flex", + }} + > + + {!isMobile && variant != PageVariant.dashboard && !isTouch && } + {!isMobile && !isTouch && } + + {!isMobile && } + {variant != PageVariant.dashboard && } + + + {variant != PageVariant.dashboard && } +
+ + + + ); +}; + +export default NavBarFrame; diff --git a/src/component/Frame/RadiusFrame.tsx b/src/component/Frame/RadiusFrame.tsx new file mode 100644 index 0000000..638cd12 --- /dev/null +++ b/src/component/Frame/RadiusFrame.tsx @@ -0,0 +1,10 @@ +import { Box, styled } from "@mui/material"; + +export const RadiusFrame = styled(Box)<{ + withBorder?: boolean; + square?: boolean; +}>(({ theme, withBorder, square }) => ({ + borderRadius: square ? 0 : theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, + border: withBorder ? `1px solid ${theme.palette.divider}` : "initial", +})); diff --git a/src/component/Icons/Add.tsx b/src/component/Icons/Add.tsx new file mode 100644 index 0000000..e00a627 --- /dev/null +++ b/src/component/Icons/Add.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Add(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/AppFolder.tsx b/src/component/Icons/AppFolder.tsx new file mode 100644 index 0000000..64407ad --- /dev/null +++ b/src/component/Icons/AppFolder.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function AppFolder(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/AppsList.tsx b/src/component/Icons/AppsList.tsx new file mode 100644 index 0000000..491dab9 --- /dev/null +++ b/src/component/Icons/AppsList.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function AppsList(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/AppsListOutlined.tsx b/src/component/Icons/AppsListOutlined.tsx new file mode 100644 index 0000000..f1de6f8 --- /dev/null +++ b/src/component/Icons/AppsListOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function AppsListOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Archive.tsx b/src/component/Icons/Archive.tsx new file mode 100644 index 0000000..441ed4e --- /dev/null +++ b/src/component/Icons/Archive.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Archive(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArchiveArrow.tsx b/src/component/Icons/ArchiveArrow.tsx new file mode 100644 index 0000000..5f8ab40 --- /dev/null +++ b/src/component/Icons/ArchiveArrow.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArchiveArrow(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowClockwise.tsx b/src/component/Icons/ArrowClockwise.tsx new file mode 100644 index 0000000..149dba2 --- /dev/null +++ b/src/component/Icons/ArrowClockwise.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowClockwise(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowClockwiseFilled.tsx b/src/component/Icons/ArrowClockwiseFilled.tsx new file mode 100644 index 0000000..7c2f43b --- /dev/null +++ b/src/component/Icons/ArrowClockwiseFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowClockwiseFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowDown.tsx b/src/component/Icons/ArrowDown.tsx new file mode 100644 index 0000000..f813f28 --- /dev/null +++ b/src/component/Icons/ArrowDown.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowDown(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowHookDownLeft.tsx b/src/component/Icons/ArrowHookDownLeft.tsx new file mode 100644 index 0000000..3d64366 --- /dev/null +++ b/src/component/Icons/ArrowHookDownLeft.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowHookDownLeft(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowHookUpRight.tsx b/src/component/Icons/ArrowHookUpRight.tsx new file mode 100644 index 0000000..605d0ee --- /dev/null +++ b/src/component/Icons/ArrowHookUpRight.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowHookUpRight(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowLeft.tsx b/src/component/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..04226cc --- /dev/null +++ b/src/component/Icons/ArrowLeft.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const ArrowLeft = createSvgIcon( + , + "ArrowLeft", +); + +export default ArrowLeft; diff --git a/src/component/Icons/ArrowRepeatAll.tsx b/src/component/Icons/ArrowRepeatAll.tsx new file mode 100644 index 0000000..1bdae1d --- /dev/null +++ b/src/component/Icons/ArrowRepeatAll.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowRepeatAll(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowRepeatOne.tsx b/src/component/Icons/ArrowRepeatOne.tsx new file mode 100644 index 0000000..6976de9 --- /dev/null +++ b/src/component/Icons/ArrowRepeatOne.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowRepeatOne(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowShuffle.tsx b/src/component/Icons/ArrowShuffle.tsx new file mode 100644 index 0000000..8be0b5f --- /dev/null +++ b/src/component/Icons/ArrowShuffle.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowShuffle(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowSort.tsx b/src/component/Icons/ArrowSort.tsx new file mode 100644 index 0000000..391f12b --- /dev/null +++ b/src/component/Icons/ArrowSort.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowSort(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowSortDownFilled.tsx b/src/component/Icons/ArrowSortDownFilled.tsx new file mode 100644 index 0000000..d6b784a --- /dev/null +++ b/src/component/Icons/ArrowSortDownFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowSortDownFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowSync.tsx b/src/component/Icons/ArrowSync.tsx new file mode 100644 index 0000000..00862a0 --- /dev/null +++ b/src/component/Icons/ArrowSync.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowSync(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ArrowSyncCircleFilled.tsx b/src/component/Icons/ArrowSyncCircleFilled.tsx new file mode 100644 index 0000000..febf8ad --- /dev/null +++ b/src/component/Icons/ArrowSyncCircleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ArrowSyncCircleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BinFullOutlined.tsx b/src/component/Icons/BinFullOutlined.tsx new file mode 100644 index 0000000..3fd3e12 --- /dev/null +++ b/src/component/Icons/BinFullOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BinFullOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Book.tsx b/src/component/Icons/Book.tsx new file mode 100644 index 0000000..6974878 --- /dev/null +++ b/src/component/Icons/Book.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Book(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Border.tsx b/src/component/Icons/Border.tsx new file mode 100644 index 0000000..a23c718 --- /dev/null +++ b/src/component/Icons/Border.tsx @@ -0,0 +1,42 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Border(props: SvgIconProps) { + return ( + + + + + + + + + + + + + ); +} diff --git a/src/component/Icons/BorderAll.tsx b/src/component/Icons/BorderAll.tsx new file mode 100644 index 0000000..db07d30 --- /dev/null +++ b/src/component/Icons/BorderAll.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BorderAll(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BorderInside.tsx b/src/component/Icons/BorderInside.tsx new file mode 100644 index 0000000..1d3d566 --- /dev/null +++ b/src/component/Icons/BorderInside.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BorderInside(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Bot.tsx b/src/component/Icons/Bot.tsx new file mode 100644 index 0000000..f9e6046 --- /dev/null +++ b/src/component/Icons/Bot.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Bot(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BoxMultiple.tsx b/src/component/Icons/BoxMultiple.tsx new file mode 100644 index 0000000..18ac6e4 --- /dev/null +++ b/src/component/Icons/BoxMultiple.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BoxMultiple(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BoxMultipleFilled.tsx b/src/component/Icons/BoxMultipleFilled.tsx new file mode 100644 index 0000000..45b9c1b --- /dev/null +++ b/src/component/Icons/BoxMultipleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BoxMultipleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BranchCompare.tsx b/src/component/Icons/BranchCompare.tsx new file mode 100644 index 0000000..9c4bc65 --- /dev/null +++ b/src/component/Icons/BranchCompare.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BranchCompare(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Broom.tsx b/src/component/Icons/Broom.tsx new file mode 100644 index 0000000..2b11cdb --- /dev/null +++ b/src/component/Icons/Broom.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Broom = createSvgIcon( + , + "Broom", +); + +export default Broom; diff --git a/src/component/Icons/BuildingShop.tsx b/src/component/Icons/BuildingShop.tsx new file mode 100644 index 0000000..3a73845 --- /dev/null +++ b/src/component/Icons/BuildingShop.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BuildingShop(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/BuildingShopFilled.tsx b/src/component/Icons/BuildingShopFilled.tsx new file mode 100644 index 0000000..c0533de --- /dev/null +++ b/src/component/Icons/BuildingShopFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BuildingShopFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CalendarClock.tsx b/src/component/Icons/CalendarClock.tsx new file mode 100644 index 0000000..58c79f0 --- /dev/null +++ b/src/component/Icons/CalendarClock.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CalendarClock(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CameraFilled.tsx b/src/component/Icons/CameraFilled.tsx new file mode 100644 index 0000000..231c5a7 --- /dev/null +++ b/src/component/Icons/CameraFilled.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const CameraFilled = createSvgIcon( + , + "CameraFilled", +); + +export default CameraFilled; diff --git a/src/component/Icons/CameraRounded.tsx b/src/component/Icons/CameraRounded.tsx new file mode 100644 index 0000000..b8b03a7 --- /dev/null +++ b/src/component/Icons/CameraRounded.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const CameraRounded = createSvgIcon( + , + "CameraRounded", +); + +export default CameraRounded; diff --git a/src/component/Icons/CaretDown.tsx b/src/component/Icons/CaretDown.tsx new file mode 100644 index 0000000..5cc0601 --- /dev/null +++ b/src/component/Icons/CaretDown.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CaretDown(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CaretRight.tsx b/src/component/Icons/CaretRight.tsx new file mode 100644 index 0000000..7620a7e --- /dev/null +++ b/src/component/Icons/CaretRight.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CaretRight(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Cart.tsx b/src/component/Icons/Cart.tsx new file mode 100644 index 0000000..ca53002 --- /dev/null +++ b/src/component/Icons/Cart.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Cart(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CheckCircleFilled.tsx b/src/component/Icons/CheckCircleFilled.tsx new file mode 100644 index 0000000..920e67a --- /dev/null +++ b/src/component/Icons/CheckCircleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CheckCircleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CheckUnchecked.tsx b/src/component/Icons/CheckUnchecked.tsx new file mode 100644 index 0000000..4cd438c --- /dev/null +++ b/src/component/Icons/CheckUnchecked.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CheckUnchecked(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Checkmark.tsx b/src/component/Icons/Checkmark.tsx new file mode 100644 index 0000000..72a2484 --- /dev/null +++ b/src/component/Icons/Checkmark.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Checkmark(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CheckmarkCircle.tsx b/src/component/Icons/CheckmarkCircle.tsx new file mode 100644 index 0000000..f5c426c --- /dev/null +++ b/src/component/Icons/CheckmarkCircle.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CheckmarkCircle(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CheckmarkCircleFilled.tsx b/src/component/Icons/CheckmarkCircleFilled.tsx new file mode 100644 index 0000000..0a7c8f8 --- /dev/null +++ b/src/component/Icons/CheckmarkCircleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CheckmarkCircleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ChevronRight.tsx b/src/component/Icons/ChevronRight.tsx new file mode 100644 index 0000000..702b6d8 --- /dev/null +++ b/src/component/Icons/ChevronRight.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ChevronRight(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CircleHintFilled.tsx b/src/component/Icons/CircleHintFilled.tsx new file mode 100644 index 0000000..8ef6afd --- /dev/null +++ b/src/component/Icons/CircleHintFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CircleHintFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Clipboard.tsx b/src/component/Icons/Clipboard.tsx new file mode 100644 index 0000000..51965e9 --- /dev/null +++ b/src/component/Icons/Clipboard.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Clipboard = createSvgIcon( + , + "Clipboard", +); + +export default Clipboard; diff --git a/src/component/Icons/ClockArrowDownload.tsx b/src/component/Icons/ClockArrowDownload.tsx new file mode 100644 index 0000000..c08763c --- /dev/null +++ b/src/component/Icons/ClockArrowDownload.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const ClockArrowDownload = createSvgIcon( + , + "ClockArrowDownload", +); + +export default ClockArrowDownload; diff --git a/src/component/Icons/ClockFilled.tsx b/src/component/Icons/ClockFilled.tsx new file mode 100644 index 0000000..4c35200 --- /dev/null +++ b/src/component/Icons/ClockFilled.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const ClockFilled = createSvgIcon( + , + "ClockFilled", +); + +export default ClockFilled; diff --git a/src/component/Icons/CloudArrowIUp.tsx b/src/component/Icons/CloudArrowIUp.tsx new file mode 100644 index 0000000..95866d8 --- /dev/null +++ b/src/component/Icons/CloudArrowIUp.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CloudArrowIUp(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CloudDownload.tsx b/src/component/Icons/CloudDownload.tsx new file mode 100644 index 0000000..dd5f254 --- /dev/null +++ b/src/component/Icons/CloudDownload.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CloudDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CloudDownloadOutlined.tsx b/src/component/Icons/CloudDownloadOutlined.tsx new file mode 100644 index 0000000..fe9907d --- /dev/null +++ b/src/component/Icons/CloudDownloadOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CloudDownloadOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CloudFilled.tsx b/src/component/Icons/CloudFilled.tsx new file mode 100644 index 0000000..57ceebe --- /dev/null +++ b/src/component/Icons/CloudFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CloudFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CoinStack.tsx b/src/component/Icons/CoinStack.tsx new file mode 100644 index 0000000..f763e7f --- /dev/null +++ b/src/component/Icons/CoinStack.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CoinStack(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Color.tsx b/src/component/Icons/Color.tsx new file mode 100644 index 0000000..ac708cd --- /dev/null +++ b/src/component/Icons/Color.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Color(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CopperCoin.tsx b/src/component/Icons/CopperCoin.tsx new file mode 100644 index 0000000..35f81d6 --- /dev/null +++ b/src/component/Icons/CopperCoin.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CopperCoin(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CopyOutlined.tsx b/src/component/Icons/CopyOutlined.tsx new file mode 100644 index 0000000..d92d013 --- /dev/null +++ b/src/component/Icons/CopyOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CopyOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Copyright.tsx b/src/component/Icons/Copyright.tsx new file mode 100644 index 0000000..c50fea5 --- /dev/null +++ b/src/component/Icons/Copyright.tsx @@ -0,0 +1,11 @@ +import { createSvgIcon } from "@mui/material"; + +const Copyright = createSvgIcon( + + + + , + "Copyright", +); + +export default Copyright; diff --git a/src/component/Icons/CubeSync.tsx b/src/component/Icons/CubeSync.tsx new file mode 100644 index 0000000..92d8506 --- /dev/null +++ b/src/component/Icons/CubeSync.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CubeSync(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CubeSyncFilled.tsx b/src/component/Icons/CubeSyncFilled.tsx new file mode 100644 index 0000000..cdb3fb6 --- /dev/null +++ b/src/component/Icons/CubeSyncFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CubeSyncFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/CubeTree.tsx b/src/component/Icons/CubeTree.tsx new file mode 100644 index 0000000..6adbfd4 --- /dev/null +++ b/src/component/Icons/CubeTree.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function CubeTree(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Currency.tsx b/src/component/Icons/Currency.tsx new file mode 100644 index 0000000..f8f95d9 --- /dev/null +++ b/src/component/Icons/Currency.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Currency(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DarkTheme.tsx b/src/component/Icons/DarkTheme.tsx new file mode 100644 index 0000000..e23e3a1 --- /dev/null +++ b/src/component/Icons/DarkTheme.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DarkTheme(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DataHistogram.tsx b/src/component/Icons/DataHistogram.tsx new file mode 100644 index 0000000..5e29e75 --- /dev/null +++ b/src/component/Icons/DataHistogram.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DataHistogram(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DataHistogramFilled.tsx b/src/component/Icons/DataHistogramFilled.tsx new file mode 100644 index 0000000..8d7c57e --- /dev/null +++ b/src/component/Icons/DataHistogramFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DataHistogramFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Delete.tsx b/src/component/Icons/Delete.tsx new file mode 100644 index 0000000..a4e3e29 --- /dev/null +++ b/src/component/Icons/Delete.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Delete(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DeleteOutlined.tsx b/src/component/Icons/DeleteOutlined.tsx new file mode 100644 index 0000000..01a7cfb --- /dev/null +++ b/src/component/Icons/DeleteOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DeleteOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DesktopFlow.tsx b/src/component/Icons/DesktopFlow.tsx new file mode 100644 index 0000000..93895ed --- /dev/null +++ b/src/component/Icons/DesktopFlow.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DesktopFlow(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Discord.tsx b/src/component/Icons/Discord.tsx new file mode 100644 index 0000000..d7877da --- /dev/null +++ b/src/component/Icons/Discord.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Discord(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Dismiss.tsx b/src/component/Icons/Dismiss.tsx new file mode 100644 index 0000000..2b15f6c --- /dev/null +++ b/src/component/Icons/Dismiss.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Dismiss(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DismissCircleFilled.tsx b/src/component/Icons/DismissCircleFilled.tsx new file mode 100644 index 0000000..b27b730 --- /dev/null +++ b/src/component/Icons/DismissCircleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DismissCircleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Divider.tsx b/src/component/Icons/Divider.tsx new file mode 100644 index 0000000..ead2330 --- /dev/null +++ b/src/component/Icons/Divider.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Divider(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Document.tsx b/src/component/Icons/Document.tsx new file mode 100644 index 0000000..e8d10f9 --- /dev/null +++ b/src/component/Icons/Document.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Document(props: SvgIconProps) { + return ( + + + + + ); +} diff --git a/src/component/Icons/DocumentArrowDownFilled.tsx b/src/component/Icons/DocumentArrowDownFilled.tsx new file mode 100644 index 0000000..d8978d9 --- /dev/null +++ b/src/component/Icons/DocumentArrowDownFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentArrowDownFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DocumentCopyFilled.tsx b/src/component/Icons/DocumentCopyFilled.tsx new file mode 100644 index 0000000..2a96b8d --- /dev/null +++ b/src/component/Icons/DocumentCopyFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentCopyFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DocumentDataLink.tsx b/src/component/Icons/DocumentDataLink.tsx new file mode 100644 index 0000000..f1cf4ee --- /dev/null +++ b/src/component/Icons/DocumentDataLink.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentDataLink(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DocumentFlowchart.tsx b/src/component/Icons/DocumentFlowchart.tsx new file mode 100644 index 0000000..e636750 --- /dev/null +++ b/src/component/Icons/DocumentFlowchart.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const DocumentFlowchart = createSvgIcon( + , + "DocumentFlowchart", +); + +export default DocumentFlowchart; diff --git a/src/component/Icons/DocumentPDF.tsx b/src/component/Icons/DocumentPDF.tsx new file mode 100644 index 0000000..6e01d84 --- /dev/null +++ b/src/component/Icons/DocumentPDF.tsx @@ -0,0 +1,11 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentPDF(props: SvgIconProps) { + return ( + + + + + + ); +} diff --git a/src/component/Icons/DocumentText.tsx b/src/component/Icons/DocumentText.tsx new file mode 100644 index 0000000..0a445ce --- /dev/null +++ b/src/component/Icons/DocumentText.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentText(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/DocumentTextOutlined.tsx b/src/component/Icons/DocumentTextOutlined.tsx new file mode 100644 index 0000000..f12825b --- /dev/null +++ b/src/component/Icons/DocumentTextOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function DocumentTextOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Download.tsx b/src/component/Icons/Download.tsx new file mode 100644 index 0000000..22582b0 --- /dev/null +++ b/src/component/Icons/Download.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Download = createSvgIcon( + , + "Download", +); + +export default Download; diff --git a/src/component/Icons/Earth.tsx b/src/component/Icons/Earth.tsx new file mode 100644 index 0000000..34f7b34 --- /dev/null +++ b/src/component/Icons/Earth.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const Earth = createSvgIcon( + , + "Earth", +); +export default Earth; diff --git a/src/component/Icons/Edit.tsx b/src/component/Icons/Edit.tsx new file mode 100644 index 0000000..fad3f8e --- /dev/null +++ b/src/component/Icons/Edit.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Edit(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/EditSetting.tsx b/src/component/Icons/EditSetting.tsx new file mode 100644 index 0000000..d15c63b --- /dev/null +++ b/src/component/Icons/EditSetting.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function EditSetting(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/EmailClock.tsx b/src/component/Icons/EmailClock.tsx new file mode 100644 index 0000000..0f2d80e --- /dev/null +++ b/src/component/Icons/EmailClock.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function EmailClock(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/EmojiEdit.tsx b/src/component/Icons/EmojiEdit.tsx new file mode 100644 index 0000000..6c25021 --- /dev/null +++ b/src/component/Icons/EmojiEdit.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const EmojiEdit = createSvgIcon( + , + "EmojiEdit", +); +export default EmojiEdit; diff --git a/src/component/Icons/Enter.tsx b/src/component/Icons/Enter.tsx new file mode 100644 index 0000000..f3179a3 --- /dev/null +++ b/src/component/Icons/Enter.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Enter(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Eye.tsx b/src/component/Icons/Eye.tsx new file mode 100644 index 0000000..173759d --- /dev/null +++ b/src/component/Icons/Eye.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Eye = createSvgIcon( + , + "Eye", +); + +export default Eye; diff --git a/src/component/Icons/FastForward.tsx b/src/component/Icons/FastForward.tsx new file mode 100644 index 0000000..3d25b6f --- /dev/null +++ b/src/component/Icons/FastForward.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FastForward(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FileAdd.tsx b/src/component/Icons/FileAdd.tsx new file mode 100644 index 0000000..e221ffd --- /dev/null +++ b/src/component/Icons/FileAdd.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FileAdd(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FileExclBox.tsx b/src/component/Icons/FileExclBox.tsx new file mode 100644 index 0000000..4f48ef8 --- /dev/null +++ b/src/component/Icons/FileExclBox.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FileExclBox(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FilePowerPointBox.tsx b/src/component/Icons/FilePowerPointBox.tsx new file mode 100644 index 0000000..2e4657d --- /dev/null +++ b/src/component/Icons/FilePowerPointBox.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FilePowerPointBox(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FileWordBox.tsx b/src/component/Icons/FileWordBox.tsx new file mode 100644 index 0000000..77ee733 --- /dev/null +++ b/src/component/Icons/FileWordBox.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FileWordBox(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FilmstripImage.tsx b/src/component/Icons/FilmstripImage.tsx new file mode 100644 index 0000000..f9721d8 --- /dev/null +++ b/src/component/Icons/FilmstripImage.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FilmstripImage(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Filter.tsx b/src/component/Icons/Filter.tsx new file mode 100644 index 0000000..8c4a498 --- /dev/null +++ b/src/component/Icons/Filter.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Filter(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Fingerprint.tsx b/src/component/Icons/Fingerprint.tsx new file mode 100644 index 0000000..20623d9 --- /dev/null +++ b/src/component/Icons/Fingerprint.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Fingerprint(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Folder.tsx b/src/component/Icons/Folder.tsx new file mode 100644 index 0000000..e436884 --- /dev/null +++ b/src/component/Icons/Folder.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Folder(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FolderAdd.tsx b/src/component/Icons/FolderAdd.tsx new file mode 100644 index 0000000..29f7e1c --- /dev/null +++ b/src/component/Icons/FolderAdd.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FolderAdd(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FolderArrowRightOutlined.tsx b/src/component/Icons/FolderArrowRightOutlined.tsx new file mode 100644 index 0000000..b8080b1 --- /dev/null +++ b/src/component/Icons/FolderArrowRightOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FolderArrowRightOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FolderArrowUp.tsx b/src/component/Icons/FolderArrowUp.tsx new file mode 100644 index 0000000..f41f3d3 --- /dev/null +++ b/src/component/Icons/FolderArrowUp.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FolderArrowUp(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FolderLink.tsx b/src/component/Icons/FolderLink.tsx new file mode 100644 index 0000000..dae8397 --- /dev/null +++ b/src/component/Icons/FolderLink.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const FolderLink = createSvgIcon( + , + "FolderLink", +); + +export default FolderLink; diff --git a/src/component/Icons/FolderOutlined.tsx b/src/component/Icons/FolderOutlined.tsx new file mode 100644 index 0000000..4f26023 --- /dev/null +++ b/src/component/Icons/FolderOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FolderOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/FolderZip.tsx b/src/component/Icons/FolderZip.tsx new file mode 100644 index 0000000..b5dc24f --- /dev/null +++ b/src/component/Icons/FolderZip.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function FolderZip(props: SvgIconProps) { + return ( + + + + + ); +} diff --git a/src/component/Icons/FullScreenMaximize.tsx b/src/component/Icons/FullScreenMaximize.tsx new file mode 100644 index 0000000..781f9b5 --- /dev/null +++ b/src/component/Icons/FullScreenMaximize.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const FullScreenMaximize = createSvgIcon( + , + "FullScreenMaximize", +); + +export default FullScreenMaximize; diff --git a/src/component/Icons/FullScreenMinimize.tsx b/src/component/Icons/FullScreenMinimize.tsx new file mode 100644 index 0000000..ef094eb --- /dev/null +++ b/src/component/Icons/FullScreenMinimize.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const FullScreenMinimize = createSvgIcon( + , + "FullScreenMinimize", +); + +export default FullScreenMinimize; diff --git a/src/component/Icons/Globe.tsx b/src/component/Icons/Globe.tsx new file mode 100644 index 0000000..fbbe732 --- /dev/null +++ b/src/component/Icons/Globe.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Globe(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/GlobeFilled.tsx b/src/component/Icons/GlobeFilled.tsx new file mode 100644 index 0000000..998c558 --- /dev/null +++ b/src/component/Icons/GlobeFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function GlobeFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Grid.tsx b/src/component/Icons/Grid.tsx new file mode 100644 index 0000000..3601771 --- /dev/null +++ b/src/component/Icons/Grid.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Grid(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/GridOutlined.tsx b/src/component/Icons/GridOutlined.tsx new file mode 100644 index 0000000..f8378b6 --- /dev/null +++ b/src/component/Icons/GridOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function GridOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/HardDrive.tsx b/src/component/Icons/HardDrive.tsx new file mode 100644 index 0000000..a923de5 --- /dev/null +++ b/src/component/Icons/HardDrive.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function HardDrive(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/HardDriveOutlined.tsx b/src/component/Icons/HardDriveOutlined.tsx new file mode 100644 index 0000000..6ad6daf --- /dev/null +++ b/src/component/Icons/HardDriveOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function HardDriveOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/HistoryOutlined.tsx b/src/component/Icons/HistoryOutlined.tsx new file mode 100644 index 0000000..6acf1cc --- /dev/null +++ b/src/component/Icons/HistoryOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function HistoryOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Home.tsx b/src/component/Icons/Home.tsx new file mode 100644 index 0000000..627b29d --- /dev/null +++ b/src/component/Icons/Home.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Home(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/HomeOutlined.tsx b/src/component/Icons/HomeOutlined.tsx new file mode 100644 index 0000000..3a65e4e --- /dev/null +++ b/src/component/Icons/HomeOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function HomeOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Image.tsx b/src/component/Icons/Image.tsx new file mode 100644 index 0000000..aa01ef6 --- /dev/null +++ b/src/component/Icons/Image.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Image(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ImageCopy.tsx b/src/component/Icons/ImageCopy.tsx new file mode 100644 index 0000000..942284e --- /dev/null +++ b/src/component/Icons/ImageCopy.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ImageCopy(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ImageCopyOutlined.tsx b/src/component/Icons/ImageCopyOutlined.tsx new file mode 100644 index 0000000..1be6d97 --- /dev/null +++ b/src/component/Icons/ImageCopyOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ImageCopyOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ImageEdit.tsx b/src/component/Icons/ImageEdit.tsx new file mode 100644 index 0000000..f45b614 --- /dev/null +++ b/src/component/Icons/ImageEdit.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const ImageEdit = createSvgIcon( + , + "ImageEdit", +); + +export default ImageEdit; diff --git a/src/component/Icons/ImageOffOutlined.tsx b/src/component/Icons/ImageOffOutlined.tsx new file mode 100644 index 0000000..e966708 --- /dev/null +++ b/src/component/Icons/ImageOffOutlined.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ImageOffOutlined(props: SvgIconProps) { + return ( + + + + + ); +} diff --git a/src/component/Icons/ImageOutlined.tsx b/src/component/Icons/ImageOutlined.tsx new file mode 100644 index 0000000..411cfa0 --- /dev/null +++ b/src/component/Icons/ImageOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ImageOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/InPrivate.tsx b/src/component/Icons/InPrivate.tsx new file mode 100644 index 0000000..3590243 --- /dev/null +++ b/src/component/Icons/InPrivate.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const InPrivate = createSvgIcon( + , + "InPrivate", +); +export default InPrivate; diff --git a/src/component/Icons/Info.tsx b/src/component/Icons/Info.tsx new file mode 100644 index 0000000..de98349 --- /dev/null +++ b/src/component/Icons/Info.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Info = createSvgIcon( + , + "Info", +); + +export default Info; diff --git a/src/component/Icons/InfoFilled.tsx b/src/component/Icons/InfoFilled.tsx new file mode 100644 index 0000000..d476d23 --- /dev/null +++ b/src/component/Icons/InfoFilled.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const InfoFilled = createSvgIcon( + , + "InfoFilled", +); + +export default InfoFilled; diff --git a/src/component/Icons/IosArrow.tsx b/src/component/Icons/IosArrow.tsx new file mode 100644 index 0000000..a885065 --- /dev/null +++ b/src/component/Icons/IosArrow.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function IosArrow(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/IosArrowLeft.tsx b/src/component/Icons/IosArrowLeft.tsx new file mode 100644 index 0000000..fd95f2a --- /dev/null +++ b/src/component/Icons/IosArrowLeft.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function IosArrowLeft(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguageC.tsx b/src/component/Icons/LanguageC.tsx new file mode 100644 index 0000000..b1929ac --- /dev/null +++ b/src/component/Icons/LanguageC.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguageC(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguageCPP.tsx b/src/component/Icons/LanguageCPP.tsx new file mode 100644 index 0000000..b0b3593 --- /dev/null +++ b/src/component/Icons/LanguageCPP.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguageCPP(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguageGo.tsx b/src/component/Icons/LanguageGo.tsx new file mode 100644 index 0000000..b107872 --- /dev/null +++ b/src/component/Icons/LanguageGo.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguageGo(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguageJS.tsx b/src/component/Icons/LanguageJS.tsx new file mode 100644 index 0000000..3b18ae1 --- /dev/null +++ b/src/component/Icons/LanguageJS.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguageJS(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguagePHP.tsx b/src/component/Icons/LanguagePHP.tsx new file mode 100644 index 0000000..5cc1e9e --- /dev/null +++ b/src/component/Icons/LanguagePHP.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguagePHP(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguagePython.tsx b/src/component/Icons/LanguagePython.tsx new file mode 100644 index 0000000..702d24e --- /dev/null +++ b/src/component/Icons/LanguagePython.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguagePython(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LanguageRust.tsx b/src/component/Icons/LanguageRust.tsx new file mode 100644 index 0000000..98a4d59 --- /dev/null +++ b/src/component/Icons/LanguageRust.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LanguageRust(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Link.tsx b/src/component/Icons/Link.tsx new file mode 100644 index 0000000..844adcb --- /dev/null +++ b/src/component/Icons/Link.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Link(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LinkDismiss.tsx b/src/component/Icons/LinkDismiss.tsx new file mode 100644 index 0000000..8cf8fac --- /dev/null +++ b/src/component/Icons/LinkDismiss.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const LinkDismiss = createSvgIcon( + , + "LinkDismiss", +); + +export default LinkDismiss; diff --git a/src/component/Icons/LinkEdit.tsx b/src/component/Icons/LinkEdit.tsx new file mode 100644 index 0000000..a46d77b --- /dev/null +++ b/src/component/Icons/LinkEdit.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LinkEdit(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LinkOutlined.tsx b/src/component/Icons/LinkOutlined.tsx new file mode 100644 index 0000000..6db714b --- /dev/null +++ b/src/component/Icons/LinkOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LinkOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LinkSetting.tsx b/src/component/Icons/LinkSetting.tsx new file mode 100644 index 0000000..16034c2 --- /dev/null +++ b/src/component/Icons/LinkSetting.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const LinkSetting = createSvgIcon( + , + "LinkSetting", +); + +export default LinkSetting; diff --git a/src/component/Icons/LockClosed.tsx b/src/component/Icons/LockClosed.tsx new file mode 100644 index 0000000..ab70211 --- /dev/null +++ b/src/component/Icons/LockClosed.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LockClosed(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/LockClosedKey.tsx b/src/component/Icons/LockClosedKey.tsx new file mode 100644 index 0000000..61f4e2d --- /dev/null +++ b/src/component/Icons/LockClosedKey.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const LockClosedKey = createSvgIcon( + , + "LockClosedKey", +); + +export default LockClosedKey; diff --git a/src/component/Icons/LockClosedOutlined.tsx b/src/component/Icons/LockClosedOutlined.tsx new file mode 100644 index 0000000..c6661c7 --- /dev/null +++ b/src/component/Icons/LockClosedOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LockClosedOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MagnetOn.tsx b/src/component/Icons/MagnetOn.tsx new file mode 100644 index 0000000..98ebaa1 --- /dev/null +++ b/src/component/Icons/MagnetOn.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MagnetOn(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MailOutlined.tsx b/src/component/Icons/MailOutlined.tsx new file mode 100644 index 0000000..aa30afa --- /dev/null +++ b/src/component/Icons/MailOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MailOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Markdown.tsx b/src/component/Icons/Markdown.tsx new file mode 100644 index 0000000..792bfc5 --- /dev/null +++ b/src/component/Icons/Markdown.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Markdown(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Money.tsx b/src/component/Icons/Money.tsx new file mode 100644 index 0000000..80d68f5 --- /dev/null +++ b/src/component/Icons/Money.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Money(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Moon.tsx b/src/component/Icons/Moon.tsx new file mode 100644 index 0000000..530624b --- /dev/null +++ b/src/component/Icons/Moon.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Moon(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MoreHorizontal.tsx b/src/component/Icons/MoreHorizontal.tsx new file mode 100644 index 0000000..dffab7c --- /dev/null +++ b/src/component/Icons/MoreHorizontal.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MoreHorizontal(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MoreVertical.tsx b/src/component/Icons/MoreVertical.tsx new file mode 100644 index 0000000..d2a82e6 --- /dev/null +++ b/src/component/Icons/MoreVertical.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const MoreVertical = createSvgIcon( + , + "MoreVertical", +); +export default MoreVertical; diff --git a/src/component/Icons/MusicNote1.tsx b/src/component/Icons/MusicNote1.tsx new file mode 100644 index 0000000..244e768 --- /dev/null +++ b/src/component/Icons/MusicNote1.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MusicNote1(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MusicNote1Outlined.tsx b/src/component/Icons/MusicNote1Outlined.tsx new file mode 100644 index 0000000..1679759 --- /dev/null +++ b/src/component/Icons/MusicNote1Outlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MusicNote1Outlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/MusicNote2.tsx b/src/component/Icons/MusicNote2.tsx new file mode 100644 index 0000000..09a2a3a --- /dev/null +++ b/src/component/Icons/MusicNote2.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MusicNote2(props: SvgIconProps) { + return ( + + + , + + ); +} diff --git a/src/component/Icons/MusicNote2Play.tsx b/src/component/Icons/MusicNote2Play.tsx new file mode 100644 index 0000000..3078046 --- /dev/null +++ b/src/component/Icons/MusicNote2Play.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function MusicNote2Play(props: SvgIconProps) { + return ( + + + , + + ); +} diff --git a/src/component/Icons/Notepad.tsx b/src/component/Icons/Notepad.tsx new file mode 100644 index 0000000..5427c74 --- /dev/null +++ b/src/component/Icons/Notepad.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Notepad(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Numbers.tsx b/src/component/Icons/Numbers.tsx new file mode 100644 index 0000000..07a7d4d --- /dev/null +++ b/src/component/Icons/Numbers.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Numbers(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Open.tsx b/src/component/Icons/Open.tsx new file mode 100644 index 0000000..677ff27 --- /dev/null +++ b/src/component/Icons/Open.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Open = createSvgIcon( + , + "Open", +); + +export default Open; diff --git a/src/component/Icons/OpenFilled.tsx b/src/component/Icons/OpenFilled.tsx new file mode 100644 index 0000000..f457c1d --- /dev/null +++ b/src/component/Icons/OpenFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function OpenFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Options.tsx b/src/component/Icons/Options.tsx new file mode 100644 index 0000000..d4e88c2 --- /dev/null +++ b/src/component/Icons/Options.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Options(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PackageOpen.tsx b/src/component/Icons/PackageOpen.tsx new file mode 100644 index 0000000..7abeb30 --- /dev/null +++ b/src/component/Icons/PackageOpen.tsx @@ -0,0 +1,17 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PackageOpen(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Password.tsx b/src/component/Icons/Password.tsx new file mode 100644 index 0000000..47990e3 --- /dev/null +++ b/src/component/Icons/Password.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Password(props: SvgIconProps) { + return ( + + + + + ); +} diff --git a/src/component/Icons/Pause.tsx b/src/component/Icons/Pause.tsx new file mode 100644 index 0000000..072b82b --- /dev/null +++ b/src/component/Icons/Pause.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Pause(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Payment.tsx b/src/component/Icons/Payment.tsx new file mode 100644 index 0000000..2edf93d --- /dev/null +++ b/src/component/Icons/Payment.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Payment(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PaymentFilled.tsx b/src/component/Icons/PaymentFilled.tsx new file mode 100644 index 0000000..cc5f349 --- /dev/null +++ b/src/component/Icons/PaymentFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PaymentFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/People.tsx b/src/component/Icons/People.tsx new file mode 100644 index 0000000..fe79502 --- /dev/null +++ b/src/component/Icons/People.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const People = createSvgIcon( + , + "People", +); +export default People; diff --git a/src/component/Icons/PeopleFilled.tsx b/src/component/Icons/PeopleFilled.tsx new file mode 100644 index 0000000..1a59367 --- /dev/null +++ b/src/component/Icons/PeopleFilled.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const PeopleFilled = createSvgIcon( + , + "PeopleFilled", +); +export default PeopleFilled; diff --git a/src/component/Icons/PeopleStar.tsx b/src/component/Icons/PeopleStar.tsx new file mode 100644 index 0000000..5f3a21a --- /dev/null +++ b/src/component/Icons/PeopleStar.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const PeopleStar = createSvgIcon( + , + "PeopleStar", +); +export default PeopleStar; diff --git a/src/component/Icons/PeopleTeam.tsx b/src/component/Icons/PeopleTeam.tsx new file mode 100644 index 0000000..5abb484 --- /dev/null +++ b/src/component/Icons/PeopleTeam.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PeopleTeam(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PeopleTeamOutlined.tsx b/src/component/Icons/PeopleTeamOutlined.tsx new file mode 100644 index 0000000..1a222a5 --- /dev/null +++ b/src/component/Icons/PeopleTeamOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PeopleTeamOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Person.tsx b/src/component/Icons/Person.tsx new file mode 100644 index 0000000..4d28044 --- /dev/null +++ b/src/component/Icons/Person.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Person(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PersonLock.tsx b/src/component/Icons/PersonLock.tsx new file mode 100644 index 0000000..2a0c498 --- /dev/null +++ b/src/component/Icons/PersonLock.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const PersonLock = createSvgIcon( + , + "PersonLock", +); + +export default PersonLock; diff --git a/src/component/Icons/PersonOutlined.tsx b/src/component/Icons/PersonOutlined.tsx new file mode 100644 index 0000000..fa4ddce --- /dev/null +++ b/src/component/Icons/PersonOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PersonOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PersonPasskey.tsx b/src/component/Icons/PersonPasskey.tsx new file mode 100644 index 0000000..b8f66f8 --- /dev/null +++ b/src/component/Icons/PersonPasskey.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PersonPasskey(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PersonStar.tsx b/src/component/Icons/PersonStar.tsx new file mode 100644 index 0000000..35d8018 --- /dev/null +++ b/src/component/Icons/PersonStar.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PersonStar(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PhoneLaptop.tsx b/src/component/Icons/PhoneLaptop.tsx new file mode 100644 index 0000000..fd06dfe --- /dev/null +++ b/src/component/Icons/PhoneLaptop.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PhoneLaptop(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PhoneLaptopOutlined.tsx b/src/component/Icons/PhoneLaptopOutlined.tsx new file mode 100644 index 0000000..3b4eb19 --- /dev/null +++ b/src/component/Icons/PhoneLaptopOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PhoneLaptopOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/PinOutlined.tsx b/src/component/Icons/PinOutlined.tsx new file mode 100644 index 0000000..f04c9ba --- /dev/null +++ b/src/component/Icons/PinOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function PinOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Play.tsx b/src/component/Icons/Play.tsx new file mode 100644 index 0000000..8bd85c6 --- /dev/null +++ b/src/component/Icons/Play.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Play(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Pulse.tsx b/src/component/Icons/Pulse.tsx new file mode 100644 index 0000000..3f53608 --- /dev/null +++ b/src/component/Icons/Pulse.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Pulse = createSvgIcon( + , + "Pulse", +); + +export default Pulse; diff --git a/src/component/Icons/QQ.tsx b/src/component/Icons/QQ.tsx new file mode 100644 index 0000000..a922ec9 --- /dev/null +++ b/src/component/Icons/QQ.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function QQ(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Raw.tsx b/src/component/Icons/Raw.tsx new file mode 100644 index 0000000..5985db9 --- /dev/null +++ b/src/component/Icons/Raw.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Raw(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/RenameFilled.tsx b/src/component/Icons/RenameFilled.tsx new file mode 100644 index 0000000..b0bfc87 --- /dev/null +++ b/src/component/Icons/RenameFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function RenameFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/RenameOutlined.tsx b/src/component/Icons/RenameOutlined.tsx new file mode 100644 index 0000000..9c92b7d --- /dev/null +++ b/src/component/Icons/RenameOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function RenameOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Save.tsx b/src/component/Icons/Save.tsx new file mode 100644 index 0000000..8ab199e --- /dev/null +++ b/src/component/Icons/Save.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Save(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Savings.tsx b/src/component/Icons/Savings.tsx new file mode 100644 index 0000000..0eaf4a7 --- /dev/null +++ b/src/component/Icons/Savings.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Savings(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Search.tsx b/src/component/Icons/Search.tsx new file mode 100644 index 0000000..7afc499 --- /dev/null +++ b/src/component/Icons/Search.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Search(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SendLogging.tsx b/src/component/Icons/SendLogging.tsx new file mode 100644 index 0000000..e704ce7 --- /dev/null +++ b/src/component/Icons/SendLogging.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SendLogging(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SendLoggingFilled.tsx b/src/component/Icons/SendLoggingFilled.tsx new file mode 100644 index 0000000..7603d78 --- /dev/null +++ b/src/component/Icons/SendLoggingFilled.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SendLoggingFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Server.tsx b/src/component/Icons/Server.tsx new file mode 100644 index 0000000..ea30e4f --- /dev/null +++ b/src/component/Icons/Server.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Server(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ServerFilled.tsx b/src/component/Icons/ServerFilled.tsx new file mode 100644 index 0000000..9a6abbc --- /dev/null +++ b/src/component/Icons/ServerFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ServerFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Setting.tsx b/src/component/Icons/Setting.tsx new file mode 100644 index 0000000..3251960 --- /dev/null +++ b/src/component/Icons/Setting.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Setting(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SettingsOutlined.tsx b/src/component/Icons/SettingsOutlined.tsx new file mode 100644 index 0000000..b0def8c --- /dev/null +++ b/src/component/Icons/SettingsOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SettingsOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Share.tsx b/src/component/Icons/Share.tsx new file mode 100644 index 0000000..aad4271 --- /dev/null +++ b/src/component/Icons/Share.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Share = createSvgIcon( + , + "Share", +); + +export default Share; diff --git a/src/component/Icons/ShareAndroid.tsx b/src/component/Icons/ShareAndroid.tsx new file mode 100644 index 0000000..350c3cf --- /dev/null +++ b/src/component/Icons/ShareAndroid.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShareAndroid(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ShareFilled.tsx b/src/component/Icons/ShareFilled.tsx new file mode 100644 index 0000000..99f81bd --- /dev/null +++ b/src/component/Icons/ShareFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShareFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ShareOutlined.tsx b/src/component/Icons/ShareOutlined.tsx new file mode 100644 index 0000000..e12b64f --- /dev/null +++ b/src/component/Icons/ShareOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShareOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Shield.tsx b/src/component/Icons/Shield.tsx new file mode 100644 index 0000000..e6755a6 --- /dev/null +++ b/src/component/Icons/Shield.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Shield(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ShieldAdd.tsx b/src/component/Icons/ShieldAdd.tsx new file mode 100644 index 0000000..f8cd5cc --- /dev/null +++ b/src/component/Icons/ShieldAdd.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShieldAdd(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/ShieldFilled.tsx b/src/component/Icons/ShieldFilled.tsx new file mode 100644 index 0000000..b08e93c --- /dev/null +++ b/src/component/Icons/ShieldFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function ShieldFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SignOut.tsx b/src/component/Icons/SignOut.tsx new file mode 100644 index 0000000..dfc8e78 --- /dev/null +++ b/src/component/Icons/SignOut.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SignOut(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Sparkle.tsx b/src/component/Icons/Sparkle.tsx new file mode 100644 index 0000000..2d34334 --- /dev/null +++ b/src/component/Icons/Sparkle.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Sparkle(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SparkleFilled.tsx b/src/component/Icons/SparkleFilled.tsx new file mode 100644 index 0000000..5ff162b --- /dev/null +++ b/src/component/Icons/SparkleFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SparkleFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/StarFilled.tsx b/src/component/Icons/StarFilled.tsx new file mode 100644 index 0000000..b7e3abe --- /dev/null +++ b/src/component/Icons/StarFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function StarFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Storage.tsx b/src/component/Icons/Storage.tsx new file mode 100644 index 0000000..55209c2 --- /dev/null +++ b/src/component/Icons/Storage.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Storage(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/StorageOutlined.tsx b/src/component/Icons/StorageOutlined.tsx new file mode 100644 index 0000000..ea8cb32 --- /dev/null +++ b/src/component/Icons/StorageOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function StorageOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Subtitles.tsx b/src/component/Icons/Subtitles.tsx new file mode 100644 index 0000000..eebc4c8 --- /dev/null +++ b/src/component/Icons/Subtitles.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Subtitles(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/SunWithTime.tsx b/src/component/Icons/SunWithTime.tsx new file mode 100644 index 0000000..a9cc69d --- /dev/null +++ b/src/component/Icons/SunWithTime.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function SunWithTime(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Sunny.tsx b/src/component/Icons/Sunny.tsx new file mode 100644 index 0000000..574b321 --- /dev/null +++ b/src/component/Icons/Sunny.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Sunny(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/TableSettings.tsx b/src/component/Icons/TableSettings.tsx new file mode 100644 index 0000000..9c2078c --- /dev/null +++ b/src/component/Icons/TableSettings.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TableSettingsOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Tag.tsx b/src/component/Icons/Tag.tsx new file mode 100644 index 0000000..0f47a4d --- /dev/null +++ b/src/component/Icons/Tag.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Tag = createSvgIcon( + , + "Tag", +); + +export default Tag; diff --git a/src/component/Icons/Tags.tsx b/src/component/Icons/Tags.tsx new file mode 100644 index 0000000..2eb78a4 --- /dev/null +++ b/src/component/Icons/Tags.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Tags = createSvgIcon( + , + "Tags", +); + +export default Tags; diff --git a/src/component/Icons/TaskList.tsx b/src/component/Icons/TaskList.tsx new file mode 100644 index 0000000..2905865 --- /dev/null +++ b/src/component/Icons/TaskList.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TaskList(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/TaskListOutlined.tsx b/src/component/Icons/TaskListOutlined.tsx new file mode 100644 index 0000000..cc206ca --- /dev/null +++ b/src/component/Icons/TaskListOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TaskListOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/TextCaseTitle.tsx b/src/component/Icons/TextCaseTitle.tsx new file mode 100644 index 0000000..1bdad8d --- /dev/null +++ b/src/component/Icons/TextCaseTitle.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TextCaseTitle(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/TextEditStyle.tsx b/src/component/Icons/TextEditStyle.tsx new file mode 100644 index 0000000..6d1be26 --- /dev/null +++ b/src/component/Icons/TextEditStyle.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const TextEditStyle = createSvgIcon( + , + "TextEditStyle", +); +export default TextEditStyle; diff --git a/src/component/Icons/TextGrammarLighting.tsx b/src/component/Icons/TextGrammarLighting.tsx new file mode 100644 index 0000000..a044c90 --- /dev/null +++ b/src/component/Icons/TextGrammarLighting.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TextGrammarLighting(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/TicketDiagonal.tsx b/src/component/Icons/TicketDiagonal.tsx new file mode 100644 index 0000000..7fad1a1 --- /dev/null +++ b/src/component/Icons/TicketDiagonal.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function TicketDiagonal(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Timer.tsx b/src/component/Icons/Timer.tsx new file mode 100644 index 0000000..73e8274 --- /dev/null +++ b/src/component/Icons/Timer.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const Timer = createSvgIcon( + , + "Timer", +); + +export default Timer; diff --git a/src/component/Icons/Translate.tsx b/src/component/Icons/Translate.tsx new file mode 100644 index 0000000..432b0a1 --- /dev/null +++ b/src/component/Icons/Translate.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Translate(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Upload.tsx b/src/component/Icons/Upload.tsx new file mode 100644 index 0000000..3b42b70 --- /dev/null +++ b/src/component/Icons/Upload.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Upload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/UploadFilled.tsx b/src/component/Icons/UploadFilled.tsx new file mode 100644 index 0000000..3e07029 --- /dev/null +++ b/src/component/Icons/UploadFilled.tsx @@ -0,0 +1,12 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function UploadFilled(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Video.tsx b/src/component/Icons/Video.tsx new file mode 100644 index 0000000..205147f --- /dev/null +++ b/src/component/Icons/Video.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Video(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/VideoOutlined.tsx b/src/component/Icons/VideoOutlined.tsx new file mode 100644 index 0000000..df11823 --- /dev/null +++ b/src/component/Icons/VideoOutlined.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function VideoOutlined(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/WalletCreditCard.tsx b/src/component/Icons/WalletCreditCard.tsx new file mode 100644 index 0000000..7abde4a --- /dev/null +++ b/src/component/Icons/WalletCreditCard.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const WalletCreditCard = createSvgIcon( + , + "WalletCreditCard", +); + +export default WalletCreditCard; diff --git a/src/component/Icons/Warning.tsx b/src/component/Icons/Warning.tsx new file mode 100644 index 0000000..299aab5 --- /dev/null +++ b/src/component/Icons/Warning.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function Warning(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/Whiteboard.tsx b/src/component/Icons/Whiteboard.tsx new file mode 100644 index 0000000..063e94d --- /dev/null +++ b/src/component/Icons/Whiteboard.tsx @@ -0,0 +1,8 @@ +import { createSvgIcon } from "@mui/material"; + +const WhiteBoard = createSvgIcon( + , + "WhiteBoard", +); + +export default WhiteBoard; diff --git a/src/component/Icons/WindowApps.tsx b/src/component/Icons/WindowApps.tsx new file mode 100644 index 0000000..cf7d9d4 --- /dev/null +++ b/src/component/Icons/WindowApps.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function WindowApps(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Icons/WrenchSettings.tsx b/src/component/Icons/WrenchSettings.tsx new file mode 100644 index 0000000..6f13959 --- /dev/null +++ b/src/component/Icons/WrenchSettings.tsx @@ -0,0 +1,7 @@ +import { createSvgIcon } from "@mui/material"; + +const WrenchSettings = createSvgIcon( + , + "WrenchSettings", +); +export default WrenchSettings; diff --git a/src/component/Login/Activication.js b/src/component/Login/Activication.js deleted file mode 100644 index 472bfbc..0000000 --- a/src/component/Login/Activication.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { - Avatar, - Button, - makeStyles, - Paper, - Typography, -} from "@material-ui/core"; -import { useHistory } from "react-router-dom"; -import API from "../../middleware/Api"; -import EmailIcon from "@material-ui/icons/EmailOutlined"; -import { useLocation } from "react-router"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up("sm")]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 110, - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( - 3 - )}px`, - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - submit: { - marginTop: theme.spacing(3), - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -function Activation() { - const { t } = useTranslation(); - const query = useQuery(); - const location = useLocation(); - - const [success, setSuccess] = useState(false); - const [email, setEmail] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const history = useHistory(); - - const classes = useStyles(); - - useEffect(() => { - API.get( - "/user/activate/" + query.get("id") + "?sign=" + query.get("sign") - ) - .then((response) => { - setEmail(response.data); - setSuccess(true); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "warning"); - history.push("/login"); - }); - // eslint-disable-next-line - }, [location]); - - return ( -
- {success && ( - - - - - - {t("login.activateSuccess")} - - - {t("login.accountActivated")} - - - - )} -
- ); -} - -export default Activation; diff --git a/src/component/Login/LoginForm.js b/src/component/Login/LoginForm.js deleted file mode 100644 index 8690781..0000000 --- a/src/component/Login/LoginForm.js +++ /dev/null @@ -1,478 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; -import { - Avatar, - Button, - Divider, - FormControl, - Link, - makeStyles, - Paper, - TextField, - Typography, -} from "@material-ui/core"; -import { Link as RouterLink, useHistory } from "react-router-dom"; -import API from "../../middleware/Api"; -import Auth from "../../middleware/Auth"; -import { bufferDecode, bufferEncode } from "../../utils/index"; -import { - EmailOutlined, - Fingerprint, - VpnKey, - VpnKeyOutlined, -} from "@material-ui/icons"; -import VpnIcon from "@material-ui/icons/VpnKeyOutlined"; -import { useLocation } from "react-router"; -import { useCaptcha } from "../../hooks/useCaptcha"; -import { - applyThemes, - setSessionStatus, - toggleSnackbar, -} from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import { useTheme } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up("sm")]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 110, - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( - 3 - )}px`, - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: "100%", // Fix IE 11 issue. - marginTop: theme.spacing(1), - }, - submit: { - marginTop: theme.spacing(3), - }, - link: { - marginTop: "20px", - display: "flex", - width: "100%", - justifyContent: "space-between", - }, - buttonContainer: { - display: "flex", - }, - authnLink: { - textAlign: "center", - marginTop: 16, - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -function LoginForm() { - const { t } = useTranslation(); - - const [email, setEmail] = useState(""); - const [pwd, setPwd] = useState(""); - const [loading, setLoading] = useState(false); - const [useAuthn, setUseAuthn] = useState(false); - const [twoFA, setTwoFA] = useState(false); - const [faCode, setFACode] = useState(""); - - const loginCaptcha = useSelector((state) => state.siteConfig.loginCaptcha); - const registerEnabled = useSelector( - (state) => state.siteConfig.registerEnabled - ); - const title = useSelector((state) => state.siteConfig.title); - const authn = useSelector((state) => state.siteConfig.authn); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const ApplyThemes = useCallback( - (theme) => dispatch(applyThemes(theme)), - [dispatch] - ); - const SetSessionStatus = useCallback( - (status) => dispatch(setSessionStatus(status)), - [dispatch] - ); - - const history = useHistory(); - const location = useLocation(); - const { - captchaLoading, - isValidate, - validate, - CaptchaRender, - captchaRefreshRef, - captchaParamsRef, - } = useCaptcha(); - const query = useQuery(); - - const classes = useStyles(); - - useEffect(() => { - setEmail(query.get("username")); - }, [location]); - - const afterLogin = (data) => { - Auth.authenticate(data); - - // 设置用户主题色 - if (data["preferred_theme"] !== "") { - ApplyThemes(data["preferred_theme"]); - } - - // 设置登录状态 - SetSessionStatus(true); - - // eslint-disable-next-line react-hooks/rules-of-hooks - if (query.get("redirect")) { - history.push(query.get("redirect")); - } else { - history.push("/home"); - } - ToggleSnackbar("top", "right", t("login.success"), "success"); - - localStorage.removeItem("siteConfigCache"); - }; - - const authnLogin = (e) => { - e.preventDefault(); - if (!navigator.credentials) { - ToggleSnackbar( - "top", - "right", - t("login.browserNotSupport"), - "warning" - ); - - return; - } - - setLoading(true); - - API.get("/user/authn/" + email) - .then((response) => { - const credentialRequestOptions = response.data; - console.log(credentialRequestOptions); - credentialRequestOptions.publicKey.challenge = bufferDecode( - credentialRequestOptions.publicKey.challenge - ); - credentialRequestOptions.publicKey.allowCredentials.forEach( - function (listItem) { - listItem.id = bufferDecode(listItem.id); - } - ); - - return navigator.credentials.get({ - publicKey: credentialRequestOptions.publicKey, - }); - }) - .then((assertion) => { - const authData = assertion.response.authenticatorData; - const clientDataJSON = assertion.response.clientDataJSON; - const rawId = assertion.rawId; - const sig = assertion.response.signature; - const userHandle = assertion.response.userHandle; - - return API.post( - "/user/authn/finish/" + email, - JSON.stringify({ - id: assertion.id, - rawId: bufferEncode(rawId), - type: assertion.type, - response: { - authenticatorData: bufferEncode(authData), - clientDataJSON: bufferEncode(clientDataJSON), - signature: bufferEncode(sig), - userHandle: bufferEncode(userHandle), - }, - }) - ); - }) - .then((response) => { - afterLogin(response.data); - }) - .catch((error) => { - console.log(error); - ToggleSnackbar("top", "right", error.message, "warning"); - }) - .then(() => { - setLoading(false); - }); - }; - - const login = (e) => { - e.preventDefault(); - setLoading(true); - if (!isValidate.current.isValidate && loginCaptcha) { - validate(() => login(e), setLoading); - return; - } - API.post("/user/session", { - userName: email, - Password: pwd, - ...captchaParamsRef.current, - }) - .then((response) => { - setLoading(false); - if (response.rawData.code === 203) { - setTwoFA(true); - } else { - afterLogin(response.data); - } - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "warning"); - captchaRefreshRef.current(); - }); - }; - - const twoFALogin = (e) => { - e.preventDefault(); - setLoading(true); - API.post("/user/2fa", { - code: faCode, - }) - .then((response) => { - setLoading(false); - afterLogin(response.data); - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "warning"); - }); - }; - - return ( -
- {!twoFA && ( - <> - - - - - - {t("login.title", { title })} - - {!useAuthn && ( -
- - - setEmail(e.target.value) - } - InputProps={{ - startAdornment: !isMobile && ( - - - - ), - }} - autoComplete - value={email} - autoFocus - /> - - - setPwd(e.target.value)} - InputProps={{ - startAdornment: !isMobile && ( - - - - ), - }} - value={pwd} - autoComplete - /> - - {loginCaptcha && } - - - - )} - {useAuthn && ( -
- - - - - ), - }} - onChange={(e) => - setEmail(e.target.value) - } - autoComplete - value={email} - autoFocus - required - /> - - -
- )} - -
-
- - {t("login.forgetPassword")} - -
-
- {registerEnabled && ( - - {t("login.signUpAccount")} - - )} -
-
-
- - {authn && ( -
- -
- )} - - )} - {twoFA && ( - - - - - - {t("login.2FA")} - -
- - - setFACode(event.target.value) - } - autoComplete - value={faCode} - autoFocus - /> - - {" "} -
{" "} - -
- )} -
- ); -} - -export default LoginForm; diff --git a/src/component/Login/ReCaptcha.js b/src/component/Login/ReCaptcha.js deleted file mode 100644 index b6c6f83..0000000 --- a/src/component/Login/ReCaptcha.js +++ /dev/null @@ -1,15 +0,0 @@ -import ReCAPTCHA from "./ReCaptchaWrapper"; -import makeAsyncScriptLoader from "react-async-script"; - -const callbackName = "onloadcallback"; -const globalName = "grecaptcha"; - -function getURL() { - const hostname = "recaptcha.net"; - return `https://${hostname}/recaptcha/api.js?onload=${callbackName}&render=explicit`; -} - -export default makeAsyncScriptLoader(getURL, { - callbackName, - globalName, -})(ReCAPTCHA); diff --git a/src/component/Login/ReCaptchaWrapper.js b/src/component/Login/ReCaptchaWrapper.js deleted file mode 100644 index 7fc46c3..0000000 --- a/src/component/Login/ReCaptchaWrapper.js +++ /dev/null @@ -1,173 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -export default class ReCAPTCHA extends React.Component { - constructor() { - super(); - this.handleExpired = this.handleExpired.bind(this); - this.handleErrored = this.handleErrored.bind(this); - this.handleChange = this.handleChange.bind(this); - this.handleRecaptchaRef = this.handleRecaptchaRef.bind(this); - } - - getValue() { - if (this.props.grecaptcha && this._widgetId !== undefined) { - return this.props.grecaptcha.getResponse(this._widgetId); - } - return null; - } - - getWidgetId() { - if (this.props.grecaptcha && this._widgetId !== undefined) { - return this._widgetId; - } - return null; - } - - execute() { - const { grecaptcha } = this.props; - - if (grecaptcha && this._widgetId !== undefined) { - return grecaptcha.execute(this._widgetId); - } else { - this._executeRequested = true; - } - } - - reset() { - if (this.props.grecaptcha && this._widgetId !== undefined) { - this.props.grecaptcha.reset(this._widgetId); - } - } - - handleExpired() { - if (this.props.onExpired) { - this.props.onExpired(); - } else { - this.handleChange(null); - } - } - - handleErrored() { - if (this.props.onErrored) this.props.onErrored(); - } - - handleChange(token) { - if (this.props.onChange) this.props.onChange(token); - } - - explicitRender() { - if ( - this.props.grecaptcha && - this.props.grecaptcha.render && - this._widgetId === undefined - ) { - const wrapper = document.createElement("div"); - this._widgetId = this.props.grecaptcha.render(wrapper, { - sitekey: this.props.sitekey, - callback: this.handleChange, - theme: this.props.theme, - type: this.props.type, - tabindex: this.props.tabindex, - "expired-callback": this.handleExpired, - "error-callback": this.handleErrored, - size: this.props.size, - stoken: this.props.stoken, - hl: this.props.hl, - badge: this.props.badge, - }); - this.captcha.appendChild(wrapper); - } - if ( - this._executeRequested && - this.props.grecaptcha && - this._widgetId !== undefined - ) { - this._executeRequested = false; - this.execute(); - } - } - - componentDidMount() { - this.explicitRender(); - } - - componentDidUpdate() { - this.explicitRender(); - } - - componentWillUnmount() { - if (this._widgetId !== undefined) { - this.delayOfCaptchaIframeRemoving(); - this.reset(); - } - } - - delayOfCaptchaIframeRemoving() { - const temporaryNode = document.createElement("div"); - document.body.appendChild(temporaryNode); - temporaryNode.style.display = "none"; - - // move of the recaptcha to a temporary node - while (this.captcha.firstChild) { - temporaryNode.appendChild(this.captcha.firstChild); - } - - // delete the temporary node after reset will be done - setTimeout(() => { - document.body.removeChild(temporaryNode); - }, 5000); - } - - handleRecaptchaRef(elem) { - this.captcha = elem; - } - - render() { - // consume properties owned by the reCATPCHA, pass the rest to the div so the user can style it. - /* eslint-disable no-unused-vars */ - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - sitekey, - onChange, - theme, - type, - tabindex, - onExpired, - onErrored, - size, - stoken, - grecaptcha, - badge, - hl, - ...childProps - } = this.props; - /* eslint-enable no-unused-vars */ - return
; - } -} - -ReCAPTCHA.displayName = "ReCAPTCHA"; -ReCAPTCHA.propTypes = { - sitekey: PropTypes.string.isRequired, - onChange: PropTypes.func, - grecaptcha: PropTypes.object, - theme: PropTypes.oneOf(["dark", "light"]), - type: PropTypes.oneOf(["image", "audio"]), - tabindex: PropTypes.number, - onExpired: PropTypes.func, - onErrored: PropTypes.func, - size: PropTypes.oneOf(["compact", "normal", "invisible"]), - stoken: PropTypes.string, - hl: PropTypes.string, - badge: PropTypes.oneOf(["bottomright", "bottomleft", "inline"]), -}; -ReCAPTCHA.defaultProps = { - // eslint-disable-next-line @typescript-eslint/no-empty-function - onChange: () => {}, - theme: "light", - type: "image", - tabindex: 0, - size: "normal", - badge: "bottomright", -}; diff --git a/src/component/Login/Register.js b/src/component/Login/Register.js deleted file mode 100644 index 4206930..0000000 --- a/src/component/Login/Register.js +++ /dev/null @@ -1,294 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import RegIcon from "@material-ui/icons/AssignmentIndOutlined"; -import { - Avatar, - Button, - Divider, - FormControl, - Input, - InputLabel, - Link, - makeStyles, - Paper, - TextField, - Typography, -} from "@material-ui/core"; -import { Link as RouterLink, useHistory } from "react-router-dom"; -import API from "../../middleware/Api"; -import EmailIcon from "@material-ui/icons/EmailOutlined"; -import { useCaptcha } from "../../hooks/useCaptcha"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import { EmailOutlined, VpnKeyOutlined } from "@material-ui/icons"; -import { useTheme } from "@material-ui/core/styles"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up("sm")]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 110, - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( - 3 - )}px`, - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: "100%", // Fix IE 11 issue. - marginTop: theme.spacing(1), - }, - submit: { - marginTop: theme.spacing(3), - }, - link: { - marginTop: "20px", - display: "flex", - width: "100%", - justifyContent: "space-between", - }, - buttonContainer: { - display: "flex", - }, - authnLink: { - textAlign: "center", - marginTop: 16, - }, - avatarSuccess: { - margin: theme.spacing(1), - backgroundColor: theme.palette.primary.main, - }, -})); - -function Register() { - const { t } = useTranslation(); - - const [input, setInput] = useState({ - email: "", - password: "", - password_repeat: "", - }); - const [loading, setLoading] = useState(false); - const [emailActive, setEmailActive] = useState(false); - - const title = useSelector((state) => state.siteConfig.title); - const regCaptcha = useSelector((state) => state.siteConfig.regCaptcha); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const history = useHistory(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - - const handleInputChange = (name) => (e) => { - setInput({ - ...input, - [name]: e.target.value, - }); - }; - - const { - captchaLoading, - isValidate, - validate, - CaptchaRender, - captchaRefreshRef, - captchaParamsRef, - } = useCaptcha(); - const classes = useStyles(); - - const register = (e) => { - e.preventDefault(); - - if (input.password !== input.password_repeat) { - ToggleSnackbar( - "top", - "right", - t("login.passwordNotMatch"), - "warning" - ); - return; - } - - setLoading(true); - if (!isValidate.current.isValidate && regCaptcha) { - validate(() => register(e), setLoading); - return; - } - API.post("/user", { - userName: input.email, - Password: input.password, - ...captchaParamsRef.current, - }) - .then((response) => { - setLoading(false); - if (response.rawData.code === 203) { - setEmailActive(true); - } else { - history.push("/login?username=" + input.email); - ToggleSnackbar( - "top", - "right", - t("login.signUpSuccess"), - "success" - ); - } - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "warning"); - captchaRefreshRef.current(); - }); - }; - - return ( -
- <> - {!emailActive && ( - - - - - - {t("login.sinUpTitle", { title })} - - -
- - - - - ), - }} - onChange={handleInputChange("email")} - autoComplete - value={input.email} - autoFocus - /> - - - - - - ), - }} - onChange={handleInputChange("password")} - value={input.password} - autoComplete - /> - - - - - - ), - }} - value={input.password_repeat} - autoComplete - /> - - {regCaptcha && } - - - - - -
-
- - {t("login.backToSingIn")} - -
-
- - {t("login.forgetPassword")} - -
-
-
- )} - {emailActive && ( - - - - - - {t("login.activateTitle")} - - - {t("login.activateDescription")} - - - )} - -
- ); -} - -export default Register; diff --git a/src/component/Login/Reset.js b/src/component/Login/Reset.js deleted file mode 100644 index 07aa8cf..0000000 --- a/src/component/Login/Reset.js +++ /dev/null @@ -1,196 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { - Avatar, - Button, - Divider, - FormControl, - Input, - InputLabel, - Link, - makeStyles, - Paper, - TextField, - Typography, -} from "@material-ui/core"; -import API from "../../middleware/Api"; -import KeyIcon from "@material-ui/icons/VpnKeyOutlined"; -import { useCaptcha } from "../../hooks/useCaptcha"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; -import { Link as RouterLink } from "react-router-dom"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import { EmailOutlined } from "@material-ui/icons"; -import { useTheme } from "@material-ui/core/styles"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up("sm")]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 110, - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( - 3 - )}px`, - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - submit: { - marginTop: theme.spacing(3), - }, - link: { - marginTop: "20px", - display: "flex", - width: "100%", - justifyContent: "space-between", - }, -})); - -function Reset() { - const { t } = useTranslation(); - - const [input, setInput] = useState({ - email: "", - }); - const [loading, setLoading] = useState(false); - const forgetCaptcha = useSelector( - (state) => state.siteConfig.forgetCaptcha - ); - const registerEnabled = useSelector( - (state) => state.siteConfig.registerEnabled - ); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const handleInputChange = (name) => (e) => { - setInput({ - ...input, - [name]: e.target.value, - }); - }; - - const { - captchaLoading, - isValidate, - validate, - CaptchaRender, - captchaRefreshRef, - captchaParamsRef, - } = useCaptcha(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - - const submit = (e) => { - e.preventDefault(); - setLoading(true); - if (!isValidate.current.isValidate && forgetCaptcha) { - validate(() => submit(e), setLoading); - return; - } - API.post("/user/reset", { - userName: input.email, - ...captchaParamsRef.current, - }) - .then(() => { - setLoading(false); - ToggleSnackbar( - "top", - "right", - t("login.resetEmailSent"), - "success" - ); - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "warning"); - captchaRefreshRef.current(); - }); - }; - - const classes = useStyles(); - - return ( -
- - - - - - {t("login.findMyPassword")} - -
- - - - - ), - }} - onChange={handleInputChange("email")} - autoComplete - value={input.email} - autoFocus - /> - - {forgetCaptcha && } - {" "} - {" "} - -
-
- - {t("login.backToSingIn")} - -
-
- {registerEnabled && ( - - {t("login.signUpAccount")} - - )} -
-
-
-
- ); -} - -export default Reset; diff --git a/src/component/Login/ResetForm.js b/src/component/Login/ResetForm.js deleted file mode 100644 index b3d70f6..0000000 --- a/src/component/Login/ResetForm.js +++ /dev/null @@ -1,213 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { - Avatar, - Button, - Divider, - FormControl, - Link, - makeStyles, - Paper, - TextField, - Typography, -} from "@material-ui/core"; -import { Link as RouterLink, useHistory } from "react-router-dom"; -import API from "../../middleware/Api"; -import { useLocation } from "react-router"; -import KeyIcon from "@material-ui/icons/VpnKeyOutlined"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import { VpnKeyOutlined } from "@material-ui/icons"; -import { useTheme } from "@material-ui/core/styles"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up("sm")]: { - width: 400, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 110, - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing( - 3 - )}px`, - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - submit: { - marginTop: theme.spacing(3), - }, - link: { - marginTop: "20px", - display: "flex", - width: "100%", - justifyContent: "space-between", - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -function ResetForm() { - const { t } = useTranslation(); - const query = useQuery(); - const [input, setInput] = useState({ - password: "", - password_repeat: "", - }); - const [loading, setLoading] = useState(false); - const registerEnabled = useSelector( - (state) => state.siteConfig.registerEnabled - ); - const handleInputChange = (name) => (e) => { - setInput({ - ...input, - [name]: e.target.value, - }); - }; - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const history = useHistory(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - - const submit = (e) => { - e.preventDefault(); - if (input.password !== input.password_repeat) { - ToggleSnackbar( - "top", - "right", - t("login.passwordNotMatch"), - "warning" - ); - return; - } - setLoading(true); - API.patch("/user/reset", { - secret: query.get("sign"), - id: query.get("id"), - Password: input.password, - }) - .then(() => { - setLoading(false); - history.push("/login"); - ToggleSnackbar( - "top", - "right", - t("login.passwordReset"), - "success" - ); - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "warning"); - }); - }; - - const classes = useStyles(); - - return ( -
- - - - - - {t("login.findMyPassword")} - -
- - - - - ), - }} - onChange={handleInputChange("password")} - autoComplete - value={input.password} - autoFocus - /> - - - - - - ), - }} - onChange={handleInputChange("password_repeat")} - autoComplete - value={input.password_repeat} - autoFocus - /> - - {" "} -
{" "} - -
-
- - {t("login.backToSingIn")} - -
-
- {registerEnabled && ( - - {t("login.signUpAccount")} - - )} -
-
-
-
- ); -} - -export default ResetForm; diff --git a/src/component/Modals/AddTag.js b/src/component/Modals/AddTag.js deleted file mode 100644 index d454219..0000000 --- a/src/component/Modals/AddTag.js +++ /dev/null @@ -1,411 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - makeStyles, - useTheme, -} from "@material-ui/core"; -import PathSelector from "../FileManager/PathSelector"; -import { useDispatch } from "react-redux"; -import API from "../../middleware/Api"; -import AppBar from "@material-ui/core/AppBar"; -import Tabs from "@material-ui/core/Tabs"; -import Tab from "@material-ui/core/Tab"; -import TextField from "@material-ui/core/TextField"; -import Typography from "@material-ui/core/Typography"; -import FormLabel from "@material-ui/core/FormLabel"; -import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup"; -import ToggleButton from "@material-ui/lab/ToggleButton"; -import { - Circle, - CircleOutline, - Heart, - HeartOutline, - Hexagon, - HexagonOutline, - Hexagram, - HexagramOutline, - Rhombus, - RhombusOutline, - Square, - SquareOutline, - Triangle, -} from "mdi-material-ui"; -import { toggleSnackbar } from "../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, - content: { - padding: 0, - marginTop: 0, - }, - marginTop: { - marginTop: theme.spacing(2), - display: "block", - }, - textField: { - marginTop: theme.spacing(1), - }, - scroll: { - overflowX: "auto", - }, - dialogContent: { - marginTop: theme.spacing(2), - }, - pathSelect: { - marginTop: theme.spacing(2), - display: "flex", - }, -})); - -const icons = { - Circle: , - CircleOutline: , - Heart: , - HeartOutline: , - Hexagon: , - HexagonOutline: , - Hexagram: , - HexagramOutline: , - Rhombus: , - RhombusOutline: , - Square: , - SquareOutline: , - Triangle: , -}; - -export default function AddTag(props) { - const theme = useTheme(); - const { t } = useTranslation(); - - const [value, setValue] = React.useState(0); - const [loading, setLoading] = React.useState(false); - const [alignment, setAlignment] = React.useState("Circle"); - const [color, setColor] = React.useState(theme.palette.text.secondary); - const [input, setInput] = React.useState({ - filename: "", - tagName: "", - path: "/", - }); - const [pathSelectDialog, setPathSelectDialog] = React.useState(false); - const [selectedPath, setSelectedPath] = useState(""); - // eslint-disable-next-line - const [selectedPathName, setSelectedPathName] = useState(""); - const setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const handleChange = (event, newValue) => { - setValue(newValue); - }; - - const handleIconChange = (event, newAlignment) => { - if (newAlignment) { - setAlignment(newAlignment); - } - }; - - const handleColorChange = (event, newAlignment) => { - if (newAlignment) { - setColor(newAlignment); - } - }; - - const handleInputChange = (name) => (event) => { - setInput({ - ...input, - [name]: event.target.value, - }); - }; - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitNewLink = () => { - setLoading(true); - - API.post("/tag/link", { - path: input.path, - name: input.tagName, - }) - .then((response) => { - setLoading(false); - props.onClose(); - props.onSuccess({ - type: 1, - name: input.tagName, - expression: input.path, - color: theme.palette.text.secondary, - icon: "FolderHeartOutline", - id: response.data, - }); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - - const submitNewTag = () => { - setLoading(true); - - API.post("/tag/filter", { - expression: input.filename, - name: input.tagName, - color: color, - icon: alignment, - }) - .then((response) => { - setLoading(false); - props.onClose(); - props.onSuccess({ - type: 0, - name: input.tagName, - color: color, - icon: alignment, - id: response.data, - }); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setLoading(false); - }); - }; - const submit = () => { - if (value === 0) { - submitNewTag(); - } else { - submitNewLink(); - } - }; - const selectPath = () => { - setInput({ - ...input, - path: selectedPath === "//" ? "/" : selectedPath, - }); - setPathSelectDialog(false); - }; - - const classes = useStyles(); - - return ( - - setPathSelectDialog(false)} - aria-labelledby="form-dialog-title" - > - - {t("navbar.addTagDialog.selectFolder")} - - - - - - - - - - - - - - - - {value === 0 && ( - - - - - - {[, ]} - - - - {t("navbar.addTagDialog.icon")} - -
- - {Object.keys(icons).map((key, index) => ( - - {icons[key]} - - ))} - -
- - {t("navbar.addTagDialog.color")} - -
- - {[ - theme.palette.text.secondary, - "#f44336", - "#e91e63", - "#9c27b0", - "#673ab7", - "#3f51b5", - "#2196f3", - "#03a9f4", - "#00bcd4", - "#009688", - "#4caf50", - "#cddc39", - "#ffeb3b", - "#ffc107", - "#ff9800", - "#ff5722", - "#795548", - "#9e9e9e", - "#607d8b", - ].map((key, index) => ( - - - - ))} - -
-
- )} - {value === 1 && ( - - -
- - -
-
- )} - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/Compress.js b/src/component/Modals/Compress.js deleted file mode 100644 index 3225933..0000000 --- a/src/component/Modals/Compress.js +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - makeStyles, -} from "@material-ui/core"; -import PathSelector from "../FileManager/PathSelector"; -import { useDispatch } from "react-redux"; -import TextField from "@material-ui/core/TextField"; -import { setModalsLoading, toggleSnackbar } from "../../redux/explorer"; -import { submitCompressTask } from "../../redux/explorer/action"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - backgroundColor: theme.palette.background.default, - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, -})); - -export default function CompressDialog(props) { - const { t } = useTranslation(); - const [selectedPath, setSelectedPath] = useState(""); - const [fileName, setFileName] = useState(""); - // eslint-disable-next-line - const [selectedPathName, setSelectedPathName] = useState(""); - - const dispatch = useDispatch(); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const SetModalsLoading = useCallback( - (status) => { - dispatch(setModalsLoading(status)); - }, - [dispatch] - ); - - const SubmitCompressTask = useCallback( - (name, path) => dispatch(submitCompressTask(name, path)), - [dispatch] - ); - - const setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const submitMove = (e) => { - if (e != null) { - e.preventDefault(); - } - SetModalsLoading(true); - - SubmitCompressTask(fileName, selectedPath) - .then(() => { - props.onClose(); - ToggleSnackbar( - "top", - "right", - t("modals.taskCreated"), - "success" - ); - SetModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - SetModalsLoading(false); - }); - }; - - const classes = useStyles(); - - return ( - - - {t("modals.saveToTitle")} - - - - {selectedPath !== "" && ( - - - setFileName(e.target.value)} - value={fileName} - fullWidth - autoFocus - id="standard-basic" - label={t("modals.zipFileName")} - /> - - - )} - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/ConcurrentOption.js b/src/component/Modals/ConcurrentOption.js deleted file mode 100644 index 7881b8e..0000000 --- a/src/component/Modals/ConcurrentOption.js +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from "react"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Input, - InputLabel, - makeStyles, -} from "@material-ui/core"; -import FormControl from "@material-ui/core/FormControl"; -import Auth from "../../middleware/Auth"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({})); - -export default function ConcurrentOptionDialog({ open, onClose, onSave }) { - const { t } = useTranslation(); - const [count, setCount] = useState( - Auth.GetPreferenceWithDefault("concurrent_limit", "5") - ); - const classes = useStyles(); - - return ( - - - {t("uploader.setConcurrent")} - - - - - - {t("uploader.concurrentTaskNumber")} - - setCount(e.target.value)} - /> - - - - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/Copy.js b/src/component/Modals/Copy.js deleted file mode 100644 index 438569b..0000000 --- a/src/component/Modals/Copy.js +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - makeStyles, -} from "@material-ui/core"; -import PathSelector from "../FileManager/PathSelector"; -import { useDispatch } from "react-redux"; -import API from "../../middleware/Api"; -import { - refreshFileList, - setModalsLoading, - toggleSnackbar, -} from "../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, -})); - -export default function CopyDialog(props) { - const { t } = useTranslation(); - const [selectedPath, setSelectedPath] = useState(""); - const [selectedPathName, setSelectedPathName] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const SetModalsLoading = useCallback( - (status) => { - dispatch(setModalsLoading(status)); - }, - [dispatch] - ); - const RefreshFileList = useCallback(() => { - dispatch(refreshFileList()); - }, [dispatch]); - - const setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const submitMove = (e) => { - if (e != null) { - e.preventDefault(); - } - SetModalsLoading(true); - const dirs = [], - items = []; - // eslint-disable-next-line - - if (props.selected[0].type === "dir") { - dirs.push(props.selected[0].id); - } else { - items.push(props.selected[0].id); - } - - API.post("/object/copy", { - src_dir: props.selected[0].path, - src: { - dirs: dirs, - items: items, - }, - dst: selectedPath === "//" ? "/" : selectedPath, - }) - .then(() => { - props.onClose(); - RefreshFileList(); - SetModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - SetModalsLoading(false); - }); - }; - - const classes = useStyles(); - - return ( - - - {t("fileManager.copyTo")} - - - - {selectedPath !== "" && ( - - - ]} - /> - - - )} - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/CreateShare.js b/src/component/Modals/CreateShare.js deleted file mode 100644 index 285eddd..0000000 --- a/src/component/Modals/CreateShare.js +++ /dev/null @@ -1,570 +0,0 @@ -import React, { useCallback, useRef } from "react"; -import { - Button, - Checkbox, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControl, - Input, - makeStyles, - TextField, -} from "@material-ui/core"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useDispatch } from "react-redux"; -import API from "../../middleware/Api"; -import List from "@material-ui/core/List"; -import ListItemText from "@material-ui/core/ListItemText"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import LockIcon from "@material-ui/icons/Lock"; -import TimerIcon from "@material-ui/icons/Timer"; -import CasinoIcon from "@material-ui/icons/Casino"; -import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; -import Divider from "@material-ui/core/Divider"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import Typography from "@material-ui/core/Typography"; -import withStyles from "@material-ui/core/styles/withStyles"; -import InputLabel from "@material-ui/core/InputLabel"; -import { Visibility, VisibilityOff } from "@material-ui/icons"; -import IconButton from "@material-ui/core/IconButton"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import Tooltip from "@material-ui/core/Tooltip"; -import MenuItem from "@material-ui/core/MenuItem"; -import Select from "@material-ui/core/Select"; -import ToggleIcon from "material-ui-toggle-icon"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - widthAnimation: {}, - shareUrl: { - minWidth: "400px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - }, - flexCenter: { - alignItems: "center", - }, - noFlex: { - display: "block", - }, - scoreCalc: { - marginTop: 10, - }, - expireLabel: { - whiteSpace: "nowrap", - }, -})); - -const ExpansionPanel = withStyles({ - root: { - border: "0px solid rgba(0, 0, 0, .125)", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": { - margin: "auto", - }, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles({ - root: { - padding: 0, - "&$expanded": {}, - }, - content: { - margin: 0, - display: "initial", - "&$expanded": { - margin: "0 0", - }, - }, - expanded: {}, -})(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - padding: 24, - backgroundColor: theme.palette.background.default, - }, -}))(MuiExpansionPanelDetails); - -export default function CreatShare(props) { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const classes = useStyles(); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const lastSubmit = useRef(null); - const [expanded, setExpanded] = React.useState(false); - const [shareURL, setShareURL] = React.useState(""); - const [values, setValues] = React.useState({ - password: "", - downloads: 1, - expires: 24 * 3600, - showPassword: false, - }); - const [shareOption, setShareOption] = React.useState({ - password: false, - expire: false, - preview: true, - }); - const [customExpires, setCustomExpires] = React.useState(3600); - const [customDownloads, setCustomDownloads] = React.useState(10); - - const handleChange = (prop) => (event) => { - // 输入密码 - if (prop === "password") { - if (event.target.value === "") { - setShareOption({ ...shareOption, password: false }); - } else { - setShareOption({ ...shareOption, password: true }); - } - } - - setValues({ ...values, [prop]: event.target.value }); - }; - - const handleClickShowPassword = () => { - setValues({ ...values, showPassword: !values.showPassword }); - }; - - const handleMouseDownPassword = (event) => { - event.preventDefault(); - }; - - const randomPassword = () => { - setShareOption({ ...shareOption, password: true }); - setValues({ - ...values, - password: Math.random().toString(36).substr(2).slice(2, 8), - showPassword: true, - }); - }; - - const handleExpand = (panel) => (event, isExpanded) => { - setExpanded(isExpanded ? panel : false); - }; - - const handleCheck = (prop) => () => { - if (!shareOption[prop]) { - handleExpand(prop)(null, true); - } - if (prop === "password" && shareOption[prop]) { - setValues({ - ...values, - password: "", - }); - } - setShareOption({ ...shareOption, [prop]: !shareOption[prop] }); - }; - - const onClose = () => { - props.onClose(); - setTimeout(() => { - setShareURL(""); - }, 500); - }; - - const senLink = () => { - if (navigator.share) { - let text = t("modals.shareLinkShareContent", { - name: props.selected[0].name, - link: shareURL, - }); - if (lastSubmit.current && lastSubmit.current.password) { - text += t("modals.shareLinkPasswordInfo", { - password: lastSubmit.current.password, - }); - } - navigator.share({ text }); - } else if (navigator.clipboard) { - navigator.clipboard.writeText(shareURL); - ToggleSnackbar("top", "right", t("modals.linkCopied"), "info"); - } - }; - - const submitShare = (e) => { - e.preventDefault(); - props.setModalsLoading(true); - const submitFormBody = { - id: props.selected[0].id, - is_dir: props.selected[0].type === "dir", - password: values.password, - downloads: shareOption.expire - ? values.downloads === -1 - ? parseInt(customDownloads) - : values.downloads - : -1, - expire: - values.expires === -1 - ? parseInt(customExpires) - : values.expires, - preview: shareOption.preview, - }; - lastSubmit.current = submitFormBody; - - API.post("/share", submitFormBody) - .then((response) => { - setShareURL(response.data); - setValues({ - password: "", - downloads: 1, - expires: 24 * 3600, - showPassword: false, - }); - setShareOption({ - password: false, - expire: false, - }); - props.setModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - props.setModalsLoading(false); - }); - }; - - const handleFocus = (event) => event.target.select(); - - return ( - - - {t("modals.createShareLink")} - - - {shareURL === "" && ( - <> - - - - - - - - - - - - - - - - - - {t("modals.sharePassword")} - - - - - - - - - } - offIcon={ - - } - /> - - - } - labelWidth={70} - /> - - - - - - - - - - - - - - - - - - {values.downloads >= 0 && ( - - )} - {values.downloads === -1 && ( - - setCustomDownloads( - e.target.value - ) - } - endAdornment={ - - {t("modals.downloads")} - - } - /> - )} - - - {t("modals.or")} - - - {values.expires >= 0 && ( - - )} - {values.expires === -1 && ( - - setCustomExpires(e.target.value) - } - endAdornment={ - - {t("modals.seconds")} - - } - /> - )} - - - {t("modals.downloadSuffix")} - - - - - - - - - - - - - - - - - - {t("modals.allowPreviewDescription")} - - - - - - - )} - {shareURL !== "" && ( - - - - )} - - - {shareURL !== "" && ( -
- -
- )} - - - {shareURL === "" && ( -
- -
- )} -
-
- ); -} diff --git a/src/component/Modals/CreateWebDAVAccount.js b/src/component/Modals/CreateWebDAVAccount.js deleted file mode 100644 index a82d136..0000000 --- a/src/component/Modals/CreateWebDAVAccount.js +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useState } from "react"; -import { Dialog, makeStyles } from "@material-ui/core"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import DialogActions from "@material-ui/core/DialogActions"; -import Button from "@material-ui/core/Button"; -import TextField from "@material-ui/core/TextField"; -import { FolderOpenOutlined, LabelOutlined } from "@material-ui/icons"; -import PathSelector from "../FileManager/PathSelector"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - formGroup: { - display: "flex", - marginTop: theme.spacing(1), - }, - formIcon: { - marginTop: 21, - marginRight: 19, - color: theme.palette.text.secondary, - }, - input: { - width: 250, - }, - dialogContent: { - paddingTop: 24, - paddingRight: 24, - paddingBottom: 8, - paddingLeft: 24, - }, - button: { - marginTop: 8, - }, -})); - -export default function CreateWebDAVAccount(props) { - const { t } = useTranslation(); - const [value, setValue] = useState({ - name: "", - path: "/", - }); - const [pathSelectDialog, setPathSelectDialog] = React.useState(false); - const [selectedPath, setSelectedPath] = useState(""); - // eslint-disable-next-line - const [selectedPathName, setSelectedPathName] = useState(""); - const classes = useStyles(); - - const setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const handleInputChange = (name) => (e) => { - setValue({ - ...value, - [name]: e.target.value, - }); - }; - - const selectPath = () => { - setValue({ - ...value, - path: selectedPath === "//" ? "/" : selectedPath, - }); - setPathSelectDialog(false); - }; - - return ( - - setPathSelectDialog(false)} - aria-labelledby="form-dialog-title" - > - - {t("navbar.addTagDialog.selectFolder")} - - - - - - - - -
-
-
-
- -
- - -
-
-
- -
-
- -
- -
-
-
-
- - - - -
- ); -} diff --git a/src/component/Modals/Decompress.js b/src/component/Modals/Decompress.js deleted file mode 100644 index 44ee371..0000000 --- a/src/component/Modals/Decompress.js +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - makeStyles, -} from "@material-ui/core"; -import PathSelector from "../FileManager/PathSelector"; -import { useDispatch } from "react-redux"; -import { setModalsLoading, toggleSnackbar } from "../../redux/explorer"; -import { submitDecompressTask } from "../../redux/explorer/action"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, -})); - -export default function DecompressDialog(props) { - const { t } = useTranslation(); - const [selectedPath, setSelectedPath] = useState(""); - const [selectedPathName, setSelectedPathName] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const SetModalsLoading = useCallback( - (status) => { - dispatch(setModalsLoading(status)); - }, - [dispatch] - ); - const SubmitDecompressTask = useCallback( - (path) => dispatch(submitDecompressTask(path)), - [dispatch] - ); - - const setMoveTarget = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const submitMove = (e) => { - if (e != null) { - e.preventDefault(); - } - SetModalsLoading(true); - SubmitDecompressTask(selectedPath) - .then(() => { - props.onClose(); - ToggleSnackbar( - "top", - "right", - t("modals.taskCreated"), - "success" - ); - SetModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - SetModalsLoading(false); - }); - }; - - const classes = useStyles(); - - return ( - - - {t("modals.decompressTo")} - - - - {selectedPath !== "" && ( - - - ]} - /> - - - )} - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/Delete.js b/src/component/Modals/Delete.js deleted file mode 100644 index d9f6750..0000000 --- a/src/component/Modals/Delete.js +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - FormControl, - makeStyles, - Tooltip, -} from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; -import { useTheme } from "@material-ui/core/styles"; -import Auth from "../../middleware/Auth"; -import API from "../../middleware/Api"; -import FormLabel from "@material-ui/core/FormLabel"; -import FormGroup from "@material-ui/core/FormGroup"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Checkbox from "@material-ui/core/Checkbox"; - -const useStyles = makeStyles((theme) => ({ - form: { - marginTop: theme.spacing(2), - }, -})); - -export default function Delete(props) { - const { t } = useTranslation(); - const theme = useTheme(); - const user = Auth.GetUser(); - const [force, setForce] = useState(false); - const [unlink, setUnlink] = useState(false); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const submitRemove = (e) => { - e.preventDefault(); - props.setModalsLoading(true); - const dirs = [], - items = []; - // eslint-disable-next-line - props.selected.map((value) => { - if (value.type === "dir") { - dirs.push(value.id); - } else { - items.push(value.id); - } - }); - API.delete("/object", { - data: { - items: items, - dirs: dirs, - force, - unlink, - }, - }) - .then((response) => { - if (response.rawData.code === 0) { - props.onClose(); - setTimeout(props.refreshFileList, 500); - } else { - ToggleSnackbar( - "top", - "right", - response.rawData.msg, - "warning" - ); - } - props.setModalsLoading(false); - props.refreshStorage(); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - props.setModalsLoading(false); - }); - }; - - const classes = useStyles(); - - return ( - - - {t("modals.deleteTitle")} - - - - - {props.selected.length === 1 && ( - ]} - /> - )} - {props.selected.length > 1 && - t("modals.deleteMultipleDescription", { - num: props.selected.length, - })} - - {user.group.advanceDelete && ( - - - {t("modals.advanceOptions")} - - - - - setForce(e.target.checked) - } - /> - } - label={t("modals.forceDelete")} - /> - - - - setUnlink(e.target.checked) - } - /> - } - label={t("modals.unlinkOnly")} - /> - - - - )} - - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/DirectoryDownload.js b/src/component/Modals/DirectoryDownload.js deleted file mode 100644 index 1740990..0000000 --- a/src/component/Modals/DirectoryDownload.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - makeStyles, - FormControlLabel, - Checkbox, -} from "@material-ui/core"; -import TextField from "@material-ui/core/TextField"; -import { useTranslation } from "react-i18next"; -import { useInterval, usePrevious, useGetState } from "ahooks"; -import { cancelDirectoryDownload } from "../../redux/explorer/action"; -import Auth from "../../middleware/Auth"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - backgroundColor: theme.palette.background.default, - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, -})); - -export default function DirectoryDownloadDialog(props) { - const { t } = useTranslation(); - - const classes = useStyles(); - - const logRef = useRef(); - const [autoScroll, setAutoScroll] = useState( - Auth.GetPreferenceWithDefault("autoScroll", true) - ); - const previousLog = usePrevious(props.log, (prev, next) => true); - const [timer, setTimer] = useState(-1); - - useInterval(() => { - if (autoScroll && logRef.current && previousLog !== props.log) { - logRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); - } - }, timer); - - useEffect(() => { - if (props.done) { - setTimer(-1); - } else if (props.open) { - setTimer(1000); - } - }, [props.done, props.open]); - - return ( - - - {t("modals.directoryDownloadTitle")} - - - - - - - } - checked={autoScroll} - onChange={() => - setAutoScroll((previous) => { - Auth.SetPreference("autoScroll", !previous); - return !previous; - }) - } - label={t("modals.directoryDownloadAutoscroll")} - /> - -
- -
-
-
- ); -} diff --git a/src/component/Modals/Loading.js b/src/component/Modals/Loading.js deleted file mode 100644 index 042a898..0000000 --- a/src/component/Modals/Loading.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import DialogContent from "@material-ui/core/DialogContent"; -import Dialog from "@material-ui/core/Dialog"; -import DialogContentText from "@material-ui/core/DialogContentText"; -import { blue } from "@material-ui/core/colors"; -import { useSelector } from "react-redux"; - -const useStyles = makeStyles({ - avatar: { - backgroundColor: blue[100], - color: blue[600], - }, - loadingContainer: { - display: "flex", - }, - loading: { - marginTop: 10, - marginLeft: 20, - }, -}); - -export default function LoadingDialog() { - const classes = useStyles(); - const open = useSelector((state) => state.viewUpdate.modals.loading); - const text = useSelector((state) => state.viewUpdate.modals.loadingText); - - return ( - - - - -
{text}
-
-
-
- ); -} diff --git a/src/component/Modals/OptionSelector.js b/src/component/Modals/OptionSelector.js deleted file mode 100644 index 00fc7aa..0000000 --- a/src/component/Modals/OptionSelector.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from "react"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, - ListItem, - ListItemText, - makeStyles, -} from "@material-ui/core"; -import { useSelector } from "react-redux"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - content: { - minWidth: 250, - }, -})); - -export default function OptionSelector() { - const { t } = useTranslation("common"); - const classes = useStyles(); - const option = useSelector((state) => state.viewUpdate.modals.option); - - return ( - - - {option && option.title} - - - - {option && - option.options.map((o) => ( - option && option.callback(o)} - button - > - - - ))} - - - - - - - ); -} diff --git a/src/component/Modals/RemoteDownload.js b/src/component/Modals/RemoteDownload.js deleted file mode 100644 index bec3ff5..0000000 --- a/src/component/Modals/RemoteDownload.js +++ /dev/null @@ -1,312 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - makeStyles, TextField -} from "@material-ui/core"; -import PathSelector from "../FileManager/PathSelector"; -import { useDispatch } from "react-redux"; -import API, { AppError } from "../../middleware/Api"; -import { - refreshFileList, - setModalsLoading, - toggleSnackbar, -} from "../../redux/explorer"; -import { Trans, useTranslation } from "react-i18next"; -import { FolderOpenOutlined } from "@material-ui/icons"; -import { pathBack } from "../../utils"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import { AccountCircle } from "mdi-material-ui"; -import { useTheme } from "@material-ui/core/styles"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import LinkIcon from "@material-ui/icons/Link"; -import Chip from "@material-ui/core/Chip"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, - formGroup: { - display: "flex", - marginBottom: theme.spacing(3), - }, - forumInput: { - flexGrow: 1, - } -})); - -export default function RemoteDownload(props) { - const { t } = useTranslation(); - const [selectPathOpen,setSelectPathOpen] = useState(false); - const [selectedPath, setSelectedPath] = useState(""); - const [selectedPathName, setSelectedPathName] = useState(""); - const [downloadTo, setDownloadTo] = useState(""); - const [url, setUrl] = useState(""); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - - useEffect(()=>{ - if (props.open){ - setDownloadTo(props.presentPath) - } - },[props.open]) - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const SetModalsLoading = useCallback( - (status) => { - dispatch(setModalsLoading(status)); - }, - [dispatch] - ); - - const setDownloadToPath = (folder) => { - const path = - folder.path === "/" - ? folder.path + folder.name - : folder.path + "/" + folder.name; - setSelectedPath(path); - setSelectedPathName(folder.name); - }; - - const selectPath = () => { - setDownloadTo(selectedPath === "//" ? "/" : selectedPath); - setSelectPathOpen(false); - }; - - const submitTorrentDownload = (e) => { - e.preventDefault(); - props.setModalsLoading(true); - API.post("/aria2/torrent/" + props.torrent.id, { - dst: - downloadTo === "//" - ? "/" - : downloadTo, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - t("modals.taskCreated"), - "success" - ); - props.onClose(); - props.setModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar( - "top", - "right", - error.message, - "error" - ); - props.setModalsLoading(false); - }); - }; - - const submitDownload = (e) => { - e.preventDefault(); - props.setModalsLoading(true); - API.post("/aria2/url", { - url: url.split("\n"), - dst: - downloadTo === "//" - ? "/" - : downloadTo, - }) - .then((response) => { - const failed = response.data - .filter((r) => r.code !== 0) - .map((r) => new AppError(r.msg, r.code, r.error).message); - if (failed.length > 0) { - ToggleSnackbar( - "top", - "right", - t("modals.taskCreateFailed", { - failed: failed.length, - details: failed.join(","), - }), - "warning" - ); - } else { - ToggleSnackbar( - "top", - "right", - t("modals.taskCreated"), - "success" - ); - } - - props.onClose(); - props.setModalsLoading(false); - }) - .catch((error) => { - ToggleSnackbar( - "top", - "right", - error.message, - "error" - ); - props.setModalsLoading(false); - }); - }; - - const classes = useStyles(); - - return ( - <> - - - {t("modals.newRemoteDownloadTitle")} - - - -
-
- setUrl(e.target.value)} - placeholder={t( - "modals.remoteDownloadURLDescription" - )} - InputProps={{ - startAdornment: !isMobile&&( - - - - ), - - }} - /> -
-
-
-
- setDownloadTo(e.target.value)} - className={classes.input} - label={t("modals.remoteDownloadDst")} - InputProps={{ - startAdornment: !isMobile&&( - - - - ), - endAdornment:( - - - - ) - }} - /> -
-
-
-
-
- - -
- -
-
-
- - setSelectPathOpen(false)} - aria-labelledby="form-dialog-title" - > - - {t("modals.remoteDownloadDst")} - - - - {selectedPathName !== "" && ( - - - ]} - /> - - - )} - - - - - - - ); -} diff --git a/src/component/Modals/SelectFile.js b/src/component/Modals/SelectFile.js deleted file mode 100644 index e054f0a..0000000 --- a/src/component/Modals/SelectFile.js +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - makeStyles, -} from "@material-ui/core"; -import FormGroup from "@material-ui/core/FormGroup"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Checkbox from "@material-ui/core/Checkbox"; -import MenuItem from "@material-ui/core/MenuItem"; -import { Virtuoso } from "react-virtuoso"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - contentFix: { - padding: "10px 24px 0px 24px", - }, - wrapper: { - margin: theme.spacing(1), - position: "relative", - }, - buttonProgress: { - color: theme.palette.secondary.light, - position: "absolute", - top: "50%", - left: "50%", - marginTop: -12, - marginLeft: -12, - }, - content: { - padding: 0, - }, - scroll: { - maxHeight: "calc(100vh - 200px)", - }, -})); - -export default function SelectFileDialog(props) { - const { t } = useTranslation(); - const [files, setFiles] = useState(props.files); - - useEffect(() => { - setFiles(props.files); - }, [props.files]); - - const handleChange = (index) => (event) => { - const filesCopy = [...files]; - // eslint-disable-next-line - filesCopy.map((v, k) => { - if (v.index === index) { - filesCopy[k] = { - ...filesCopy[k], - selected: event.target.checked ? "true" : "false", - }; - } - }); - setFiles(filesCopy); - }; - - const submit = () => { - const index = []; - // eslint-disable-next-line - files.map((v) => { - if (v.selected === "true") { - index.push(parseInt(v.index)); - } - }); - props.onSubmit(index); - }; - - const classes = useStyles(); - - return ( - - - {t("download.selectDownloadingFile")} - - - ( - - - - } - label={v.path} - /> - - - )} - /> - - - -
- -
-
-
- ); -} diff --git a/src/component/Modals/TimeZone.js b/src/component/Modals/TimeZone.js deleted file mode 100644 index 2459f85..0000000 --- a/src/component/Modals/TimeZone.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - makeStyles, -} from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import TextField from "@material-ui/core/TextField"; -import { - refreshTimeZone, - timeZone, - validateTimeZone, -} from "../../utils/datetime"; -import FormControl from "@material-ui/core/FormControl"; -import Auth from "../../middleware/Auth"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({})); - -export default function TimeZoneDialog(props) { - const { t } = useTranslation(); - const [timeZoneValue, setTimeZoneValue] = useState(timeZone); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const saveZoneInfo = () => { - if (!validateTimeZone(timeZoneValue)) { - ToggleSnackbar("top", "right", "无效的时区名称", "warning"); - return; - } - Auth.SetPreference("timeZone", timeZoneValue); - refreshTimeZone(); - props.onClose(); - }; - - const classes = useStyles(); - - return ( - - - {t("setting.timeZone")} - - - - - setTimeZoneValue(e.target.value)} - /> - - - - - -
- -
-
-
- ); -} diff --git a/src/component/Navbar/DarkModeSwitcher.js b/src/component/Navbar/DarkModeSwitcher.js deleted file mode 100644 index 0757a94..0000000 --- a/src/component/Navbar/DarkModeSwitcher.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useCallback } from "react"; -import { IconButton, makeStyles } from "@material-ui/core"; -import DayIcon from "@material-ui/icons/Brightness7"; -import NightIcon from "@material-ui/icons/Brightness4"; -import { useDispatch, useSelector } from "react-redux"; -import Tooltip from "@material-ui/core/Tooltip"; -import Auth from "../../middleware/Auth"; -import classNames from "classnames"; -import { toggleDaylightMode } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles(() => ({ - icon: { - color: "rgb(255, 255, 255)", - opacity: "0.54", - }, -})); - -const DarkModeSwitcher = ({ position }) => { - const { t } = useTranslation(); - const ThemeType = useSelector( - (state) => state.siteConfig.theme.palette.type - ); - const dispatch = useDispatch(); - const ToggleThemeMode = useCallback(() => dispatch(toggleDaylightMode()), [ - dispatch, - ]); - const isDayLight = (ThemeType && ThemeType === "light") || !ThemeType; - const isDark = ThemeType && ThemeType === "dark"; - const toggleMode = () => { - Auth.SetPreference("theme_mode", isDayLight ? "dark" : "light"); - ToggleThemeMode(); - }; - const classes = useStyles(); - return ( - - - {isDayLight && } - {isDark && } - - - ); -}; - -export default DarkModeSwitcher; diff --git a/src/component/Navbar/FileTags.js b/src/component/Navbar/FileTags.js deleted file mode 100644 index 8bb014c..0000000 --- a/src/component/Navbar/FileTags.js +++ /dev/null @@ -1,407 +0,0 @@ -import React, { Suspense, useCallback, useState } from "react"; -import { - Divider, - List, - ListItemIcon, - ListItemText, - makeStyles, - withStyles, -} from "@material-ui/core"; -import { Clear, KeyboardArrowRight } from "@material-ui/icons"; -import classNames from "classnames"; -import FolderShared from "@material-ui/icons/FolderShared"; -import UploadIcon from "@material-ui/icons/CloudUpload"; -import VideoIcon from "@material-ui/icons/VideoLibraryOutlined"; -import ImageIcon from "@material-ui/icons/CollectionsOutlined"; -import MusicIcon from "@material-ui/icons/LibraryMusicOutlined"; -import DocIcon from "@material-ui/icons/FileCopyOutlined"; -import { useHistory, useLocation } from "react-router"; -import pathHelper from "../../utils/page"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import MuiListItem from "@material-ui/core/ListItem"; -import { useDispatch } from "react-redux"; -import Auth from "../../middleware/Auth"; -import { - Circle, - CircleOutline, - FolderHeartOutline, - Heart, - HeartOutline, - Hexagon, - HexagonOutline, - Hexagram, - HexagramOutline, - Rhombus, - RhombusOutline, - Square, - SquareOutline, - TagPlus, - Triangle, - TriangleOutline, -} from "mdi-material-ui"; -import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; -import IconButton from "@material-ui/core/IconButton"; -import API from "../../middleware/Api"; -import { navigateTo, searchMyFile, toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const ListItem = withStyles((theme) => ({ - root: { - borderRadius:theme.shape.borderRadius, - }, -}))(MuiListItem); - -const ExpansionPanel = withStyles({ - root: { - maxWidth: "100%", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": { margin: 0 }, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles((theme) =>({ - root: { - minHeight: 0, - padding: 0, - "&$expanded": { - minHeight: 0, - }, - }, - content: { - maxWidth: "100%", - margin: 0, - display: "block", - "&$expanded": { - margin: "0", - }, - }, - expanded: {}, -}))(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - display: "block", - padding: theme.spacing(0), - }, -}))(MuiExpansionPanelDetails); - -const useStyles = makeStyles((theme) => ({ - expand: { - display: "none", - transition: ".15s all ease-in-out", - }, - expanded: { - display: "block", - transform: "rotate(90deg)", - }, - iconFix: { - marginLeft: "16px", - }, - hiddenButton: { - display: "none", - }, - subMenu: { - marginLeft: theme.spacing(2), - }, - overFlow: { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }, - paddingList:{ - padding:theme.spacing(1), - }, - paddingSummary:{ - paddingLeft:theme.spacing(1), - paddingRight:theme.spacing(1), - } -})); - -const icons = { - Circle: Circle, - CircleOutline: CircleOutline, - Heart: Heart, - HeartOutline: HeartOutline, - Hexagon: Hexagon, - HexagonOutline: HexagonOutline, - Hexagram: Hexagram, - HexagramOutline: HexagramOutline, - Rhombus: Rhombus, - RhombusOutline: RhombusOutline, - Square: Square, - SquareOutline: SquareOutline, - Triangle: Triangle, - TriangleOutline: TriangleOutline, - FolderHeartOutline: FolderHeartOutline, -}; - -const AddTag = React.lazy(() => import("../Modals/AddTag")); - -export default function FileTag() { - const classes = useStyles(); - const { t } = useTranslation(); - - const location = useLocation(); - const history = useHistory(); - - const isHomePage = pathHelper.isHomePage(location.pathname); - - const [tagOpen, setTagOpen] = useState(true); - const [addTagModal, setAddTagModal] = useState(false); - const [tagHover, setTagHover] = useState(null); - const [tags, setTags] = useState( - Auth.GetUser().tags ? Auth.GetUser().tags : [] - ); - - const dispatch = useDispatch(); - const SearchMyFile = useCallback((k, p) => dispatch(searchMyFile(k, p)), [ - dispatch, - ]); - const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const getIcon = (icon, color) => { - if (icons[icon]) { - const IconComponent = icons[icon]; - return ( - - ); - } - return ; - }; - - const submitSuccess = (tag) => { - const newTags = [...tags, tag]; - setTags(newTags); - const user = Auth.GetUser(); - user.tags = newTags; - Auth.SetUser(user); - }; - - const submitDelete = (id) => { - API.delete("/tag/" + id) - .then(() => { - const newTags = tags.filter((v) => { - return v.id !== id; - }); - setTags(newTags); - const user = Auth.GetUser(); - user.tags = newTags; - Auth.SetUser(user); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - return ( - <> - - setAddTagModal(false)} - /> - - isHomePage && setTagOpen(!tagOpen)} - > - -
- - !isHomePage && history.push("/home?path=%2F") - } - > - - - {!(tagOpen && isHomePage) && ( - - )} - - - -
- - -
- - - setTagHover(null)}> - - - - - - - - - - - - - {[ - { - key: t("navbar.videos"), - id: "video", - icon: ( - - ), - }, - { - key: t("navbar.photos"), - id: "image", - icon: ( - - ), - }, - { - key: t("navbar.music"), - id: "audio", - icon: ( - - ), - }, - { - key: t("navbar.documents"), - id: "doc", - icon: ( - - ), - }, - ].map((v) => ( - - SearchMyFile(v.id + "/internal", "") - } - > - - {v.icon} - - - - ))} - {tags.map((v) => ( - setTagHover(v.id)} - onClick={() => { - if (v.type === 0) { - SearchMyFile("tag/" + v.id, ""); - } else { - NavigateTo(v.expression); - } - }} - > - - {getIcon( - v.type === 0 - ? v.icon - : "FolderHeartOutline", - v.type === 0 ? v.color : null - )} - - - - {tagHover === v.id && ( - submitDelete(v.id)} - > - - - - - )} - - ))} - - setAddTagModal(true)}> - - - - - - {" "} - - -
- - ); -} diff --git a/src/component/Navbar/Navbar.js b/src/component/Navbar/Navbar.js deleted file mode 100644 index b82f5c4..0000000 --- a/src/component/Navbar/Navbar.js +++ /dev/null @@ -1,968 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classNames from "classnames"; -import { connect } from "react-redux"; -import ShareIcon from "@material-ui/icons/Share"; -import MusicNote from "@material-ui/icons/MusicNote"; -import BackIcon from "@material-ui/icons/ArrowBack"; -import OpenIcon from "@material-ui/icons/OpenInNew"; -import DownloadIcon from "@material-ui/icons/CloudDownload"; -import RenameIcon from "@material-ui/icons/BorderColor"; -import MoveIcon from "@material-ui/icons/Input"; -import DeleteIcon from "@material-ui/icons/Delete"; -import MenuIcon from "@material-ui/icons/Menu"; -import { isPreviewable } from "../../config"; -import { changeThemeColor, sizeToString, vhCheck } from "../../utils"; -import Uploader from "../Uploader/Uploader.js"; -import pathHelper from "../../utils/page"; -import SezrchBar from "./SearchBar"; -import StorageBar from "./StorageBar"; -import UserAvatar from "./UserAvatar"; -import UserInfo from "./UserInfo"; -import { - FolderDownload, - AccountArrowRight, - AccountPlus, - LogoutVariant, -} from "mdi-material-ui"; -import { withRouter } from "react-router-dom"; -import { - AppBar, - Drawer, - Grow, - Hidden, - IconButton, - List, - ListItemIcon, - ListItemText, - SwipeableDrawer, - Toolbar, - Tooltip, - Typography, - withStyles, - withTheme -} from "@material-ui/core"; -import Auth from "../../middleware/Auth"; -import API from "../../middleware/Api"; -import FileTag from "./FileTags"; -import { Assignment, Devices, MoreHoriz, Settings } from "@material-ui/icons"; -import Divider from "@material-ui/core/Divider"; -import SubActions from "../FileManager/Navigator/SubActions"; -import { - audioPreviewSetIsOpen, - changeContextMenu, - drawerToggleAction, - navigateTo, - openCreateFolderDialog, - openLoadingDialog, - openMoveDialog, - openMusicDialog, - openPreview, - openRemoveDialog, - openRenameDialog, - openShareDialog, - saveFile, - setSelectedTarget, - setSessionStatus, - showImgPreivew, - toggleSnackbar, -} from "../../redux/explorer"; -import { - startBatchDownload, - startDirectoryDownload, - startDownload, -} from "../../redux/explorer/action"; -import { withTranslation } from "react-i18next"; -import MuiListItem from "@material-ui/core/ListItem"; - -vhCheck(); -const drawerWidth = 240; -const drawerWidthMobile = 270; - -const ListItem = withStyles((theme) => ({ - root: { - borderRadius:theme.shape.borderRadius, - }, -}))(MuiListItem); - -const mapStateToProps = (state) => { - return { - desktopOpen: state.viewUpdate.open, - selected: state.explorer.selected, - isMultiple: state.explorer.selectProps.isMultiple, - withFolder: state.explorer.selectProps.withFolder, - withFile: state.explorer.selectProps.withFile, - path: state.navigator.path, - title: state.siteConfig.title, - subTitle: state.viewUpdate.subTitle, - loadUploader: state.viewUpdate.loadUploader, - isLogin: state.viewUpdate.isLogin, - shareInfo: state.viewUpdate.shareInfo, - registerEnabled: state.siteConfig.registerEnabled, - audioPreviewPlayingName: state.explorer.audioPreview.playingName, - audioPreviewIsOpen: state.explorer.audioPreview.isOpen, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - handleDesktopToggle: (open) => { - dispatch(drawerToggleAction(open)); - }, - setSelectedTarget: (targets) => { - dispatch(setSelectedTarget(targets)); - }, - navigateTo: (path) => { - dispatch(navigateTo(path)); - }, - openCreateFolderDialog: () => { - dispatch(openCreateFolderDialog()); - }, - changeContextMenu: (type, open) => { - dispatch(changeContextMenu(type, open)); - }, - saveFile: () => { - dispatch(saveFile()); - }, - openMusicDialog: () => { - dispatch(openMusicDialog()); - }, - showImgPreivew: (first) => { - dispatch(showImgPreivew(first)); - }, - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - openRenameDialog: () => { - dispatch(openRenameDialog()); - }, - openMoveDialog: () => { - dispatch(openMoveDialog()); - }, - openRemoveDialog: () => { - dispatch(openRemoveDialog()); - }, - openShareDialog: () => { - dispatch(openShareDialog()); - }, - openLoadingDialog: (text) => { - dispatch(openLoadingDialog(text)); - }, - setSessionStatus: () => { - dispatch(setSessionStatus()); - }, - openPreview: (share) => { - dispatch(openPreview(share)); - }, - audioPreviewOpen: () => { - dispatch(audioPreviewSetIsOpen(true)); - }, - startBatchDownload: (share) => { - dispatch(startBatchDownload(share)); - }, - startDirectoryDownload: (share) => { - dispatch(startDirectoryDownload(share)); - }, - startDownload: (share, file) => { - dispatch(startDownload(share, file)); - }, - }; -}; - -const styles = (theme) => ({ - appBar: { - marginLeft: drawerWidth, - [theme.breakpoints.down("xs")]: { - marginLeft: drawerWidthMobile, - }, - zIndex: theme.zIndex.drawer + 1, - transition: " background-color 250ms", - }, - - drawer: { - width: 0, - flexShrink: 0, - }, - drawerDesktop: { - width: drawerWidth, - flexShrink: 0, - }, - icon: { - marginRight: theme.spacing(2), - }, - menuButton: { - marginRight: 20, - [theme.breakpoints.up("sm")]: { - display: "none", - }, - }, - menuButtonDesktop: { - marginRight: 20, - [theme.breakpoints.down("xs")]: { - display: "none", - }, - }, - menuIcon: { - marginRight: 20, - }, - toolbar: theme.mixins.toolbar, - drawerPaper: { - width: drawerWidthMobile, - }, - drawerPaperDesktop: { - width: drawerWidth, - }, - upDrawer: { - overflowX: "hidden", - [theme.breakpoints.up("sm")]: { - display: "flex", - flexDirection: "column", - height: "100%", - justifyContent: "space-between", - }, - }, - drawerOpen: { - width: drawerWidth, - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - }, - drawerClose: { - transition: theme.transitions.create("width", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: "hidden", - width: 0, - }, - content: { - flexGrow: 1, - padding: theme.spacing(3), - }, - grow: { - flexGrow: 1, - }, - badge: { - top: 1, - right: -15, - }, - nested: { - paddingLeft: theme.spacing(4), - }, - sectionForFile: { - display: "flex", - }, - extendedIcon: { - marginRight: theme.spacing(1), - }, - addButton: { - marginLeft: "40px", - marginTop: "25px", - marginBottom: "15px", - }, - fabButton: { - borderRadius: "100px", - }, - badgeFix: { - right: "10px", - }, - iconFix: { - marginLeft: "16px", - }, - dividerFix: { - marginTop: "8px", - }, - folderShareIcon: { - verticalAlign: "sub", - marginRight: "5px", - }, - shareInfoContainer: { - display: "flex", - marginTop: "15px", - marginBottom: "20px", - marginLeft: "28px", - textDecoration: "none", - }, - shareAvatar: { - width: "40px", - height: "40px", - }, - stickFooter: { - bottom: "0px", - position: "absolute", - backgroundColor: theme.palette.background.paper, - width: "100%", - }, - ownerInfo: { - marginLeft: "10px", - width: "150px", - }, - minStickDrawer: { - overflowY: "auto", - }, - paddingList:{ - padding:theme.spacing(1), - } -}); -class NavbarCompoment extends Component { - constructor(props) { - super(props); - this.state = { - mobileOpen: false, - }; - this.UploaderRef = React.createRef(); - } - - UNSAFE_componentWillMount() { - this.unlisten = this.props.history.listen(() => { - this.setState(() => ({ mobileOpen: false })); - }); - } - componentWillUnmount() { - this.unlisten(); - } - - componentDidMount() { - changeThemeColor( - this.props.selected.length <= 1 && - !(!this.props.isMultiple && this.props.withFile) - ? this.props.theme.palette.primary.main - : this.props.theme.palette.background.default - ); - } - - UNSAFE_componentWillReceiveProps = (nextProps) => { - if ( - (this.props.selected.length === 0) !== - (nextProps.selected.length === 0) - ) { - changeThemeColor( - !(this.props.selected.length === 0) - ? this.props.theme.palette.type === "dark" - ? this.props.theme.palette.background.default - : this.props.theme.palette.primary.main - : this.props.theme.palette.background.default - ); - } - }; - - handleDrawerToggle = () => { - this.setState((state) => ({ mobileOpen: !state.mobileOpen })); - }; - - openDownload = () => { - this.props.startDownload(this.props.shareInfo, this.props.selected[0]); - }; - - openDirectoryDownload = (e) => { - this.props.startDirectoryDownload(this.props.shareInfo); - }; - - archiveDownload = (e) => { - this.props.startBatchDownload(this.props.shareInfo); - }; - - signOut = () => { - API.delete("/user/session/") - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("login.loggedOut"), - "success" - ); - Auth.signout(); - window.location.reload(); - this.props.setSessionStatus(false); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "warning" - ); - }) - .finally(() => { - this.handleClose(); - }); - }; - - render() { - const { classes, t } = this.props; - const user = Auth.GetUser(this.props.isLogin); - const isHomePage = pathHelper.isHomePage(this.props.location.pathname); - const isSharePage = pathHelper.isSharePage( - this.props.location.pathname - ); - - const drawer = ( -
- {pathHelper.isMobile() && } - - {Auth.Check(this.props.isLogin) && ( - <> -
- - - - this.props.history.push("/shares?") - } - > - - - - - - {user.group.allowRemoteDownload && ( - - this.props.history.push("/aria2?") - } - > - - - - - - )} - {user.group.webdav && ( - - this.props.history.push("/webdav?") - } - > - - - - - - )} - - - this.props.history.push("/tasks?") - } - > - - - - - - {pathHelper.isMobile() && ( - <> - - - this.props.history.push( - "/setting?" - ) - } - > - - - - - - - - - - - - - - )} - -
-
- -
- - )} - - {!Auth.Check(this.props.isLogin) && ( -
- this.props.history.push("/login")} - > - - - - - - {this.props.registerEnabled && ( - - this.props.history.push("/signup") - } - > - - - - - - )} -
- )} -
- ); - const iOS = - process.browser && /iPad|iPhone|iPod/.test(navigator.userAgent); - return ( -
- - - {this.props.selected.length === 0 && ( - - - - )} - {this.props.selected.length === 0 && ( - - this.props.handleDesktopToggle( - !this.props.desktopOpen - ) - } - className={classes.menuButtonDesktop} - > - - - )} - {this.props.selected.length > 0 && - (isHomePage || - pathHelper.isSharePage( - this.props.location.pathname - )) && ( - 0}> - - this.props.setSelectedTarget([]) - } - > - - - - )} - {this.props.selected.length === 0 && ( - { - this.props.history.push("/"); - }} - > - {this.props.subTitle - ? this.props.subTitle - : this.props.title} - - )} - - {!this.props.isMultiple && - (this.props.withFile || this.props.withFolder) && - !pathHelper.isMobile() && ( - - {this.props.selected[0].name}{" "} - {this.props.withFile && - (isHomePage || - pathHelper.isSharePage( - this.props.location.pathname - )) && - "(" + - sizeToString( - this.props.selected[0].size - ) + - ")"} - - )} - - {this.props.selected.length > 1 && - !pathHelper.isMobile() && ( - - {t("navbar.objectsSelected", { - num: this.props.selected.length, - })} - - )} - {this.props.selected.length === 0 && } -
- {this.props.selected.length > 0 && - (isHomePage || isSharePage) && ( -
- {!this.props.isMultiple && - this.props.withFile && - isPreviewable( - this.props.selected[0].name - ) && ( - - - - this.props.openPreview( - this.props - .shareInfo - ) - } - > - - - - - )} - {!this.props.isMultiple && - this.props.withFile && ( - - - - this.openDownload() - } - > - - - - - )} - {(this.props.isMultiple || - this.props.withFolder) && - window.showDirectoryPicker && - window.isSecureContext && ( - - - - this.openDirectoryDownload() - } - > - - - - - )} - {(this.props.isMultiple || - this.props.withFolder) && ( - - - - this.archiveDownload() - } - > - - - - - )} - {!this.props.isMultiple && - !pathHelper.isMobile() && - !isSharePage && ( - - - - this.props.openShareDialog() - } - > - - - - - )} - {!this.props.isMultiple && !isSharePage && ( - - - - this.props.openRenameDialog() - } - > - - - - - )} - {!isSharePage && ( -
- {!pathHelper.isMobile() && ( - - - - this.props.openMoveDialog() - } - > - - - - - )} - - - - - this.props.openRemoveDialog() - } - > - - - - - - {pathHelper.isMobile() && ( - - - - this.props.changeContextMenu( - "file", - true - ) - } - > - - - - - )} -
- )} -
- )} - {this.props.selected.length <= 1 && - !(!this.props.isMultiple && this.props.withFile) && - this.props.audioPreviewPlayingName != null && ( - - - - )} - - {this.props.selected.length === 0 && } - {this.props.selected.length === 0 && - pathHelper.isMobile() && - (isHomePage || this.props.shareInfo) && ( - - )} - - - - - - - this.setState(() => ({ mobileOpen: true })) - } - disableDiscovery={iOS} - ModalProps={{ - keepMounted: true, // Better open performance on mobile. - }} - > - {drawer} - - - - -
- {drawer} - - -
- ); - } -} -NavbarCompoment.propTypes = { - classes: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, -}; - -const Navbar = connect( - mapStateToProps, - mapDispatchToProps -)( - withTheme( - withStyles(styles)(withRouter(withTranslation()(NavbarCompoment))) - ) -); - -export default Navbar; diff --git a/src/component/Navbar/SearchBar.js b/src/component/Navbar/SearchBar.js deleted file mode 100644 index 21a5be5..0000000 --- a/src/component/Navbar/SearchBar.js +++ /dev/null @@ -1,283 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import SearchIcon from "@material-ui/icons/Search"; -import { fade } from "@material-ui/core/styles/colorManipulator"; -import FileIcon from "@material-ui/icons/InsertDriveFile"; -import ShareIcon from "@material-ui/icons/Share"; -import { connect } from "react-redux"; - -import { - Fade, - InputBase, - ListItemIcon, - ListItemText, - MenuItem, - Paper, - Popper, - Typography, - withStyles, -} from "@material-ui/core"; -import { withRouter } from "react-router"; -import pathHelper from "../../utils/page"; -import { configure, HotKeys } from "react-hotkeys"; -import { searchMyFile } from "../../redux/explorer"; -import FolderIcon from "@material-ui/icons/Folder"; -import { Trans, withTranslation } from "react-i18next"; - -configure({ - ignoreTags: [], -}); - -const mapStateToProps = (state) => { - return { - path: state.navigator.path, - search: state.explorer.search, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - searchMyFile: (keywords, path) => { - dispatch(searchMyFile(keywords, path)); - }, - }; -}; - -const styles = (theme) => ({ - search: { - [theme.breakpoints.down("sm")]: { - display: "none", - }, - position: "relative", - borderRadius: theme.shape.borderRadius, - backgroundColor: fade(theme.palette.common.white, 0.15), - "&:hover": { - backgroundColor: fade(theme.palette.common.white, 0.25), - }, - marginRight: theme.spacing(2), - marginLeft: 0, - width: "100%", - [theme.breakpoints.up("sm")]: { - marginLeft: theme.spacing(7.2), - width: "auto", - }, - }, - searchIcon: { - width: theme.spacing(9), - height: "100%", - position: "absolute", - pointerEvents: "none", - display: "flex", - alignItems: "center", - justifyContent: "center", - }, - inputRoot: { - color: "inherit", - width: "100%", - }, - inputInput: { - paddingTop: theme.spacing(1), - paddingRight: theme.spacing(1), - paddingBottom: theme.spacing(1), - paddingLeft: theme.spacing(7), - transition: theme.transitions.create("width"), - width: "100%", - [theme.breakpoints.up("md")]: { - width: 200, - "&:focus": { - width: 300, - }, - }, - }, - suggestBox: { - zIndex: "9999", - width: 364, - }, -}); - -const keyMap = { - SEARCH: "enter", -}; - -class SearchBarCompoment extends Component { - constructor(props) { - super(props); - this.state = { - anchorEl: null, - input: "", - }; - } - - handlers = { - SEARCH: (e) => { - if (pathHelper.isHomePage(this.props.location.pathname)) { - this.searchMyFile("")(); - } else { - this.searchShare(); - } - e.target.blur(); - }, - }; - - handleChange = (event) => { - const { currentTarget } = event; - this.input = event.target.value; - this.setState({ - anchorEl: currentTarget, - input: event.target.value, - }); - }; - - cancelSuggest = () => { - this.setState({ - input: "", - }); - }; - - searchMyFile = (path) => () => { - this.props.searchMyFile("keywords/" + this.input, path); - }; - - searchShare = () => { - this.props.history.push( - "/search?keywords=" + encodeURIComponent(this.input) - ); - }; - - render() { - const { classes, t } = this.props; - const { anchorEl } = this.state; - const id = this.state.input !== "" ? "simple-popper" : null; - const isHomePage = pathHelper.isHomePage(this.props.location.pathname); - - return ( -
-
- -
- - - - - {({ TransitionProps }) => ( - - - {isHomePage && ( - - - - - - , - ]} - /> - - } - /> - - )} - - {isHomePage && - this.props.path !== "/" && - !this.props.search && ( - - - - - - , - ]} - /> - - } - /> - - )} - - - - - - - , - ]} - /> - - } - /> - - - - )} - -
- ); - } -} - -SearchBarCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const SearchBar = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(SearchBarCompoment)))); - -export default SearchBar; diff --git a/src/component/Navbar/StorageBar.js b/src/component/Navbar/StorageBar.js deleted file mode 100644 index 92d769a..0000000 --- a/src/component/Navbar/StorageBar.js +++ /dev/null @@ -1,197 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import StorageIcon from "@material-ui/icons/Storage"; -import { connect } from "react-redux"; -import API from "../../middleware/Api"; -import { sizeToString } from "../../utils"; - -import { - Divider, - LinearProgress, - Tooltip, - Typography, - withStyles, -} from "@material-ui/core"; -import ButtonBase from "@material-ui/core/ButtonBase"; -import { withRouter } from "react-router"; -import { toggleSnackbar } from "../../redux/explorer"; -import { Link as RouterLink } from "react-router-dom"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - refresh: state.viewUpdate.storageRefresh, - isLogin: state.viewUpdate.isLogin, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -const styles = (theme) => ({ - iconFix: { - marginLeft: "32px", - marginRight: "17px", - color: theme.palette.text.secondary, - marginTop: "2px", - }, - textFix: { - padding: " 0 0 0 16px", - }, - storageContainer: { - display: "flex", - marginTop: "15px", - textAlign: "left", - marginBottom: "11px", - }, - detail: { - width: "100%", - marginRight: "35px", - }, - info: { - width: "131px", - overflow: "hidden", - textOverflow: "ellipsis", - [theme.breakpoints.down("xs")]: { - width: "162px", - }, - marginTop: "5px", - }, - bar: { - marginTop: "5px", - }, - stickFooter: { - backgroundColor: theme.palette.background.paper, - }, -}); - -// TODO 使用 hooks 重构 -class StorageBarCompoment extends Component { - state = { - percent: 0, - used: null, - total: null, - showExpand: false, - }; - - firstLoad = true; - - componentDidMount = () => { - if (this.firstLoad && this.props.isLogin) { - this.firstLoad = !this.firstLoad; - this.updateStatus(); - } - }; - - componentWillUnmount() { - this.firstLoad = false; - } - - UNSAFE_componentWillReceiveProps = (nextProps) => { - if ( - (this.props.isLogin && this.props.refresh !== nextProps.refresh) || - (this.props.isLogin !== nextProps.isLogin && nextProps.isLogin) - ) { - this.updateStatus(); - } - }; - - updateStatus = () => { - let percent = 0; - API.get("/user/storage") - .then((response) => { - if (response.data.used / response.data.total >= 1) { - percent = 100; - this.props.toggleSnackbar( - "top", - "right", - this.props.t("navbar.exceedQuota"), - "warning" - ); - } else { - percent = (response.data.used / response.data.total) * 100; - } - this.setState({ - percent: percent, - used: sizeToString(response.data.used), - total: sizeToString(response.data.total), - }); - }) - // eslint-disable-next-line @typescript-eslint/no-empty-function - .catch(() => {}); - }; - - render() { - const { classes, t } = this.props; - return ( -
this.setState({ showExpand: true })} - onMouseLeave={() => this.setState({ showExpand: false })} - className={classes.stickFooter} - > - - -
- -
- - {t("navbar.storage")} - - -
- - - {this.state.used === null - ? " -- " - : this.state.used} - {" / "} - {this.state.total === null - ? " -- " - : this.state.total} - - -
-
-
-
-
- ); - } -} - -StorageBarCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const StorageBar = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(StorageBarCompoment)))); - -export default StorageBar; diff --git a/src/component/Navbar/UserAvatar.js b/src/component/Navbar/UserAvatar.js deleted file mode 100644 index 93b3dc1..0000000 --- a/src/component/Navbar/UserAvatar.js +++ /dev/null @@ -1,176 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import SettingIcon from "@material-ui/icons/Settings"; -import UserAvatarPopover from "./UserAvatarPopover"; -import { AccountCircle } from "mdi-material-ui"; -import Auth from "../../middleware/Auth"; -import { - Avatar, - Grow, - IconButton, - Tooltip, - withStyles, -} from "@material-ui/core"; -import { withRouter } from "react-router-dom"; -import pathHelper from "../../utils/page"; -import DarkModeSwitcher from "./DarkModeSwitcher"; -import { Home } from "@material-ui/icons"; -import { setUserPopover } from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - selected: state.explorer.selected, - isMultiple: state.explorer.selectProps.isMultiple, - withFolder: state.explorer.selectProps.withFolder, - withFile: state.explorer.selectProps.withFile, - isLogin: state.viewUpdate.isLogin, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - setUserPopover: (anchor) => { - dispatch(setUserPopover(anchor)); - }, - }; -}; - -const styles = (theme) => ({ - mobileHidden: { - [theme.breakpoints.down("xs")]: { - display: "none", - }, - whiteSpace: "nowrap", - }, - avatar: { - width: "30px", - height: "30px", - }, - header: { - display: "flex", - padding: "20px 20px 20px 20px", - }, - largeAvatar: { - height: "90px", - width: "90px", - }, - info: { - marginLeft: "10px", - width: "139px", - }, - badge: { - marginTop: "10px", - }, - visitorMenu: { - width: 200, - }, -}); - -class UserAvatarCompoment extends Component { - state = { - anchorEl: null, - }; - - showUserInfo = (e) => { - this.props.setUserPopover(e.currentTarget); - }; - - handleClose = () => { - this.setState({ - anchorEl: null, - }); - }; - - openURL = (url) => { - window.location.href = url; - }; - - returnHome = () => { - window.location.href = "/home"; - }; - - render() { - const { classes, t } = this.props; - const loginCheck = Auth.Check(this.props.isLogin); - const user = Auth.GetUser(this.props.isLogin); - const isAdminPage = pathHelper.isAdminPage( - this.props.location.pathname - ); - - return ( -
- -
- {!isAdminPage && ( - <> - - {loginCheck && ( - <> - - - this.props.history.push( - "/setting?" - ) - } - color="inherit" - > - - - - - )} - - )} - {isAdminPage && ( - - - - - - )} - - {!loginCheck && } - {loginCheck && ( - - )} - {" "} -
-
- -
- ); - } -} - -UserAvatarCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const UserAvatar = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(UserAvatarCompoment)))); - -export default UserAvatar; diff --git a/src/component/Navbar/UserAvatarPopover.js b/src/component/Navbar/UserAvatarPopover.js deleted file mode 100644 index a8ca6e4..0000000 --- a/src/component/Navbar/UserAvatarPopover.js +++ /dev/null @@ -1,260 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import { - AccountArrowRight, - AccountPlus, - DesktopMacDashboard, - HomeAccount, - LogoutVariant, -} from "mdi-material-ui"; -import { withRouter } from "react-router-dom"; -import Auth from "../../middleware/Auth"; -import { - Avatar, - Chip, - Divider, - ListItemIcon, - MenuItem, - Popover, - Typography, - withStyles, -} from "@material-ui/core"; -import API from "../../middleware/Api"; -import pathHelper from "../../utils/page"; -import { - setSessionStatus, - setUserPopover, - toggleSnackbar, -} from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - anchorEl: state.viewUpdate.userPopoverAnchorEl, - registerEnabled: state.siteConfig.registerEnabled, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - setUserPopover: (anchor) => { - dispatch(setUserPopover(anchor)); - }, - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - setSessionStatus: (status) => { - dispatch(setSessionStatus(status)); - }, - }; -}; -const styles = () => ({ - avatar: { - width: "30px", - height: "30px", - }, - header: { - display: "flex", - padding: "20px 20px 20px 20px", - }, - largeAvatar: { - height: "90px", - width: "90px", - }, - info: { - marginLeft: "10px", - width: "139px", - }, - badge: { - marginTop: "10px", - }, - visitorMenu: { - width: 200, - }, -}); - -class UserAvatarPopoverCompoment extends Component { - handleClose = () => { - this.props.setUserPopover(null); - }; - - openURL = (url) => { - window.location.href = url; - }; - - sigOut = () => { - API.delete("/user/session/") - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("login.loggedOut"), - "success" - ); - Auth.signout(); - window.location.reload(); - this.props.setSessionStatus(false); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "warning" - ); - }) - .then(() => { - this.handleClose(); - }); - }; - - render() { - const { classes, t } = this.props; - const user = Auth.GetUser(); - const isAdminPage = pathHelper.isAdminPage( - this.props.location.pathname - ); - - return ( - - {!Auth.Check() && ( -
- - this.props.history.push("/login")} - > - - - - {t("login.signIn")} - - {this.props.registerEnabled && ( - - this.props.history.push("/signup") - } - > - - - - {t("login.signUp")} - - )} -
- )} - {Auth.Check() && ( -
-
-
- -
-
- {user.nickname} - - {user.user_name} - - -
-
-
- - {!isAdminPage && ( - { - this.handleClose(); - this.props.history.push( - "/profile/" + user.id - ); - }} - > - - - - {t("navbar.myProfile")} - - )} - {user.group.id === 1 && ( - { - this.handleClose(); - this.props.history.push("/admin/home"); - }} - > - - - - {t("navbar.dashboard")} - - )} - - - - - - {t("login.logout")} - -
-
- )} -
- ); - } -} - -UserAvatarPopoverCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const UserAvatarPopover = connect( - mapStateToProps, - mapDispatchToProps -)( - withStyles(styles)( - withRouter(withTranslation()(UserAvatarPopoverCompoment)) - ) -); - -export default UserAvatarPopover; diff --git a/src/component/Navbar/UserInfo.js b/src/component/Navbar/UserInfo.js deleted file mode 100644 index 2daeffb..0000000 --- a/src/component/Navbar/UserInfo.js +++ /dev/null @@ -1,152 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { connect } from "react-redux"; -import { Typography, withStyles } from "@material-ui/core"; -import Auth from "../../middleware/Auth"; -import DarkModeSwitcher from "./DarkModeSwitcher"; -import Avatar from "@material-ui/core/Avatar"; -import { setUserPopover } from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const mapStateToProps = (state) => { - return { - isLogin: state.viewUpdate.isLogin, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - setUserPopover: (anchor) => { - dispatch(setUserPopover(anchor)); - }, - }; -}; - -const styles = (theme) => ({ - userNav: { - height: "170px", - backgroundColor: theme.palette.primary.main, - padding: "20px 20px 2em", - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='" + - theme.palette.primary.light.replace("#", "%23") + - "' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.dark.replace("#", "%23") + - "' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.light.replace("#", "%23") + - "' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.dark.replace("#", "%23") + - "' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.main.replace("#", "%23") + - "' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.light.replace("#", "%23") + - "' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='943 900 1210 900 971 687'/%3E%3C/svg%3E\")", - backgroundSize: "cover", - }, - avatar: { - display: "block", - width: "70px", - height: "70px", - border: " 2px solid #fff", - borderRadius: "50%", - overflow: "hidden", - boxShadow: - "0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12)", - }, - avatarImg: { - width: "66px", - height: "66px", - }, - nickName: { - color: "#fff", - marginTop: "15px", - fontSize: "17px", - }, - flexAvatar: { - display: "flex", - justifyContent: "space-between", - alignItems: "flex-start", - }, - groupName: { - color: "#ffffff", - opacity: "0.54", - }, - storageCircle: { - width: "200px", - }, -}); - -class UserInfoCompoment extends Component { - showUserInfo = (e) => { - this.props.setUserPopover(e.currentTarget); - }; - - render() { - const { classes, t } = this.props; - const isLogin = Auth.Check(this.props.isLogin); - const user = Auth.GetUser(this.props.isLogin); - - return ( -
-
- {/* eslint-disable-next-line */} - - {isLogin && ( - - )} - {!isLogin && ( - - )} - - -
-
- - {isLogin ? user.nickname : t("navbar.notLoginIn")} - - - {isLogin ? user.group.name : t("navbar.visitor")} - -
-
- ); - } -} - -UserInfoCompoment.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const UserInfo = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withTranslation()(UserInfoCompoment))); - -export default UserInfo; diff --git a/src/component/Pages/Devices/AppPromotion.tsx b/src/component/Pages/Devices/AppPromotion.tsx new file mode 100644 index 0000000..a322e89 --- /dev/null +++ b/src/component/Pages/Devices/AppPromotion.tsx @@ -0,0 +1,247 @@ +import { useAppSelector } from "../../../redux/hooks.ts"; +import { alpha, Box, Grid, Typography, useTheme } from "@mui/material"; +import { Trans, useTranslation } from "react-i18next"; +import { useMemo, useState } from "react"; +import SessionManager from "../../../session"; +import { QRCodeSVG } from "qrcode.react"; +import { SecondaryButton } from "../../Common/StyledComponents.tsx"; + +const PhoneSkeleton = () => { + const theme = useTheme(); + + return ( + + + + + + ); +}; + +const AppPromotion = () => { + const title = useAppSelector((state) => state.siteConfig.basic.config.title); + const { t } = useTranslation(); + const [showQr, setShowQr] = useState(false); + const theme = useTheme(); + const refreshToken = useMemo(() => { + return ( + window.location.origin + + "/login?refresh_token=" + + (SessionManager.currentLoginOrNull()?.token?.refresh_token ?? "") + ); + }, [showQr]); + + return ( + + + + + + + `linear-gradient(180deg, transparent 82%, ${alpha( + theme.palette.secondary.main, + 0.3, + )} 0%)`, + }} + />, + ]} + /> + + + +
    +
  1. + + {t("setting.downloadOurApp")} + + + + + + +
  2. +
  3. + + {t("setting.fillInEndpoint")} + + + setShowQr(false)} + style={{ + transition: "all .3s ease-in", + filter: showQr ? "initial" : "blur(8px)", + backgroundColor: "#fff", + }} + value={refreshToken} + /> + + {!showQr && ( + setShowQr(true)} + sx={{ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + }} + color={"inherit"} + variant={"outlined"} + > + {t("setting.shoeQr")} + + )} + +
  4. +
  5. + + {t("setting.loginApp")} + +
  6. +
+
+
+
+ + + + + + + + + + theme.palette.mode === "dark" + ? "brightness(0.7)" + : "none", + }} + /> + + + + + +
+ ); +}; + +export default AppPromotion; diff --git a/src/component/Pages/Devices/ConnectionInfoDialog.tsx b/src/component/Pages/Devices/ConnectionInfoDialog.tsx new file mode 100644 index 0000000..e6e5ff8 --- /dev/null +++ b/src/component/Pages/Devices/ConnectionInfoDialog.tsx @@ -0,0 +1,90 @@ +import { useTranslation } from "react-i18next"; +import { + DialogContent, + DialogProps, + FilledInput, + IconButton, + InputAdornment, + InputLabel, + Stack, +} from "@mui/material"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { DavAccount } from "../../../api/setting.ts"; +import FormControl from "@mui/material/FormControl"; +import CopyOutlined from "../../Icons/CopyOutlined.tsx"; +import { copyToClipboard } from "../../../util"; +import SessionManager from "../../../session"; + +export interface ConnectionInfoDialogProps extends DialogProps { + account?: DavAccount; +} + +const InfoTextField = ({ label, value }: { label: string; value: string }) => { + return ( + + {label} + + copyToClipboard(value)}> + + + + } + inputProps={{ + readOnly: true, + }} + /> + + ); +}; + +const ConnectionInfoDialog = ({ + onClose, + account, + ...rest +}: ConnectionInfoDialogProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + return ( + onClose && onClose({}, "escapeKeyDown")} + dialogProps={{ + onClose: onClose, + fullWidth: true, + maxWidth: "xs", + disableRestoreFocus: true, + ...rest, + }} + > + + + + + + + + + ); +}; +export default ConnectionInfoDialog; diff --git a/src/component/Pages/Devices/CreateDAVAccountDialog.tsx b/src/component/Pages/Devices/CreateDAVAccountDialog.tsx new file mode 100644 index 0000000..664a48c --- /dev/null +++ b/src/component/Pages/Devices/CreateDAVAccountDialog.tsx @@ -0,0 +1,193 @@ +import { useTranslation } from "react-i18next"; +import { + Checkbox, + DialogContent, + DialogProps, + FormGroup, + Stack, + Typography, + useTheme, +} from "@mui/material"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { useSnackbar } from "notistack"; +import { useEffect, useMemo, useState } from "react"; +import { PathSelectorForm } from "../../Common/Form/PathSelectorForm.tsx"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import { Filesystem } from "../../../util/uri.ts"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import Tag from "../../Icons/Tag.tsx"; +import DialogAccordion from "../../Dialogs/DialogAccordion.tsx"; +import Boolset from "../../../util/boolset.ts"; +import { GroupPermission } from "../../../api/user.ts"; +import SessionManager from "../../../session"; +import { SmallFormControlLabel } from "../../Common/StyledComponents.tsx"; +import { + sendCreateDavAccounts, + sendUpdateDavAccounts, +} from "../../../api/api.ts"; +import { DavAccount, DavAccountOption } from "../../../api/setting.ts"; + +export interface CreateDAVAccountDialogProps extends DialogProps { + onAccountAdded?: (account: DavAccount) => void; + onAccountUpdated?: (account: DavAccount) => void; + editTarget?: DavAccount; +} + +const CreateDAVAccountDialog = ({ + onClose, + onAccountAdded, + onAccountUpdated, + editTarget, + open, + ...rest +}: CreateDAVAccountDialogProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const [loading, setLoading] = useState(false); + const [name, setName] = useState(""); + const [path, setPath] = useState(defaultPath); + const [readonly, setReadonly] = useState(false); + const [proxy, setProxy] = useState(false); + + const theme = useTheme(); + + const groupProxyEnabled = useMemo(() => { + const perm = new Boolset( + SessionManager.currentLoginOrNull()?.user.group?.permission, + ); + return perm.enabled(GroupPermission.webdav_proxy); + }, []); + + useEffect(() => { + if (open && editTarget) { + setName(editTarget.name); + setPath(editTarget.uri); + const options = new Boolset(editTarget.options); + setReadonly(options.enabled(DavAccountOption.readonly)); + setProxy(options.enabled(DavAccountOption.proxy)); + } + }, [open]); + + const onAccept = () => { + if (!name || !path) { + return; + } + + setLoading(true); + const req = { + name, + uri: path, + proxy, + readonly, + }; + dispatch( + editTarget + ? sendUpdateDavAccounts(editTarget.id, req) + : sendCreateDavAccounts(req), + ) + .then((account) => { + onClose && onClose({}, "escapeKeyDown"); + !editTarget && onAccountAdded && onAccountAdded(account); + editTarget && onAccountUpdated && onAccountUpdated(account); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + + + } + variant="outlined" + value={name} + multiline + onChange={(e) => setName(e.target.value)} + label={t("setting.annotation")} + fullWidth + /> + + + + setReadonly(e.target.checked)} + checked={readonly} + /> + } + label={t("application:setting.readonlyOn")} + /> + + {t("application:setting.readonlyTooltip")} + + {groupProxyEnabled && ( + <> + setProxy(e.target.checked)} + checked={proxy} + /> + } + label={t("application:setting.proxy")} + /> + + {t("application:setting.proxyTooltip")} + + + )} + + + + + + ); +}; +export default CreateDAVAccountDialog; diff --git a/src/component/Pages/Devices/DavAccountList.tsx b/src/component/Pages/Devices/DavAccountList.tsx new file mode 100644 index 0000000..a93ef6d --- /dev/null +++ b/src/component/Pages/Devices/DavAccountList.tsx @@ -0,0 +1,179 @@ +import * as React from "react"; +import { useCallback, useState } from "react"; +import { + Box, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { getDavAccounts } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { DavAccount } from "../../../api/setting.ts"; +import DavAccountRow from "./DavAccountRow.tsx"; +import { NoWrapTableCell } from "../../Common/StyledComponents.tsx"; +import { CellHeaderWithPadding } from "../../FileManager/Dialogs/LockConflictDetails.tsx"; +import CreateDAVAccountDialog from "./CreateDAVAccountDialog.tsx"; +import ConnectionInfoDialog from "./ConnectionInfoDialog.tsx"; + +const defaultPageSize = 50; + +export interface DavAccountListProps { + creatAccountDialog: boolean; + setCreateAccountDialog: (value: boolean) => void; +} + +const DavAccountList = ({ + creatAccountDialog, + setCreateAccountDialog, +}: DavAccountListProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [nextPageToken, setNextPageToken] = useState(""); + const [accounts, setAccounts] = useState([]); + const [loading, setLoading] = useState(false); + const [accountInfoTarget, setAccountInfoTarget] = useState< + DavAccount | undefined + >(); + const [editTarget, setEditTarget] = useState(); + const [editOpen, setEditOpen] = useState(false); + + const loadNextPage = useCallback( + (originAccounts: DavAccount[], token?: string) => () => { + setLoading(true); + dispatch( + getDavAccounts({ + page_size: defaultPageSize, + next_page_token: token, + }), + ) + .then((res) => { + setAccounts([...originAccounts, ...res.accounts]); + if (res.pagination?.next_token) { + setNextPageToken(res.pagination.next_token); + } else { + setNextPageToken(undefined); + } + }) + .catch(() => { + setNextPageToken(undefined); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch], + ); + + const refresh = () => { + loadNextPage([], "")(); + }; + + const onAccountDeleted = useCallback( + (id: string) => { + setAccounts((accounts) => + accounts.filter((account) => account.id !== id), + ); + }, + [setAccounts], + ); + + const onAccountAdded = (account: DavAccount) => { + setAccounts([account, ...accounts]); + setAccountInfoTarget(account); + }; + + const onEditOpen = (account: DavAccount) => { + setEditOpen(true); + setEditTarget(account); + }; + + return ( + + setCreateAccountDialog(false)} + onAccountAdded={onAccountAdded} + /> + setEditOpen(false)} + onAccountUpdated={(account) => + setAccounts(accounts.map((a) => (a.id == account.id ? account : a))) + } + /> + setAccountInfoTarget(undefined)} + /> + + + + + + {t("setting.annotation")} + + + + {t("setting.rootFolder")} + + + + {t("fileManager.permissions")} + + + {t("setting.proxy")} + + + {t("fileManager.createdAt")} + + + {t("fileManager.actions")} + + + + + {accounts.map((account) => ( + setAccountInfoTarget(account)} + key={account.id} + account={account} + onAccountDeleted={onAccountDeleted} + onEditClicked={() => onEditOpen(account)} + /> + ))} + {nextPageToken != undefined && ( + <> + {[...Array(4)].map((_, i) => ( + + ))} + + )} + +
+ {nextPageToken == undefined && accounts.length == 0 && ( + + + {t("application:setting.listEmpty")} + + + )} +
+
+ ); +}; + +export default DavAccountList; diff --git a/src/component/Pages/Devices/DavAccountRow.tsx b/src/component/Pages/Devices/DavAccountRow.tsx new file mode 100644 index 0000000..73fd9c3 --- /dev/null +++ b/src/component/Pages/Devices/DavAccountRow.tsx @@ -0,0 +1,196 @@ +import * as React from "react"; +import { useEffect, useMemo } from "react"; +import { + IconButton, + ListItemText, + Menu, + Skeleton, + TableRow, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { NoWrapCell, SquareChip } from "../../Common/StyledComponents.tsx"; +import { DavAccount, DavAccountOption } from "../../../api/setting.ts"; +import FileBadge from "../../FileManager/FileBadge.tsx"; +import { FileType } from "../../../api/explorer.ts"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import Boolset from "../../../util/boolset.ts"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import { useInView } from "react-intersection-observer"; +import Eye from "../../Icons/Eye.tsx"; +import { Edit } from "@mui/icons-material"; +import CloudFilled from "../../Icons/CloudFilled.tsx"; +import { + bindMenu, + bindTrigger, + usePopupState, +} from "material-ui-popup-state/hooks"; +import MoreVertical from "../../Icons/MoreVertical.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import { sendDeleteDavAccount } from "../../../api/api.ts"; + +export interface DavAccountRowProps { + account?: DavAccount; + onLoad?: () => void; + loading?: boolean; + onAccountDeleted: (id: string) => void; + onClick?: () => void; + onEditClicked?: (account: DavAccount) => void; +} + +const DavAccountRow = ({ + account, + onAccountDeleted, + onLoad, + loading, + onClick, + onEditClicked, +}: DavAccountRowProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const { ref, inView } = useInView({ + rootMargin: "200px 0px", + triggerOnce: true, + skip: !loading || !onLoad, + }); + + const actionMenuState = usePopupState({ + variant: "popover", + popupId: "action" + account?.id, + }); + + useEffect(() => { + if (!inView) { + return; + } + + if (onLoad) { + onLoad(); + } + }, [inView]); + + const [readOnly, proxy] = useMemo(() => { + if (!account?.options) return [false, false] as const; + const bs = new Boolset(account.options); + return [ + bs.enabled(DavAccountOption.readonly), + bs.enabled(DavAccountOption.proxy), + ] as const; + }, [account?.options]); + + const { onClose, ...rest } = bindMenu(actionMenuState); + + const onEdit = () => { + if (account) { + onEditClicked?.(account); + } + onClose && onClose(); + }; + + const onDelete = () => { + if (!account?.id) { + return; + } + + onClose(); + dispatch(sendDeleteDavAccount(account.id)); + onAccountDeleted(account.id); + }; + + return ( + + + {loading ? ( + + ) : ( + account?.name + )} + + + {loading ? ( + + ) : ( + + )} + + + {loading ? ( + + ) : readOnly ? ( + } + size={"small"} + label={t("setting.readonlyOn")} + /> + ) : ( + } + size={"small"} + label={t("setting.readonlyOff")} + /> + )} + + + {loading ? ( + + ) : proxy ? ( + } + sx={{ + backgroundColor: (theme) => theme.palette.primary.light, + }} + color={"success"} + size={"small"} + label={t("application:setting.proxied")} + /> + ) : ( + t("setting.none") + )} + + + {loading || !account ? ( + + ) : ( + + )} + + e.stopPropagation()}> + {account && ( + + + + )} + + e.stopPropagation()} + onClose={onClose} + slotProps={{ + paper: { + sx: { + minWidth: 150, + }, + }, + }} + {...rest} + > + + {t(`fileManager.edit`)} + + + {t(`fileManager.delete`)} + + + + ); +}; + +export default DavAccountRow; diff --git a/src/component/Pages/Devices/Devices.tsx b/src/component/Pages/Devices/Devices.tsx new file mode 100644 index 0000000..161d565 --- /dev/null +++ b/src/component/Pages/Devices/Devices.tsx @@ -0,0 +1,108 @@ +import PageHeader, { PageTabQuery } from "../PageHeader.tsx"; +import { Button, Container, Grow } from "@mui/material"; +import * as React from "react"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; +import ResponsiveTabs from "../../Common/ResponsiveTabs.tsx"; +import DavAccountList from "./DavAccountList.tsx"; +import Add from "../../Icons/Add.tsx"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import SessionManager from "../../../session"; +import Boolset from "../../../util/boolset.ts"; +import { GroupPermission } from "../../../api/user.ts"; +import AppPromotion from "./AppPromotion.tsx"; +import PageContainer from "../PageContainer.tsx"; + +export enum DevicePageTab { + Dav = "dav", + App = "app", +} + +const Devices = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [searchParams, setSearchParams] = useSearchParams(); + const [creatAccountDialog, setCreateAccountDialog] = useState(false); + const appPromotion = useAppSelector( + (state) => state.siteConfig.app.config?.app_promotion, + ); + + const webDavEnabled = useMemo(() => { + const user = SessionManager.currentLoginOrNull(); + let enabled = false; + if (user && user.user.group?.permission) { + const bs = new Boolset(user.user.group.permission); + enabled = bs.enabled(GroupPermission.webdav); + } + + return enabled; + }, []); + + const tabs = useMemo(() => { + const res = []; + if (webDavEnabled) { + res.push({ + label: t("application:setting.webdavAccounts"), + value: DevicePageTab.Dav, + }); + } + if (appPromotion) { + res.push({ + label: t("application:setting.iOSApp"), + value: DevicePageTab.App, + }); + } + return res; + }, [webDavEnabled, appPromotion]); + + const [tab, setTab] = useState( + searchParams.get(PageTabQuery) ?? + (webDavEnabled ? DevicePageTab.Dav : DevicePageTab.App), + ); + + useEffect(() => { + dispatch(loadSiteConfig("app")); + }, []); + + return ( + + + + + + } + title={t("application:navbar.connect")} + /> + setTab(newValue)} + tabs={tabs} + /> + {tab == DevicePageTab.Dav && webDavEnabled && ( + + )} + {tab == DevicePageTab.App && appPromotion && } + + {!webDavEnabled && !appPromotion && ( + + )} + + + ); +}; + +export default Devices; diff --git a/src/component/Pages/HomeRedirect.tsx b/src/component/Pages/HomeRedirect.tsx new file mode 100644 index 0000000..5de2ec5 --- /dev/null +++ b/src/component/Pages/HomeRedirect.tsx @@ -0,0 +1,16 @@ +import SessionManager from "../../session"; +import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; + +export const HomeRedirect = () => { + const navigate = useNavigate(); + useEffect(() => { + if (SessionManager.currentLoginOrNull()) { + navigate("/home"); + } else { + navigate("/session"); + } + }, []); + + return
; +}; diff --git a/src/component/Pages/Login/Activate.tsx b/src/component/Pages/Login/Activate.tsx new file mode 100644 index 0000000..717d487 --- /dev/null +++ b/src/component/Pages/Login/Activate.tsx @@ -0,0 +1,93 @@ +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useNavigate } from "react-router-dom"; +import { useSnackbar } from "notistack"; +import { useQuery } from "../../../util"; +import React, { useEffect, useState } from "react"; +import { Box, Button, Typography } from "@mui/material"; +import PageTitle from "../../../router/PageTitle.tsx"; +import CheckmarkCircle from "../../Icons/CheckmarkCircle.tsx"; +import { setHeadlessFrameLoading } from "../../../redux/globalStateSlice.ts"; +import { sendEmailActivate } from "../../../api/api.ts"; + +const Activate = () => { + const { t } = useTranslation(); + const { reg_captcha } = useAppSelector( + (state) => state.siteConfig.login.config, + ); + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const query = useQuery(); + + const [success, setSuccess] = useState(true); + const [email, setEmail] = useState(""); + + useEffect(() => { + const sign = query.get("sign"); + const id = query.get("id"); + if (!sign || !id) { + setSuccess(false); + navigate("/session"); + return; + } + + dispatch(setHeadlessFrameLoading(true)); + dispatch(sendEmailActivate(id, decodeURIComponent(sign))) + .then((u) => { + setEmail(u?.email ?? ""); + setSuccess(true); + }) + .catch(() => { + navigate("/session"); + }) + .finally(() => { + dispatch(setHeadlessFrameLoading(false)); + }); + }, []); + + return ( + + + + {success && ( + <> + + + theme.palette.success.main, + mt: 1, + }} + > + {t("application:login.accountActivated")} + + + + + )} + + + ); +}; + +export default Activate; diff --git a/src/component/Pages/Login/Phases/Phase2FA.tsx b/src/component/Pages/Login/Phases/Phase2FA.tsx new file mode 100644 index 0000000..d2fd7df --- /dev/null +++ b/src/component/Pages/Login/Phases/Phase2FA.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from "react-i18next"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { Control } from "../Signin/SignIn.tsx"; +import { FormControl, styled, Typography } from "@mui/material"; +import { MuiOtpInput } from "mui-one-time-password-input"; + +interface Phase2FAProps { + control?: Control; + otp: string; + onOtpChange: (otp: string) => void; + loading: boolean; +} + +const MuiOtpInputStyled = styled(MuiOtpInput)` + display: flex; + gap: 8px; + max-width: 650px; + margin-inline: auto; +`; + +const Phase2FA = ({ control, otp, onOtpChange, loading }: Phase2FAProps) => { + const { t } = useTranslation(); + const regEnabled = useAppSelector( + (state) => state.siteConfig.login.config.register_enabled, + ); + return ( + <> + + {t("login.input2FACode")} + + + + + + {control?.submit} + {control?.back} + + ); +}; + +export default Phase2FA; diff --git a/src/component/Pages/Login/Phases/PhaseCollectEmail.tsx b/src/component/Pages/Login/Phases/PhaseCollectEmail.tsx new file mode 100644 index 0000000..f51a12a --- /dev/null +++ b/src/component/Pages/Login/Phases/PhaseCollectEmail.tsx @@ -0,0 +1,106 @@ +import { Box, Divider, FormControl, Link, Stack } from "@mui/material"; +import { useEffect } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { useQuery } from "../../../../util"; +import { OutlineIconTextField } from "../../../Common/Form/OutlineIconTextField.tsx"; +import MailOutlined from "../../../Icons/MailOutlined.tsx"; +import PasskeyLoginButton from "../Signin/PasskeyLoginButton.tsx"; +import { Control } from "../Signin/SignIn.tsx"; + +export const LegalLinks = () => { + const { t } = useTranslation(); + const tos = useAppSelector((state) => state.siteConfig.login.config.tos_url); + const privacyPolicy = useAppSelector((state) => state.siteConfig.login.config.privacy_policy_url); + return ( + <> + {(tos || privacyPolicy) && ( + + {tos && ( + + {t("login.termOfUse")} + + )} + {tos && privacyPolicy && " | "} + {privacyPolicy && ( + + {t("login.privacyPolicy")} + + )} + + )} + + ); +}; + +interface PhaseCollectEmailProps { + email: string; + setEmail: (email: string) => void; + control?: Control; +} + +const PhaseCollectEmail = ({ email, setEmail, control }: PhaseCollectEmailProps) => { + const { t } = useTranslation(); + const query = useQuery(); + const { register_enabled, authn } = useAppSelector((state) => state.siteConfig.login.config); + const tos = useAppSelector((state) => state.siteConfig.login.config.tos_url); + const privacyPolicy = useAppSelector((state) => state.siteConfig.login.config.privacy_policy_url); + + const showFooter = tos || privacyPolicy || authn; + + useEffect(() => { + if (!!query.get("email")) { + setEmail(query.get("email") ?? ""); + } + }, []); + + return ( + <> + + setEmail(e.target.value)} + icon={} + autoComplete={"username webauthn"} + value={email} + autoFocus + /> + + {control?.submit} + {control?.back} + {register_enabled && ( + + ]} + /> + + )} + {showFooter && ( + <> + + {authn && } + + + )} + + ); +}; + +export default PhaseCollectEmail; diff --git a/src/component/Pages/Login/Phases/PhaseCollectPassword.tsx b/src/component/Pages/Login/Phases/PhaseCollectPassword.tsx new file mode 100644 index 0000000..8e09669 --- /dev/null +++ b/src/component/Pages/Login/Phases/PhaseCollectPassword.tsx @@ -0,0 +1,87 @@ +import { Divider, FormControl, Link, Stack, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { PrepareLoginResponse } from "../../../../api/user.ts"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { Captcha, CaptchaParams } from "../../../Common/Captcha/Captcha.tsx"; +import { OutlineIconTextField } from "../../../Common/Form/OutlineIconTextField.tsx"; +import Password from "../../../Icons/Password.tsx"; +import PasskeyLoginButton from "../Signin/PasskeyLoginButton.tsx"; +import { Control } from "../Signin/SignIn.tsx"; + +interface PhaseCollectPasswordProps { + pwd: string; + email: string; + setPwd: (pwd: string) => void; + control?: Control; + loginOptions?: PrepareLoginResponse; + captchaGen: number; + setCaptchaState: (state: CaptchaParams) => void; + onForget?: () => void; +} + +const PhaseCollectPassword = ({ + pwd, + email, + setPwd, + control, + loginOptions, + captchaGen, + setCaptchaState, + onForget, +}: PhaseCollectPasswordProps) => { + const { t } = useTranslation(); + const { login_captcha, authn } = useAppSelector((state) => state.siteConfig.login.config); + + const moreOptions = loginOptions?.webauthn_enabled && authn; + return ( + <> + {loginOptions?.password_enabled && ( + <> + {t("login.enterPasswordHint", { email: email })} + + setPwd(e.target.value)} + icon={} + value={pwd} + autoComplete={"true"} + /> + + {t("login.forgetPassword")} + + + {login_captcha && ( + + + + )} + {control?.submit} + {moreOptions && ( + + + {t("login.or")} + + + )} + + )} + {!loginOptions?.password_enabled && ( + + {t("login.paswordlessHint", { email: email })} + + )} + {loginOptions?.webauthn_enabled && authn && } + {control?.back} + + ); +}; + +export default PhaseCollectPassword; diff --git a/src/component/Pages/Login/Phases/PhaseForgetPassword.tsx b/src/component/Pages/Login/Phases/PhaseForgetPassword.tsx new file mode 100644 index 0000000..ea11103 --- /dev/null +++ b/src/component/Pages/Login/Phases/PhaseForgetPassword.tsx @@ -0,0 +1,42 @@ +import { useTranslation } from "react-i18next"; +import { Control } from "../Signin/SignIn.tsx"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { FormControl } from "@mui/material"; +import { Captcha, CaptchaParams } from "../../../Common/Captcha/Captcha.tsx"; + +interface PhaseForgetPasswordProps { + email: string; + control?: Control; + captchaGen: number; + setCaptchaState: (state: CaptchaParams) => void; +} + +const PhaseForgetPassword = ({ + captchaGen, + setCaptchaState, + control, +}: PhaseForgetPasswordProps) => { + const { t } = useTranslation(); + const { forget_captcha } = useAppSelector( + (state) => state.siteConfig.login.config, + ); + + return ( + <> + {forget_captcha && ( + + + + )} + {control?.submit} + {control?.back} + + ); +}; + +export default PhaseForgetPassword; diff --git a/src/component/Pages/Login/Phases/PhaseSignupNeeded.tsx b/src/component/Pages/Login/Phases/PhaseSignupNeeded.tsx new file mode 100644 index 0000000..f2119df --- /dev/null +++ b/src/component/Pages/Login/Phases/PhaseSignupNeeded.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Stack, Typography } from "@mui/material"; +import { useAppSelector } from "../../../../redux/hooks.ts"; +import { Control } from "../Signin/SignIn.tsx"; + +interface SignupNeededProps { + email: string; + control?: Control; +} + +const SignupNeeded = ({ email, control }: SignupNeededProps) => { + const { t } = useTranslation(); + const regEnabled = useAppSelector( + (state) => state.siteConfig.login.config.register_enabled, + ); + return ( + <> + {regEnabled && ( + + {t("login.signupHint", { email: email })} + + )} + {!regEnabled && ( + + {t("login.accountNotFoundHint", { email: email })} + + )} + + {regEnabled && control?.submit} + + {control?.back} + + ); +}; + +export default SignupNeeded; diff --git a/src/component/Pages/Login/Reset.tsx b/src/component/Pages/Login/Reset.tsx new file mode 100644 index 0000000..a53c9ae --- /dev/null +++ b/src/component/Pages/Login/Reset.tsx @@ -0,0 +1,133 @@ +import { LoadingButton } from "@mui/lab"; +import { Box, FormControl, Link, Typography } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { sendReset } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import PageTitle from "../../../router/PageTitle.tsx"; +import { useQuery } from "../../../util"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; +import Password from "../../Icons/Password.tsx"; + +const Reset = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const query = useQuery(); + + const formRef = useRef(null); + const [password, setPassword] = useState(""); + const [passwordRepeat, setPasswordRepeat] = useState(""); + const [loading, setLoading] = useState(false); + + const submit = () => { + if (!formRef.current) { + return; + } + + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + + if (passwordRepeat != password) { + enqueueSnackbar({ + message: t("login.passwordNotMatch"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + setLoading(true); + dispatch( + sendReset(query.get("id") ?? "0", { + password, + secret: query.get("secret") ?? "", + }), + ) + .then((u) => { + navigate("/session?phase=email&email=" + encodeURIComponent(u?.email ?? "")); + enqueueSnackbar({ + message: t("login.passwordReset"), + variant: "success", + action: DefaultCloseAction, + }); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + + + {t("login.repeatPassword")} + + + + setPassword(e.target.value)} + icon={} + value={password} + autoComplete={"false"} + /> + + + setPasswordRepeat(e.target.value)} + icon={} + value={passwordRepeat} + autoComplete={"false"} + /> + + + {t("login.resetPassword")} + + + ]} + /> + + + + + + ); +}; + +export default Reset; diff --git a/src/component/Pages/Login/SessionIntro.tsx b/src/component/Pages/Login/SessionIntro.tsx new file mode 100644 index 0000000..8aafddf --- /dev/null +++ b/src/component/Pages/Login/SessionIntro.tsx @@ -0,0 +1,26 @@ +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { useEffect } from "react"; +import { ConfigLoadState } from "../../../redux/siteConfigSlice.ts"; +import { setHeadlessFrameLoading } from "../../../redux/globalStateSlice.ts"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import { Outlet } from "react-router-dom"; + +const SessionIntro = () => { + const dispatch = useAppDispatch(); + const loginConfigLoading = useAppSelector( + (state) => state.siteConfig.login.loaded, + ); + useEffect(() => { + dispatch(loadSiteConfig("login")); + }, []); + useEffect(() => { + if (loginConfigLoading == ConfigLoadState.NotLoaded) { + dispatch(setHeadlessFrameLoading(true)); + } else { + dispatch(setHeadlessFrameLoading(false)); + } + }, [loginConfigLoading]); + return <>{loginConfigLoading != ConfigLoadState.NotLoaded && }; +}; + +export default SessionIntro; diff --git a/src/component/Pages/Login/SideTransition.css b/src/component/Pages/Login/SideTransition.css new file mode 100644 index 0000000..79269cd --- /dev/null +++ b/src/component/Pages/Login/SideTransition.css @@ -0,0 +1,21 @@ +.side-enter { + opacity: 0; + transform: translateX(-100%); +} +.side-enter-active { + opacity: 1; + transform: translateX(0%); +} +.side-exit { + opacity: 1; + transform: translateX(0%); +} +.side-exit-active { + opacity: 0; + transform: translateX(100%); +} +.side-enter-active, +.side-exit-active { + transition: opacity 300ms, transform 300ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/src/component/Pages/Login/Signin/PasskeyLoginButton.tsx b/src/component/Pages/Login/Signin/PasskeyLoginButton.tsx new file mode 100644 index 0000000..39bde48 --- /dev/null +++ b/src/component/Pages/Login/Signin/PasskeyLoginButton.tsx @@ -0,0 +1,117 @@ +import { LoadingButton } from "@mui/lab"; +import { ButtonProps } from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendFinishPasskeyLogin, sendPreparePasskeyLogin } from "../../../../api/api.ts"; +import { setHeadlessFrameLoading } from "../../../../redux/globalStateSlice.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { refreshUserSession } from "../../../../redux/thunks/session.ts"; +import { bufferEncode, urlBase64BufferDecode, useQuery } from "../../../../util"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import Fingerprint from "../../../Icons/Fingerprint.tsx"; + +export interface PasskeyLoginButtonProps extends ButtonProps { + autoComplete?: boolean; +} + +export default function PasskeyLoginButton({ autoComplete, ...rest }: PasskeyLoginButtonProps) { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const query = useQuery(); + + const [loading, setLoading] = useState(false); + const abortRef = useRef(new AbortController()); + + useEffect(() => { + abortRef.current = new AbortController(); + if ( + autoComplete && + navigator.credentials && + window.PublicKeyCredential && + PublicKeyCredential.isConditionalMediationAvailable + ) { + PublicKeyCredential.isConditionalMediationAvailable().then((v) => { + if (!abortRef.current.signal.aborted) startLogin(true)(); + }); + } + return () => { + abortRef.current?.abort(); + }; + }, []); + + const startLogin = (conditional: boolean) => async () => { + if (!navigator.credentials || !window.PublicKeyCredential) { + enqueueSnackbar({ + message: t("setting.browserNotSupported"), + variant: "warning", + action: DefaultCloseAction, + }); + + return; + } + + if (!conditional) { + abortRef.current.abort(); + } + + setLoading(!conditional); + try { + const opts = await dispatch(sendPreparePasskeyLogin()); + const credential = await navigator.credentials.get({ + publicKey: { + // url decode base64 + challenge: urlBase64BufferDecode(opts.options.publicKey.challenge), + timeout: opts.options.publicKey.timeout, + rpId: opts.options.publicKey.rpId, + }, + ...(conditional ? { mediation: "conditional", signal: abortRef.current.signal } : {}), + }); + if (credential) { + dispatch(setHeadlessFrameLoading(true)); + const c = credential as PublicKeyCredential; + const response = await dispatch( + sendFinishPasskeyLogin({ + session_id: opts.session_id, + response: JSON.stringify({ + id: credential.id, + type: credential.type, + rawId: bufferEncode(c.rawId), + response: { + // @ts-ignore + attestationObject: bufferEncode(c.response.attestationObject), + clientDataJSON: bufferEncode(c.response.clientDataJSON), + // @ts-ignore + signature: bufferEncode(c.response.signature), + // @ts-ignore + userHandle: bufferEncode(c.response.userHandle), + // @ts-ignore + authenticatorData: bufferEncode(c.response.authenticatorData), + }, + }), + }), + ); + dispatch(refreshUserSession(response, query.get("redirect"))); + } + } catch (e) { + console.log(e); + } finally { + !conditional && setLoading(false); + dispatch(setHeadlessFrameLoading(false)); + } + }; + + return ( + } + fullWidth + {...rest} + > + {t("login.useFIDO2")} + + ); +} diff --git a/src/component/Pages/Login/Signin/SignIn.tsx b/src/component/Pages/Login/Signin/SignIn.tsx new file mode 100644 index 0000000..83334fd --- /dev/null +++ b/src/component/Pages/Login/Signin/SignIn.tsx @@ -0,0 +1,300 @@ +import { ArrowBackIos } from "@mui/icons-material"; +import { LoadingButton } from "@mui/lab"; +import { Box, Button, Typography } from "@mui/material"; +import { enqueueSnackbar } from "notistack"; +import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { send2FALogin, sendLogin, sendPrepareLogin, sendResetEmail } from "../../../../api/api.ts"; +import { AppError, Code } from "../../../../api/request.ts"; +import { PrepareLoginResponse } from "../../../../api/user.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { refreshUserSession } from "../../../../redux/thunks/session.ts"; +import PageTitle from "../../../../router/PageTitle.tsx"; +import { useQuery } from "../../../../util"; +import { CaptchaParams } from "../../../Common/Captcha/Captcha.tsx"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import Phase2FA from "../Phases/Phase2FA.tsx"; +import PhaseCollectEmail from "../Phases/PhaseCollectEmail.tsx"; +import PhaseCollectPassword from "../Phases/PhaseCollectPassword.tsx"; +import PhaseForgetPassword from "../Phases/PhaseForgetPassword.tsx"; +import PhaseSignupNeeded from "../Phases/PhaseSignupNeeded.tsx"; +import "../SideTransition.css"; + +enum EmailLoginPhase { + CollectEmail, + CollectPassword, + SignupNeeded, + Collect2FA, + ForgetPassword, +} + +export interface Control { + submit: JSX.Element; + back: JSX.Element; +} + +interface phaseSetting { + title: string; + nextButtonText: string; + showBackButton: boolean; + previous?: EmailLoginPhase; + control?: Control; +} + +const EmailLogin = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const query = useQuery(); + + const [phase, setPhase] = useState(EmailLoginPhase.CollectEmail); + const [email, setEmail] = useState(""); + const [pwd, setPwd] = useState(""); + const [otp, setOTP] = useState(""); + const [captchaGen, setCaptchaGen] = useState(0); + const [loading, setLoading] = useState(false); + const captchaState = useRef(); + const twoFaSession = useRef(""); + const [direction, setDirection] = useState<"left" | "right" | "up" | "down">("right"); + const [loginOptions, setLoginOptions] = useState(); + + const prepareLogin = useCallback(async () => { + try { + setLoading(true); + const opts = await dispatch(sendPrepareLogin(email)); + setLoginOptions(opts); + setPhase(EmailLoginPhase.CollectPassword); + } catch (e) { + if (e instanceof AppError && e.code === Code.NodeFound) { + // User not registered yet, guided to register + setPhase(EmailLoginPhase.SignupNeeded); + } + } finally { + setLoading(false); + } + }, [dispatch, email, setPhase]); + + const passwordLogin = useCallback( + async (email: string, password: string, captcha?: CaptchaParams) => { + try { + setLoading(true); + const loginRes = await dispatch(sendLogin({ email, password, ...captcha })); + dispatch(refreshUserSession(loginRes, query.get("redirect"))); + } catch (e) { + if (e instanceof AppError && e.code === Code.Continue) { + twoFaSession.current = e.response.data; + // User not registered yet, guided to register + setPhase(EmailLoginPhase.Collect2FA); + } else { + setCaptchaGen((g) => g + 1); + } + } finally { + setLoading(false); + } + }, + [dispatch, setPhase, setCaptchaGen, setLoading], + ); + + const finish2FA = useCallback(async (otp: string, ticket: string) => { + try { + setLoading(true); + const loginRes = await dispatch(send2FALogin({ otp, session_id: ticket })); + dispatch(refreshUserSession(loginRes, query.get("redirect"))); + } finally { + setOTP(""); + setLoading(false); + } + }, []); + + const submitSendResetEmail = useCallback( + async (email: string, captcha?: CaptchaParams) => { + try { + setLoading(true); + await dispatch(sendResetEmail({ email, ...captcha })); + setPhase(EmailLoginPhase.CollectEmail); + enqueueSnackbar({ + message: t("login.resetEmailSent"), + variant: "success", + action: DefaultCloseAction, + }); + } catch (e) { + setCaptchaGen((g) => g + 1); + } finally { + setLoading(false); + } + }, + [dispatch, setPhase, setCaptchaGen, setLoading], + ); + + const submit = (e?: FormEvent) => { + e?.preventDefault(); + switch (phase) { + case EmailLoginPhase.CollectEmail: + prepareLogin(); + break; + case EmailLoginPhase.SignupNeeded: + navigate(`/session/signup?email=${email}`); + break; + case EmailLoginPhase.CollectPassword: + passwordLogin(email, pwd, captchaState.current); + break; + case EmailLoginPhase.Collect2FA: + finish2FA(otp, twoFaSession.current); + break; + case EmailLoginPhase.ForgetPassword: + submitSendResetEmail(email, captchaState.current); + break; + } + }; + + useEffect(() => { + if (otp.length === 6) { + submit(); + } + }, [otp]); + + const phaseConfig = useMemo((): phaseSetting => { + var phaseSetting: phaseSetting = { + title: t("login.siginToYourAccount"), + nextButtonText: t("login.continue"), + showBackButton: false, + }; + switch (phase) { + case EmailLoginPhase.CollectEmail: + phaseSetting = { + title: t("login.siginToYourAccount"), + nextButtonText: t("login.continue"), + showBackButton: false, + }; + break; + case EmailLoginPhase.SignupNeeded: + phaseSetting = { + title: t("login.siginToYourAccount"), + nextButtonText: t("login.signUpAccount"), + showBackButton: true, + previous: EmailLoginPhase.CollectEmail, + }; + break; + case EmailLoginPhase.CollectPassword: + phaseSetting = { + title: t("login.enterPassword"), + nextButtonText: t("login.signIn"), + showBackButton: true, + previous: EmailLoginPhase.CollectEmail, + }; + break; + case EmailLoginPhase.Collect2FA: + phaseSetting = { + title: t("login.2FA"), + nextButtonText: t("login.signIn"), + showBackButton: true, + previous: EmailLoginPhase.CollectPassword, + }; + break; + case EmailLoginPhase.ForgetPassword: + phaseSetting = { + title: t("login.resetPassword"), + nextButtonText: t("login.sendMeAnEmail"), + showBackButton: true, + previous: EmailLoginPhase.CollectPassword, + }; + break; + default: + break; + } + phaseSetting.control = { + submit: ( + + {phaseSetting.nextButtonText} + + ), + back: ( + <> + {phaseSetting.showBackButton && ( + + )} + + ), + }; + return phaseSetting; + }, [phase, t, loading]); + + return ( + + {phaseConfig.title} + + + node.addEventListener("transitionend", done, false)} + classNames="side" + key={phase} + > + + {phase === EmailLoginPhase.CollectPassword && ( + setPhase(EmailLoginPhase.ForgetPassword)} + setPwd={setPwd} + control={phaseConfig.control} + loginOptions={loginOptions} + captchaGen={captchaGen} + setCaptchaState={(s) => (captchaState.current = s)} + /> + )} + {phase === EmailLoginPhase.SignupNeeded && ( + + )} + {phase === EmailLoginPhase.Collect2FA && ( + setOTP(t)} + control={phaseConfig.control} + /> + )} + {phase === EmailLoginPhase.CollectEmail && ( + + )} + {phase === EmailLoginPhase.ForgetPassword && ( + (captchaState.current = s)} + control={phaseConfig.control} + /> + )} + + + + + + ); +}; + +const SignIn = () => { + const { t } = useTranslation(); + + return ( + + + + + ); +}; + +export default SignIn; diff --git a/src/component/Pages/Login/Signup.tsx b/src/component/Pages/Login/Signup.tsx new file mode 100644 index 0000000..ca4d2bc --- /dev/null +++ b/src/component/Pages/Login/Signup.tsx @@ -0,0 +1,253 @@ +import { LoadingButton } from "@mui/lab"; +import { Box, Divider, FormControl, Link, Typography } from "@mui/material"; +import i18next from "i18next"; +import { useSnackbar } from "notistack"; +import { useEffect, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { sendSinUp } from "../../../api/api.ts"; +import { AppError, Code } from "../../../api/request.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import PageTitle from "../../../router/PageTitle.tsx"; +import { useQuery } from "../../../util"; +import { Captcha, CaptchaParams } from "../../Common/Captcha/Captcha.tsx"; +import { OutlineIconTextField } from "../../Common/Form/OutlineIconTextField.tsx"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; +import EmailClock from "../../Icons/EmailClock.tsx"; +import MailOutlined from "../../Icons/MailOutlined.tsx"; +import Password from "../../Icons/Password.tsx"; +import { LegalLinks } from "./Phases/PhaseCollectEmail.tsx"; + +export enum SignUpPhase { + Main, + EmailActivation, +} + +interface signUpPhaseProps { + title: string; +} + +const signUpPhaseSettings: Record = { + [SignUpPhase.Main]: { + title: "login.createNewAccount", + }, + [SignUpPhase.EmailActivation]: { + title: "login.lastStep", + }, +}; + +const SignUp = () => { + const { t } = useTranslation(); + const { reg_captcha } = useAppSelector((state) => state.siteConfig.login.config); + const tos = useAppSelector((state) => state.siteConfig.login.config.tos_url); + const privacyPolicy = useAppSelector((state) => state.siteConfig.login.config.privacy_policy_url); + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const query = useQuery(); + + const [phase, setPhase] = useState(SignUpPhase.Main); + const formRef = useRef(null); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [passwordRepeat, setPasswordRepeat] = useState(""); + const [captchaGen, setCaptchaGen] = useState(0); + const captchaState = useRef(); + const [loading, setLoading] = useState(false); + + const showFooter = tos || privacyPolicy; + + useEffect(() => { + if (!!query.get("email")) { + setEmail(query.get("email") ?? ""); + } + }, []); + + const submit = () => { + if (!formRef.current) { + return; + } + + if (!formRef.current.checkValidity()) { + formRef.current.reportValidity(); + return; + } + + if (passwordRepeat != password) { + enqueueSnackbar({ + message: t("login.passwordNotMatch"), + variant: "warning", + action: DefaultCloseAction, + }); + return; + } + + setLoading(true); + dispatch( + sendSinUp({ + email, + password, + language: i18next.language, + ...captchaState.current, + }), + ) + .then(() => { + navigate("/session?phase=email&email=" + encodeURIComponent(email)); + enqueueSnackbar({ + message: t("login.signUpSuccess"), + variant: "success", + action: DefaultCloseAction, + }); + }) + .catch((e) => { + if (e instanceof AppError && e.code === Code.Continue) { + setPhase(SignUpPhase.EmailActivation); + } else { + setCaptchaGen((g) => g + 1); + } + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + + + {t(signUpPhaseSettings[phase].title)} + + + node.addEventListener("transitionend", done, false)} + classNames="side" + key={phase} + > + + {phase === SignUpPhase.EmailActivation && ( + + t.palette.action.disabled, + }} + /> + + {t("application:login.activateDescription")} + + + )} + {phase === SignUpPhase.Main && ( + + + setEmail(e.target.value)} + icon={} + value={email} + autoFocus + /> + + + setPassword(e.target.value)} + icon={} + value={password} + autoComplete={"false"} + /> + + + setPasswordRepeat(e.target.value)} + icon={} + value={passwordRepeat} + autoComplete={"false"} + /> + + {reg_captcha && ( + + (captchaState.current = s)} + /> + + )} + + {t("login.signUp")} + + + ]} + /> + + {showFooter && ( + <> + + + + )} + + )} + + + + + + + ); +}; + +export default SignUp; diff --git a/src/component/Pages/NoMatch.tsx b/src/component/Pages/NoMatch.tsx new file mode 100644 index 0000000..0a96b8f --- /dev/null +++ b/src/component/Pages/NoMatch.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from "react-i18next"; +import { Box, Typography } from "@mui/material"; +import React from "react"; +import DismissCircleFilled from "../Icons/DismissCircleFilled.tsx"; + +const NoMatch = () => { + const { t } = useTranslation(); + return ( + + + theme.palette.action.active, + mt: 1, + }} + > + {t("common:pageNotFound")} + + + ); +}; + +export default NoMatch; diff --git a/src/component/Pages/PageContainer.tsx b/src/component/Pages/PageContainer.tsx new file mode 100644 index 0000000..c3bb492 --- /dev/null +++ b/src/component/Pages/PageContainer.tsx @@ -0,0 +1,22 @@ +import { BoxProps, useMediaQuery, useTheme } from "@mui/material"; +import { RadiusFrame } from "../Frame/RadiusFrame.tsx"; + +const PageContainer = (props: BoxProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + return ( + + ); +}; + +export default PageContainer; diff --git a/src/component/Pages/PageHeader.tsx b/src/component/Pages/PageHeader.tsx new file mode 100644 index 0000000..1b9fac0 --- /dev/null +++ b/src/component/Pages/PageHeader.tsx @@ -0,0 +1,45 @@ +import { Box, IconButton, Tooltip, Typography } from "@mui/material"; +import ArrowClockwiseFilled from "../Icons/ArrowClockwiseFilled.tsx"; +import { useTranslation } from "react-i18next"; +import PageTitle from "../../router/PageTitle.tsx"; + +export const PageTabQuery = "tab"; + +export interface PageHeaderProps { + title: string; + loading?: boolean; + onRefresh?: () => void; + skipChangingDocumentTitle?: boolean; + secondaryAction?: React.ReactNode; +} + +const PageHeader = ({ + title, + secondaryAction, + onRefresh, + loading, + skipChangingDocumentTitle, +}: PageHeaderProps) => { + const { t } = useTranslation(); + return ( + + + + {title} + + {!skipChangingDocumentTitle && } + {onRefresh && ( + + + + + + )} + + {secondaryAction && secondaryAction} + + + ); +}; + +export default PageHeader; diff --git a/src/component/Pages/Pages.tsx b/src/component/Pages/Pages.tsx new file mode 100644 index 0000000..4de121a --- /dev/null +++ b/src/component/Pages/Pages.tsx @@ -0,0 +1,8 @@ +import TaskList from "./Tasks/TaskList.tsx"; +import ShareList from "./Shares/ShareList.tsx"; +import DownloadList from "./Tasks/DownloadList.tsx"; +import Devices from "./Devices/Devices.tsx"; +import Setting from "./Setting/Setting.tsx"; +import Profile from "./Profile/Profile.tsx"; + +export { Setting, TaskList, ShareList, DownloadList, Devices, Profile }; diff --git a/src/component/Pages/Profile/Profile.tsx b/src/component/Pages/Profile/Profile.tsx new file mode 100644 index 0000000..619f5dd --- /dev/null +++ b/src/component/Pages/Profile/Profile.tsx @@ -0,0 +1,183 @@ +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; +import { + Box, + Container, + FormControl, + Grid, + ListItemText, + SelectChangeEvent, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PageHeader from "../PageHeader.tsx"; +import { getUserShares } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import { setSelected } from "../../../redux/fileManagerSlice.ts"; +import { Share } from "../../../api/explorer.ts"; +import { DenseSelect } from "../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import ShareCard from "../Shares/ShareCard.tsx"; +import { useParams } from "react-router-dom"; +import { User } from "../../../api/user.ts"; +import { loadUserInfo } from "../../../redux/thunks/session.ts"; +import { UserProfile } from "../../Common/User/UserPopover.tsx"; +import PageContainer from "../PageContainer.tsx"; + +const defaultPageSize = 50; + +const Profile = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [nextPageToken, setNextPageToken] = useState(""); + const [shares, setShares] = useState([]); + const [loading, setLoading] = useState(false); + const [orderDirection, setOrderDirection] = useState("desc"); + const [user, setUser] = useState(); + + const { id } = useParams<{ id: string }>(); + + useEffect(() => { + if (!id) { + return; + } + dispatch(loadUserInfo(id)).then((u) => { + if (u) { + setUser(u); + } + }); + }, [id]); + + const loadNextPage = useCallback( + (originShares: Share[], token?: string, direction?: string) => () => { + setLoading(true); + dispatch( + getUserShares( + { + page_size: defaultPageSize, + order_direction: direction ?? orderDirection, + next_page_token: token, + }, + id ?? "", + ), + ) + .then((res) => { + setShares([...originShares, ...res.shares]); + if (res.pagination?.next_token) { + setNextPageToken(res.pagination.next_token); + } else { + setNextPageToken(undefined); + } + }) + .catch(() => { + setNextPageToken(undefined); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch, orderDirection, setSelected], + ); + + const refresh = (direction?: string) => { + loadNextPage([], "", direction)(); + }; + + const onShareDeleted = useCallback( + (id: string) => { + setShares((shares) => shares.filter((share) => share.id !== id)); + }, + [setShares], + ); + + const onSelectChange = useCallback( + (e: SelectChangeEvent) => { + setOrderDirection(e.target.value as string); + refresh(e.target.value as string); + }, + [refresh, setOrderDirection], + ); + + return ( + + + + + theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)", + borderRadius: (theme) => `${theme.shape.borderRadius}px`, + mb: 4, + p: 2, + }} + > + {user && } + + + + + + + {t("application:share.createdAtDesc")} + + + + + {t("application:share.createdAtAsc")} + + + + + } + skipChangingDocumentTitle + onRefresh={() => refresh()} + loading={loading} + title={t("application:share.somebodyShare", { + name: user?.nickname ?? "-", + })} + /> + + + {shares.map((share) => ( + + ))} + {nextPageToken != undefined && ( + <> + {[...Array(4)].map((_, i) => ( + + ))} + + )} + + + {nextPageToken == undefined && shares.length == 0 && ( + + + + )} + + + ); +}; + +export default Profile; diff --git a/src/component/Pages/Setting/AvatarCropperDialog.tsx b/src/component/Pages/Setting/AvatarCropperDialog.tsx new file mode 100644 index 0000000..37ee8a6 --- /dev/null +++ b/src/component/Pages/Setting/AvatarCropperDialog.tsx @@ -0,0 +1,306 @@ +import { DialogContent } from "@mui/material"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; +import { useTranslation } from "react-i18next"; +import { DependencyList, useEffect, useRef, useState } from "react"; +import { + centerCrop, + Crop, + makeAspectCrop, + PixelCrop, + ReactCrop, +} from "react-image-crop"; +import "react-image-crop/dist/ReactCrop.css"; +import { useSnackbar } from "notistack"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { sendUploadAvatar } from "../../../api/api.ts"; + +export function useDebounceEffect( + fn: () => void, + waitTime: number, + deps?: DependencyList, +) { + useEffect(() => { + const t = setTimeout(() => { + // @ts-ignore + fn.apply(undefined, deps); + }, waitTime); + + return () => { + clearTimeout(t); + }; + }, deps); +} + +const TO_RADIANS = Math.PI / 180; + +export async function canvasPreview( + image: HTMLImageElement, + canvas: HTMLCanvasElement, + crop: PixelCrop, + scale = 1, + rotate = 0, +) { + const ctx = canvas.getContext("2d"); + + if (!ctx) { + throw new Error("No 2d context"); + } + + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + // devicePixelRatio slightly increases sharpness on retina devices + // at the expense of slightly slower render times and needing to + // size the image back down if you want to download/upload and be + // true to the images natural size. + const pixelRatio = window.devicePixelRatio; + // const pixelRatio = 1 + + canvas.width = Math.floor(crop.width * scaleX * pixelRatio); + canvas.height = Math.floor(crop.height * scaleY * pixelRatio); + + ctx.scale(pixelRatio, pixelRatio); + ctx.imageSmoothingQuality = "high"; + + const cropX = crop.x * scaleX; + const cropY = crop.y * scaleY; + + const rotateRads = rotate * TO_RADIANS; + const centerX = image.naturalWidth / 2; + const centerY = image.naturalHeight / 2; + + ctx.save(); + + // 5) Move the crop origin to the canvas origin (0,0) + ctx.translate(-cropX, -cropY); + // 4) Move the origin to the center of the original position + ctx.translate(centerX, centerY); + // 3) Rotate around the origin + ctx.rotate(rotateRads); + // 2) Scale the image + ctx.scale(scale, scale); + // 1) Move the center of the image to the origin (0,0) + ctx.translate(-centerX, -centerY); + ctx.drawImage( + image, + 0, + 0, + image.naturalWidth, + image.naturalHeight, + 0, + 0, + image.naturalWidth, + image.naturalHeight, + ); + + ctx.restore(); +} + +export interface AvatarCropperDialogProps { + open?: boolean; + onClose: () => void; + file?: File; + onAvatarUpdated: () => void; +} + +function centerAspectCrop( + mediaWidth: number, + mediaHeight: number, + aspect: number, +) { + return centerCrop( + makeAspectCrop( + { + unit: "px", + width: mediaWidth * 0.9, + }, + aspect, + mediaWidth, + mediaHeight, + ), + mediaWidth, + mediaHeight, + ); +} + +const AvatarCropperDialog = ({ + open, + onClose, + onAvatarUpdated, + file, +}: AvatarCropperDialogProps) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const [src, setSrc] = useState(null); + const [crop, setCrop] = useState(undefined); + const [completedCrop, setCompletedCrop] = useState(); + const imageRef = useRef(null); + const previewCanvasRef = useRef(null); + const blobUrlRef = useRef(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (open) { + setCrop(undefined); + setSrc(null); + if (file) { + const reader = new FileReader(); + reader.addEventListener("load", () => setSrc(reader.result as string)); + reader.readAsDataURL(file); + } + } + }, [open]); + + const onImageLoad = (e: React.SyntheticEvent) => { + const { width, height } = e.currentTarget; + setCrop(centerAspectCrop(width, height, 1)); + }; + + const renderImage = async (): Promise => { + const image = imageRef.current; + const previewCanvas = previewCanvasRef.current; + if (!image || !previewCanvas || !completedCrop) { + throw new Error("Crop canvas does not exist"); + } + + // This will size relative to the uploaded image + // size. If you want to size according to what they + // are looking at on screen, remove scaleX + scaleY + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + + const offscreen = new OffscreenCanvas( + completedCrop.width * scaleX, + completedCrop.height * scaleY, + ); + const ctx = offscreen.getContext("2d"); + if (!ctx) { + throw new Error("No 2d context"); + } + + ctx.drawImage( + previewCanvas, + 0, + 0, + previewCanvas.width, + previewCanvas.height, + 0, + 0, + offscreen.width, + offscreen.height, + ); + // You might want { type: "image/jpeg", quality: <0 to 1> } to + // reduce image size + const imgType = "image/png"; + const blob = await offscreen.convertToBlob({ + type: imgType, + }); + + return blob; + }; + + const onSubmit = () => { + setLoading(true); + renderImage() + .then((blob) => { + dispatch(sendUploadAvatar(blob, "image/png")) + .then(() => { + enqueueSnackbar({ + message: t("application:setting.avatarUpdated", {}), + variant: "success", + action: DefaultCloseAction, + }); + onAvatarUpdated(); + onClose(); + }) + .finally(() => { + setLoading(false); + }); + }) + .catch((e) => { + enqueueSnackbar({ + message: e.message, + variant: "error", + action: DefaultCloseAction, + }); + setLoading(false); + }); + }; + + useDebounceEffect( + async () => { + if ( + completedCrop?.width && + completedCrop?.height && + imageRef.current && + previewCanvasRef.current + ) { + console.log(completedCrop); + // We use canvasPreview as it's much faster than imgPreview. + canvasPreview( + imageRef.current, + previewCanvasRef.current, + completedCrop, + 1, + 0, + ); + } + }, + 100, + [completedCrop], + ); + + return ( + + + {src && ( + setCrop(percentCrop)} + minWidth={100} + aspect={1} + onComplete={(p) => setCompletedCrop(p)} + minHeight={100} + > + Avatar + + )} + {!!completedCrop && ( + + )} + + + ); +}; + +export default AvatarCropperDialog; diff --git a/src/component/Pages/Setting/AvatarSetting.tsx b/src/component/Pages/Setting/AvatarSetting.tsx new file mode 100644 index 0000000..ee35c8b --- /dev/null +++ b/src/component/Pages/Setting/AvatarSetting.tsx @@ -0,0 +1,130 @@ +import { User } from "../../../api/user.ts"; +import UserAvatar from "../../Common/User/UserAvatar.tsx"; +import SettingForm from "./SettingForm.tsx"; +import { useTranslation } from "react-i18next"; +import Edit from "../../Icons/Edit.tsx"; +import { SecondaryButton } from "../../Common/StyledComponents.tsx"; +import { ListItemIcon, ListItemText, Menu } from "@mui/material"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import React, { useState } from "react"; +import { + bindMenu, + bindTrigger, + usePopupState, +} from "material-ui-popup-state/hooks"; +import Upload from "../../Icons/Upload.tsx"; +import Fingerprint from "../../Icons/Fingerprint.tsx"; +import AvatarCropperDialog from "./AvatarCropperDialog.tsx"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { sendUploadAvatar } from "../../../api/api.ts"; +import { useSnackbar } from "notistack"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; + +export interface AvatarSettingProps { + user: User; +} + +const AvatarSetting = ({ user }: AvatarSettingProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const popupState = usePopupState({ + popupId: "avatarEdit", + variant: "popover", + }); + const fileInput = React.createRef(); + const { onClose } = bindMenu(popupState); + const [imageCropperOpen, setImageCropperOpen] = useState(false); + const [avtarFile, setAvtarFile] = useState(undefined); + const [avatarGeneration, setAvatarGeneration] = useState(1); + + const onUploadClicked = () => { + onClose(); + fileInput.current?.click(); + }; + + const onImageChange = () => { + const file = fileInput.current?.files?.[0]; + if (!file) { + return; + } + + fileInput.current.value = ""; + setAvtarFile(file); + setImageCropperOpen(true); + }; + + const onAvatarUpdated = () => { + setAvatarGeneration((a) => a + 1); + }; + + const setGravatar = () => { + dispatch(sendUploadAvatar()).then(() => { + enqueueSnackbar({ + message: t("application:setting.avatarUpdated", {}), + variant: "success", + action: DefaultCloseAction, + }); + onAvatarUpdated(); + }); + onClose(); + }; + + return ( + + + } + {...bindTrigger(popupState)} + > + {t("fileManager.edit")} + + setImageCropperOpen(false)} + open={imageCropperOpen} + /> + + + + + + + {t(`setting.uploadImage`)} + + + + + + {t(`setting.useGravatar`)} + + + + ); +}; + +export default AvatarSetting; diff --git a/src/component/Pages/Setting/PreferenceSetting.tsx b/src/component/Pages/Setting/PreferenceSetting.tsx new file mode 100644 index 0000000..a38bb30 --- /dev/null +++ b/src/component/Pages/Setting/PreferenceSetting.tsx @@ -0,0 +1,283 @@ +import { LoadingButton } from "@mui/lab"; +import { + Box, + Checkbox, + Chip, + Collapse, + FormControl, + FormHelperText, + ListItemText, + Stack, + styled, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import i18next from "i18next"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendUpdateUserSetting } from "../../../api/api.ts"; +import { UserSettings as UserSettingsType } from "../../../api/user.ts"; +import { languages } from "../../../i18n.ts"; +import { setPreferredTheme } from "../../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { selectLanguage } from "../../../redux/thunks/settings.ts"; +import SessionManager, { UserSettings } from "../../../session"; +import { refreshTimeZone, timeZone } from "../../../util/datetime.ts"; +import { + DenseAutocomplete, + DenseFilledTextField, + DenseSelect, + SmallFormControlLabel, +} from "../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import { ColorCircle, SelectorBox } from "../../FileManager/FileInfo/ColorCircle/CircleColorSelector.tsx"; +import { SwitchPopover } from "../../Frame/NavBar/DarkThemeSwitcher.tsx"; +import SettingForm from "./SettingForm.tsx"; + +export interface PreferenceSettingProps { + setting: UserSettingsType; + setSetting: (setting: UserSettingsType) => void; +} + +declare namespace Intl { + type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit"; + + function supportedValuesOf(input: Key): string[]; +} + +interface ThemeOption { + [key: string]: any; +} + +const OutlinedSettingBox = styled(Box)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1), + border: `1px solid ${theme.palette.mode === "light" ? "rgba(0, 0, 0, 0.23)" : "rgba(255, 255, 255, 0.23)"}`, +})); + +const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { themes } = useAppSelector((s) => s.siteConfig.basic.config); + + const [timeZoneValue, setTimeZoneValue] = useState(timeZone); + const versionMaxRef = useRef(null); + const [loading, setLoading] = useState(false); + const preferredTheme = useAppSelector((state) => state.globalState.preferredTheme); + const defaultTheme = useAppSelector((state) => state.siteConfig.basic.config.default_theme); + const [versionRetentionEnabled, setVersionRetentionEnabled] = useState(setting.version_retention_enabled); + const [versionRetentionMax, setVersionRetentionMax] = useState(setting.version_retention_max); + const [versionRetentionExts, setVersionRetentionExts] = useState(setting.version_retention_ext); + const [showSaveButton, setShowSaveButton] = useState(false); + + const onRetentionCheckChange = (e: React.ChangeEvent) => { + setVersionRetentionEnabled(e.target.checked); + setShowSaveButton(true); + }; + + const onVersionRetentionMaxChange = (e: React.ChangeEvent) => { + setVersionRetentionMax(parseInt(e.target.value) ?? 0); + setShowSaveButton(true); + }; + + const newExtAdded = (_e: any, newValue: string[]) => { + // Remove start dots in each value if presented + setVersionRetentionExts(newValue.map((v) => v.replace(/^\./, ""))); + setShowSaveButton(true); + }; + + useEffect(() => { + setVersionRetentionEnabled(setting.version_retention_enabled); + setVersionRetentionMax(setting.version_retention_max); + setVersionRetentionExts(setting.version_retention_ext); + }, [setting]); + + const selectTimeZone = (value: string) => { + setTimeZoneValue(value); + SessionManager.set(UserSettings.TimeZone, value); + refreshTimeZone(); + }; + + const themeOptions: ThemeOption = useMemo(() => { + if (themes) { + return JSON.parse(themes); + } + + return {}; + }, [themes]); + + const applyTheme = (color: string) => { + dispatch(setPreferredTheme(color)); + dispatch( + sendUpdateUserSetting({ + preferred_theme: color, + }), + ); + }; + + const saveVersionRetention = () => { + setLoading(true); + dispatch( + sendUpdateUserSetting({ + version_retention_enabled: versionRetentionEnabled, + version_retention_max: versionRetentionMax, + version_retention_ext: versionRetentionExts, + }), + ) + .then(() => { + setShowSaveButton(false); + setSetting({ + ...setting, + version_retention_enabled: versionRetentionEnabled, + version_retention_max: versionRetentionMax, + version_retention_ext: versionRetentionExts, + }); + }) + .finally(() => { + setLoading(false); + }); + }; + + return ( + + + + dispatch(selectLanguage(e.target.value as string))}> + {languages.map((l) => ( + + + {l.displayName} + + + ))} + + {t("setting.languageDes")} + + + + + selectTimeZone(e.target.value as string)}> + {Intl.supportedValuesOf("timeZone").map((v) => ( + + + {v} + + + ))} + + {t("setting.timezoneDes")} + + + + + + + + {Object.keys(themeOptions).map((color, index) => ( + applyTheme(color)} + selected={(preferredTheme && preferredTheme == color) || (!preferredTheme && defaultTheme == color)} + /> + ))} + + + + + + } + label={t("application:setting.enableVersionRetention")} + /> + + {t("application:setting.enableVersionRetentionDes")} + + + + + + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ; + }) + } + renderInput={(params) => ( + + )} + /> + + + + + + + + {t("fileManager.save")} + + + + + + + ); +}; + +export default PreferenceSetting; diff --git a/src/component/Pages/Setting/ProfileSetting.tsx b/src/component/Pages/Setting/ProfileSetting.tsx new file mode 100644 index 0000000..013a0c8 --- /dev/null +++ b/src/component/Pages/Setting/ProfileSetting.tsx @@ -0,0 +1,109 @@ +import { LoadingButton } from "@mui/lab"; +import { Collapse, Grid2, Stack, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendUpdateUserSetting } from "../../../api/api.ts"; +import { UserSettings } from "../../../api/user.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import SessionManager from "../../../session"; +import { DenseFilledTextField } from "../../Common/StyledComponents.tsx"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import AvatarSetting from "./AvatarSetting.tsx"; +import SettingForm from "./SettingForm.tsx"; + +export interface ProfileSettingProps { + setting: UserSettings; + setSetting: (setting: UserSettings) => void; +} + +const ProfileSetting = ({ setting, setSetting }: ProfileSettingProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const nickRef = useRef(null); + + const user = SessionManager.currentLoginOrNull(); + const [nick, setNick] = useState(user?.user.nickname); + const [nickLoading, setNickLoading] = useState(false); + + const onClick = () => { + // Validate input length + if (nickRef.current && nickRef.current.checkValidity()) { + setNickLoading(true); + dispatch(sendUpdateUserSetting({ nick })) + .then(() => { + if (user?.user) { + SessionManager.updateUserIfExist({ + ...user?.user, + nickname: nick ?? "", + }); + } + }) + .finally(() => { + setNickLoading(false); + }); + } else { + // Input is invalid, show validation errors + nickRef.current?.reportValidity(); + } + }; + + return ( + + + + {user && } + + + + + + + + setNick(e.target.value)} + value={nick} + required + inputProps={{ maxLength: 255 }} + helperText={t("setting.nickNameDes")} + inputRef={nickRef} + /> + + + {t("fileManager.save")} + + + + + + + {user?.user.id} + + + + + + + + + + + + {user?.user.group?.name} + + + + + + + ); +}; + +export default ProfileSetting; diff --git a/src/component/Pages/Setting/Security/Disable2FADialog.tsx b/src/component/Pages/Setting/Security/Disable2FADialog.tsx new file mode 100644 index 0000000..51216af --- /dev/null +++ b/src/component/Pages/Setting/Security/Disable2FADialog.tsx @@ -0,0 +1,138 @@ +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { useTranslation } from "react-i18next"; +import { useSnackbar } from "notistack"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import React, { useEffect, useState } from "react"; +import { + Box, + DialogContent, + FormControl, + Stack, + styled, + Typography, +} from "@mui/material"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import { sendUpdateUserSetting } from "../../../../api/api.ts"; +import { MuiOtpInput } from "mui-one-time-password-input"; + +export interface Disable2FADialogProps { + open?: boolean; + onClose: () => void; + on2FADisabled: () => void; +} + +const MuiOtpInputStyled = styled(MuiOtpInput)` + display: flex; + gap: 8px; + max-width: 650px; + margin-inline: auto; +`; + +const Disable2FADialog = ({ + open, + onClose, + on2FADisabled, +}: Disable2FADialogProps) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + + const [loading, setLoading] = useState(false); + const [code, setCode] = useState(""); + + useEffect(() => { + if (open) { + setLoading(false); + setCode(""); + } + }, [open]); + + useEffect(() => { + if (code.length === 6) { + setLoading(true); + dispatch( + sendUpdateUserSetting({ + two_fa_enabled: false, + two_fa_code: code, + }), + ) + .then(() => { + enqueueSnackbar({ + message: t("setting.settingSaved"), + variant: "success", + }); + on2FADisabled(); + onClose(); + }) + .catch(() => { + setLoading(false); + setCode(""); + }); + } + }, [code]); + + return ( + + + + + + node.addEventListener("transitionend", done, false) + } + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + + {t("setting.inputCurrent2FACode")} + + + + + + )} + + + + + + + ); +}; + +export default Disable2FADialog; diff --git a/src/component/Pages/Setting/Security/Enable2FADialog.tsx b/src/component/Pages/Setting/Security/Enable2FADialog.tsx new file mode 100644 index 0000000..51ad811 --- /dev/null +++ b/src/component/Pages/Setting/Security/Enable2FADialog.tsx @@ -0,0 +1,176 @@ +import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx"; +import { useTranslation } from "react-i18next"; +import { useSnackbar } from "notistack"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import React, { useEffect, useState } from "react"; +import { + Box, + DialogContent, + FormControl, + Stack, + styled, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import AutoHeight from "../../../Common/AutoHeight.tsx"; +import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; +import { + get2FAInitSecret, + sendUpdateUserSetting, +} from "../../../../api/api.ts"; +import { QRCodeSVG } from "qrcode.react"; +import SessionManager from "../../../../session"; +import { MuiOtpInput } from "mui-one-time-password-input"; + +export interface Enable2FADialogProps { + open?: boolean; + onClose: () => void; + on2FAEnabled: () => void; +} + +const MuiOtpInputStyled = styled(MuiOtpInput)` + display: flex; + gap: 8px; + max-width: 650px; + margin-inline: auto; +`; + +const Enable2FADialog = ({ + open, + onClose, + on2FAEnabled, +}: Enable2FADialogProps) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const siteTitle = useAppSelector( + (state) => state.siteConfig.basic.config.title, + ); + const user = SessionManager.currentLoginOrNull(); + + const [loading, setLoading] = useState(false); + const [code, setCode] = useState(""); + const [secret, setSecret] = useState(""); + + useEffect(() => { + if (open) { + setLoading(true); + setCode(""); + dispatch(get2FAInitSecret()) + .then((res) => { + setSecret(res); + }) + .finally(() => { + setLoading(false); + }); + } + }, [open]); + + useEffect(() => { + if (code.length === 6) { + setLoading(true); + dispatch( + sendUpdateUserSetting({ + two_fa_enabled: true, + two_fa_code: code, + }), + ) + .then(() => { + enqueueSnackbar({ + message: t("setting.settingSaved"), + variant: "success", + }); + on2FAEnabled(); + onClose(); + }) + .catch(() => { + setLoading(false); + setCode(""); + }); + } + }, [code]); + + return ( + + + + + + node.addEventListener("transitionend", done, false) + } + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && ( + + + + + + + {t("setting.2faDescription")} + + + {t("setting.inputCurrent2FACode")} + + + + + + + )} + + + + + + + ); +}; + +export default Enable2FADialog; diff --git a/src/component/Pages/Setting/Security/PasskeyList.tsx b/src/component/Pages/Setting/Security/PasskeyList.tsx new file mode 100644 index 0000000..470e944 --- /dev/null +++ b/src/component/Pages/Setting/Security/PasskeyList.tsx @@ -0,0 +1,196 @@ +import { Box, IconButton, List, Typography, useMediaQuery, useTheme } from "@mui/material"; +import dayjs from "dayjs"; +import { useSnackbar } from "notistack"; +import { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { + sendDeletePasskey, + sendFinishPasskeyRegistration, + sendPreparePasskeyRegistration, +} from "../../../../api/api.ts"; +import { Passkey, UserSettings } from "../../../../api/user.ts"; +import { useAppDispatch } from "../../../../redux/hooks.ts"; +import { confirmOperation } from "../../../../redux/thunks/dialog.ts"; +import { bufferEncode, urlBase64BufferDecode } from "../../../../util"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { NoWrapTypography, SecondaryLoadingButton } from "../../../Common/StyledComponents.tsx"; +import TimeBadge from "../../../Common/TimeBadge.tsx"; +import Dismiss from "../../../Icons/Dismiss.tsx"; +import Fingerprint from "../../../Icons/Fingerprint.tsx"; +import ShieldAdd from "../../../Icons/ShieldAdd.tsx"; +import SettingListItem from "../SettingListItem.tsx"; + +export interface PasskeyProps { + setting: UserSettings; + onPasskeyAdded: (passkey: Passkey) => void; + onPasskeyDeleted: (passkeyID: string) => void; +} + +const PasskeyList = (props: PasskeyProps) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + const [loading, setLoading] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + const addPasskey = async () => { + if (!navigator.credentials || !window.PublicKeyCredential) { + enqueueSnackbar({ + message: t("setting.browserNotSupported"), + variant: "warning", + action: DefaultCloseAction, + }); + + return; + } + + setLoading(true); + try { + const opts = await dispatch(sendPreparePasskeyRegistration()); + const credential = await navigator.credentials.create({ + publicKey: { + rp: opts.publicKey.rp, + user: { + id: urlBase64BufferDecode(opts.publicKey.user.id), + name: opts.publicKey.user.name, + displayName: opts.publicKey.user.displayName, + }, + + // url decode base64 + challenge: urlBase64BufferDecode(opts.publicKey.challenge), + pubKeyCredParams: opts.publicKey.pubKeyCredParams, + timeout: opts.publicKey.timeout, + excludeCredentials: opts.publicKey.excludeCredentials?.map((c) => ({ + id: urlBase64BufferDecode(c.id), + type: c.type, + })), + authenticatorSelection: opts.publicKey.authenticatorSelection, + }, + }); + if (credential) { + const c = credential as PublicKeyCredential; + const newPasskey = await dispatch( + sendFinishPasskeyRegistration({ + response: JSON.stringify({ + id: credential.id, + type: credential.type, + rawId: bufferEncode(c.rawId), + response: { + // @ts-ignore + attestationObject: bufferEncode(c.response.attestationObject), + clientDataJSON: bufferEncode(c.response.clientDataJSON), + }, + }), + name: t("setting.passkeyName"), + ua: navigator.userAgent, + }), + ); + + props.onPasskeyAdded(newPasskey); + } + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const deletePasskey = (passkeyID: string) => { + dispatch(confirmOperation(t("setting.removedAuthenticatorConfirm"))).then(() => { + setDeleteLoading(true); + dispatch(sendDeletePasskey(passkeyID)) + .then(() => { + props.onPasskeyDeleted(passkeyID); + }) + .finally(() => { + setDeleteLoading(false); + }); + }); + }; + return ( + + {!props.setting.passkeys?.length && ( + `${theme.shape.borderRadius}px`, + border: (theme) => + `1px solid ${theme.palette.mode === "light" ? "rgba(0, 0, 0, 0.23)" : "rgba(255, 255, 255, 0.23)"}`, + textAlign: "center", + p: 1, + py: 2, + }} + > + theme.palette.text.secondary, + }} + /> + + {t("setting.noAuthenticator")} + + + )} + + {props.setting.passkeys?.map((passkey) => ( + deletePasskey(passkey.id)} disabled={deleteLoading}> + + + } + settingTitle={{passkey.name}} + settingDescription={ + + ]} + /> + + passkey.used_at && dayjs().diff(dayjs(passkey.used_at), "day") < 7 + ? theme.palette.success.main + : theme.palette.text.secondary, + }} + > + {passkey.used_at ? ( + ]} + /> + ) : ( + t("setting.neverUsed") + )} + + + } + /> + ))} + + } + > + {t("setting.addNewAuthenticator")} + + + ); +}; + +export default PasskeyList; diff --git a/src/component/Pages/Setting/Security/SecuritySetting.tsx b/src/component/Pages/Setting/Security/SecuritySetting.tsx new file mode 100644 index 0000000..738f8b6 --- /dev/null +++ b/src/component/Pages/Setting/Security/SecuritySetting.tsx @@ -0,0 +1,212 @@ +import { LoadingButton } from "@mui/lab"; +import { Box, Collapse, Stack, useMediaQuery, useTheme } from "@mui/material"; +import { useSnackbar } from "notistack"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { sendUpdateUserSetting } from "../../../../api/api.ts"; +import { Passkey, UserSettings } from "../../../../api/user.ts"; +import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts"; +import SessionManager from "../../../../session"; +import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx"; +import { DenseFilledTextField, SecondaryButton, SquareChip } from "../../../Common/StyledComponents.tsx"; +import Edit from "../../../Icons/Edit.tsx"; +import Open from "../../../Icons/Open.tsx"; +import { ProfileSettingProps } from "../ProfileSetting.tsx"; +import SettingForm from "../SettingForm.tsx"; +import Disable2FADialog from "./Disable2FADialog.tsx"; +import Enable2FADialog from "./Enable2FADialog.tsx"; +import PasskeyList from "./PasskeyList.tsx"; + +export interface SecuritySettingProps { + setting: UserSettings; + setSetting: (setting: UserSettings) => void; +} + +const SecuritySetting = ({ setting, setSetting }: ProfileSettingProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const { enqueueSnackbar } = useSnackbar(); + const navigate = useNavigate(); + + const authEnabled = useAppSelector((s) => s.siteConfig.login.config.authn); + + const resetPwdFormRef = React.createRef(); + const [showResetPassword, setShowResetPassword] = useState(false); + const [resetLoading, setResetLoading] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [repeatPassword, setRepeatPassword] = useState(""); + const [enable2FAOpen, setEnable2FAOpen] = useState(false); + const [disable2FAOpen, setDisable2FAOpen] = useState(false); + + const submitResetPassword = () => { + if (!resetPwdFormRef.current) { + return; + } + if (!resetPwdFormRef.current.checkValidity()) { + resetPwdFormRef.current.reportValidity(); + return; + } + + if (newPassword !== repeatPassword) { + enqueueSnackbar({ + variant: "warning", + message: t("login.passwordNotMatch"), + action: DefaultCloseAction, + }); + return; + } + + setResetLoading(true); + dispatch( + sendUpdateUserSetting({ + current_password: currentPassword, + new_password: newPassword, + }), + ) + .then(() => { + SessionManager.signOutCurrent(); + navigate("/session"); + setShowResetPassword(false); + enqueueSnackbar({ + variant: "success", + message: t("login.passwordReset"), + action: DefaultCloseAction, + }); + }) + .finally(() => { + setResetLoading(false); + }); + }; + + const on2FAChange = (enabled: boolean) => () => { + setSetting({ + ...setting, + two_fa_enabled: enabled, + }); + }; + + const onPasskeyAdded = (passkey: Passkey) => { + setSetting({ + ...setting, + passkeys: setting.passkeys ? [...setting.passkeys, passkey] : [passkey], + }); + }; + + const onPasskeyDeleted = (passkeyID: string) => { + setSetting({ + ...setting, + passkeys: setting.passkeys?.filter((p) => p.id !== passkeyID), + }); + }; + + return ( + + + + setShowResetPassword(true)} + variant={"contained"} + startIcon={} + > + {t("login.resetPassword")} + + + +
+ + setCurrentPassword(e.target.value)} + inputProps={{ + type: "password", + minLength: 4, + maxLength: 64, + }} + /> + setNewPassword(e.target.value)} + inputProps={{ + type: "password", + minLength: 6, + maxLength: 64, + }} + /> + setRepeatPassword(e.target.value)} + inputProps={{ + type: "password", + minLength: 6, + maxLength: 64, + }} + /> + + + {t("fileManager.save")} + + + +
+
+
+ + {t("setting.2fa")} + {setting.two_fa_enabled && ( + theme.typography.caption.fontSize, + }} + color={"success"} + size={"small"} + variant={"outlined"} + label={t("setting.enabled")} + /> + )} +
+ } + lgWidth={5} + > + (setting.two_fa_enabled ? setDisable2FAOpen(true) : setEnable2FAOpen(true))} + variant={"contained"} + startIcon={} + > + {t(`setting.${setting.two_fa_enabled ? "disable" : "enable"}2FA`)} + + + {authEnabled && ( + + + + )} + setEnable2FAOpen(false)} on2FAEnabled={on2FAChange(true)} /> + setDisable2FAOpen(false)} + on2FADisabled={on2FAChange(false)} + /> + + ); +}; + +export default SecuritySetting; diff --git a/src/component/Pages/Setting/Setting.tsx b/src/component/Pages/Setting/Setting.tsx new file mode 100644 index 0000000..d387073 --- /dev/null +++ b/src/component/Pages/Setting/Setting.tsx @@ -0,0 +1,120 @@ +import { PersonOutline } from "@mui/icons-material"; +import { Box, Container } from "@mui/material"; +import { useQueryState } from "nuqs"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CSSTransition, SwitchTransition } from "react-transition-group"; +import { getUserSettings } from "../../../api/api.ts"; +import { UserSettings } from "../../../api/user.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import ResponsiveTabs, { Tab } from "../../Common/ResponsiveTabs.tsx"; +import EditSetting from "../../Icons/EditSetting.tsx"; +import LockClosedOutlined from "../../Icons/LockClosedOutlined.tsx"; +import PageContainer from "../PageContainer.tsx"; +import PageHeader, { PageTabQuery } from "../PageHeader.tsx"; +import PreferenceSetting from "./PreferenceSetting.tsx"; +import ProfileSetting from "./ProfileSetting.tsx"; +import SecuritySetting from "./Security/SecuritySetting.tsx"; + +export enum SettingPageTab { + Profile = "profile", + Preference = "preference", + Security = "security", +} + +const Setting = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [tab, setTab] = useQueryState(PageTabQuery); + const [loading, setLoading] = useState(true); + const [setting, setSetting] = useState(undefined); + + useEffect(() => { + setLoading(true); + dispatch(loadSiteConfig("login")); + dispatch(getUserSettings()) + .then((res) => { + setSetting(res); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const tabs: Tab[] = useMemo(() => { + const res = []; + res.push( + ...[ + { + label: t("application:setting.profile"), + value: SettingPageTab.Profile, + icon: , + }, + { + label: t("application:setting.preference"), + value: SettingPageTab.Preference, + icon: , + }, + { + label: t("application:setting.security"), + value: SettingPageTab.Security, + icon: , + }, + ], + ); + return res; + }, [t]); + + return ( + + + + setTab(newValue)} + tabs={tabs} + /> + + node.addEventListener("transitionend", done, false)} + classNames="fade" + key={`${loading}`} + > + + {loading && ( + + + + )} + {!loading && setting && ( + + {(!tab || tab == SettingPageTab.Profile) && ( + + )} + {tab == SettingPageTab.Preference && } + {tab == SettingPageTab.Security && } + + )} + + + + + + ); +}; + +export default Setting; diff --git a/src/component/Pages/Setting/SettingForm.tsx b/src/component/Pages/Setting/SettingForm.tsx new file mode 100644 index 0000000..29106fe --- /dev/null +++ b/src/component/Pages/Setting/SettingForm.tsx @@ -0,0 +1,94 @@ +import { Chip, Grid2, Typography, styled } from "@mui/material"; +import { useCallback, useEffect, useState } from "react"; +import ProDialog from "../../Admin/Common/ProDialog"; + +export const ProChip = styled(Chip)(({ theme }) => ({ + marginLeft: 8, + height: "20px", + fontSize: "12px", + background: `linear-gradient(45deg, ${theme.palette.primary.main} 30%, ${theme.palette.primary.light} 90%)`, + color: theme.palette.primary.contrastText, +})); + +export interface SettingFormProps { + title?: React.ReactNode; + children: React.ReactNode; + lgWidth?: number; + secondary?: React.ReactNode; + spacing?: number; + anchorId?: string; + noContainer?: boolean; + pro?: boolean; +} + +const SettingForm = ({ + title, + children, + lgWidth = 8, + secondary, + spacing, + noContainer, + anchorId, + pro, +}: SettingFormProps) => { + const [proOpen, setProOpen] = useState(false); + useEffect(() => { + if (anchorId && window.location.hash === `#${anchorId}`) { + const anchor = document.getElementById(`anchor-${anchorId}`); + if (anchor) { + anchor.scrollIntoView({ behavior: "smooth" }); + // clear hash, not query + window.history.replaceState({}, "", window.location.pathname + window.location.search); + } + } + }, [anchorId]); + + const handleProClick = useCallback( + (e: React.MouseEvent) => { + if (pro) { + e.stopPropagation(); + setProOpen(true); + } + }, + [pro], + ); + + const inner = ( + <> + + {title && ( + + {title} + {pro && } + + )} +
{children}
+
+ {secondary && secondary} + {pro && setProOpen(false)} />} + + ); + if (noContainer) { + return inner; + } + return ( + + {inner} + + ); +}; + +export default SettingForm; diff --git a/src/component/Pages/Setting/SettingListItem.tsx b/src/component/Pages/Setting/SettingListItem.tsx new file mode 100644 index 0000000..2d6c59f --- /dev/null +++ b/src/component/Pages/Setting/SettingListItem.tsx @@ -0,0 +1,68 @@ +import { + Avatar, + ListItem, + ListItemAvatar, + ListItemProps, + ListItemSecondaryAction, + styled, + SvgIconProps, +} from "@mui/material"; +import { StyledListItemText } from "../../Common/StyledComponents.tsx"; +import * as React from "react"; + +const StyledListItem = styled(ListItem)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + border: `1px solid ${ + theme.palette.mode === "light" + ? "rgba(0, 0, 0, 0.23)" + : "rgba(255, 255, 255, 0.23)" + }`, + marginTop: theme.spacing(1), + paddingBottom: theme.spacing(0.5), + paddingTop: theme.spacing(0.5), +})); + +export interface SettingListItemProps extends ListItemProps { + icon: (props: SvgIconProps) => JSX.Element; + iconColor?: string; + settingTitle?: React.ReactNode; + settingDescription?: React.ReactNode; + settingAction?: React.ReactNode; +} + +const SettingListItem = ({ + icon, + iconColor, + settingTitle, + settingDescription, + settingAction, + ...rest +}: SettingListItemProps) => { + const Icon = icon; + return ( + + + + + + + + {settingAction && ( + {settingAction} + )} + + ); +}; + +export default SettingListItem; diff --git a/src/component/Pages/Setting/StorageSetting.tsx b/src/component/Pages/Setting/StorageSetting.tsx new file mode 100644 index 0000000..505a4fd --- /dev/null +++ b/src/component/Pages/Setting/StorageSetting.tsx @@ -0,0 +1,128 @@ +import { Box, Stack, styled, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Capacity, UserSettings } from "../../../api/user.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { updateUserCapacity } from "../../../redux/thunks/filemanager.ts"; +import { loadSiteConfig } from "../../../redux/thunks/site.ts"; +import { sizeToString } from "../../../util"; +import SettingForm from "./SettingForm.tsx"; + +export const StorageBar = styled(Box)(({ theme }) => ({ + height: "10px", + borderRadius: `${theme.shape.borderRadius}px`, + width: "100%", + backgroundColor: theme.palette.grey[theme.palette.mode === "light" ? 200 : 800], + overflow: "hidden", +})); + +export const StoragePart = styled(Box)(() => ({ + transition: "width .6s ease", + height: "100%", + fontSize: "12px", + lineHeight: "20px", + float: "left", +})); + +export const StorageBlock = styled(Box)(() => ({ + display: "inline-block", + width: "10px", + height: "10px", + borderRadius: "50%", + marginRight: "5px", +})); + +export interface StorageSettingProps { + setting: UserSettings; +} + +export const CapacityBar = ({ capacity, forceRow }: { capacity?: Capacity; forceRow?: boolean }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")) || forceRow; + const { t } = useTranslation(); + const [storageBreakdown, setStorageBreakdown] = useState({ + used: 0, + base: 0, + }); + useEffect(() => { + let summary = { + used: 0, + base: 0, + }; + + if (!capacity) { + return; + } + + summary.used = capacity.used / capacity.total; + summary.base = 1; + setStorageBreakdown(summary); + }, [capacity]); + + return ( + <> + + theme.palette.warning.light, + width: `${storageBreakdown.used * 100}%`, + }} + /> + + + + theme.palette.warning.light, + }} + /> + {t("vas.used", { + size: sizeToString(capacity?.used ?? 0), + })} + + + theme.palette.grey[theme.palette.mode === "light" ? 200 : 800], + }} + /> + {t("vas.total", { + size: sizeToString(capacity?.total ?? 0), + })} + + + + ); +}; + +const StorageSetting = ({ setting }: StorageSettingProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const navigate = useNavigate(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const capacity = useAppSelector((state) => state.fileManager[0].capacity); + + const [storageBreakdown, setStorageBreakdown] = useState({ + used: 0, + base: 0, + }); + + useEffect(() => { + dispatch(updateUserCapacity(0)); + dispatch(loadSiteConfig("vas")); + }, []); + + return ( + + + + + + + + ); +}; + +export default StorageSetting; diff --git a/src/component/Pages/Shares/ShareCard.tsx b/src/component/Pages/Shares/ShareCard.tsx new file mode 100644 index 0000000..bee64b9 --- /dev/null +++ b/src/component/Pages/Shares/ShareCard.tsx @@ -0,0 +1,267 @@ +import { + Box, + Chip, + Grid, + ListItemIcon, + ListItemText, + Menu, + MenuProps, + Skeleton, + Tooltip, + Typography, + useTheme, +} from "@mui/material"; +import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useInView } from "react-intersection-observer"; +import { useNavigate } from "react-router-dom"; +import { sendDeleteShare } from "../../../api/api.ts"; +import { FileType, Share } from "../../../api/explorer.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { confirmOperation } from "../../../redux/thunks/dialog.ts"; +import { openShareEditByID } from "../../../redux/thunks/share.ts"; +import SessionManager from "../../../session"; +import { DefaultCloseAction } from "../../Common/Snackbar/snackbar.tsx"; +import { NoWrapBox, NoWrapTypography } from "../../Common/StyledComponents.tsx"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import { DenseDivider, SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx"; +import Clipboard from "../../Icons/Clipboard.tsx"; +import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; +import Eye from "../../Icons/Eye.tsx"; +import LinkEdit from "../../Icons/LinkEdit.tsx"; +import Open from "../../Icons/Open.tsx"; +import { SummaryButton } from "../Tasks/TaskCard.tsx"; + +export interface ShareCardProps { + share?: Share; + onLoad?: () => void; + loading?: boolean; + onShareDeleted: (id: string) => void; +} + +interface ActionMenuProps extends MenuProps { + share: Share; + onShareDeleted: (id: string) => void; +} + +const ActionMenu = ({ share, onShareDeleted, onClose, ...rest }: ActionMenuProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const deleteShare = useCallback(() => { + dispatch(confirmOperation(t("fileManager.deleteShareWarning"))).then(() => { + dispatch(sendDeleteShare(share.id)).then(() => { + onClose && onClose({}, "backdropClick"); + enqueueSnackbar({ + message: t("application:share.shareCanceled"), + variant: "success", + action: DefaultCloseAction, + }); + onShareDeleted(share.id); + }); + }); + }, [t, share.id, onClose, dispatch, enqueueSnackbar]); + + const openEdit = useCallback(() => { + dispatch(openShareEditByID(share.id, share.password)); + onClose && onClose({}, "backdropClick"); + }, [dispatch, share, onClose]); + + const openLink = useCallback(() => { + window.open(share.url, "_blank"); + onClose && onClose({}, "backdropClick"); + }, [share, onClose]); + + const copyLink = useCallback(() => { + navigator.clipboard.writeText(share.url); + enqueueSnackbar({ + message: t("modals.linkCopied"), + variant: "success", + action: DefaultCloseAction, + }); + onClose && onClose({}, "backdropClick"); + }, [share, onClose, enqueueSnackbar, t]); + + return ( + + + + + + {t(`fileManager.open`)} + + + + + + {t(`share.copyLinkToClipboard`)} + + {!share.expired && ( + + + + + {t(`fileManager.${share?.expired ? "editAndReactivate" : "edit"}`)} + + )} + + + + + + {t(`fileManager.delete`)} + + + ); +}; + +const ShareCard = ({ share, onShareDeleted, onLoad, loading }: ShareCardProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { ref, inView } = useInView({ + rootMargin: "200px 0px", + triggerOnce: true, + skip: !loading || !onLoad, + }); + + const popupState = usePopupState({ + popupId: "shareAction", + variant: "popover", + }); + + useEffect(() => { + if (!inView) { + return; + } + + if (onLoad) { + onLoad(); + } + }, [inView]); + + const user = SessionManager.currentLoginOrNull(); + + return ( + <> + {share && } + + { + e.preventDefault(); + if (share?.owner?.id === user?.user.id) { + popupState.open(e); + } + }} + sx={{ p: 0, minHeight: 0, width: "100%", textAlign: "left" }} + {...(share?.owner?.id != user?.user.id + ? { + onClick: () => { + window.open(share?.url ?? "#", "_blank"); + }, + } + : bindTrigger(popupState))} + > + + + {share ? ( + + ) : ( + + )} + + + + + + + {loading ? : share?.name} + + + + {share?.expired && ( + + )} + + + + + {!share?.created_at ? ( + + ) : ( + + + + + {share?.visited ?? 0} + + + )} + + + + + + + + + ); +}; + +export default ShareCard; diff --git a/src/component/Pages/Shares/ShareList.tsx b/src/component/Pages/Shares/ShareList.tsx new file mode 100644 index 0000000..9279462 --- /dev/null +++ b/src/component/Pages/Shares/ShareList.tsx @@ -0,0 +1,147 @@ +import * as React from "react"; +import { useCallback, useState } from "react"; +import { + Box, + Container, + FormControl, + Grid, + ListItemText, + SelectChangeEvent, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PageHeader from "../PageHeader.tsx"; +import { getShares } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import { setSelected } from "../../../redux/fileManagerSlice.ts"; +import ShareCard from "./ShareCard.tsx"; +import { Share } from "../../../api/explorer.ts"; +import { DenseSelect } from "../../Common/StyledComponents.tsx"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import PageContainer from "../PageContainer.tsx"; + +const defaultPageSize = 50; + +const ShareList = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [nextPageToken, setNextPageToken] = useState(""); + const [shares, setShares] = useState([]); + const [loading, setLoading] = useState(false); + const [orderDirection, setOrderDirection] = useState("desc"); + + const loadNextPage = useCallback( + (originShares: Share[], token?: string, direction?: string) => () => { + setLoading(true); + dispatch( + getShares({ + page_size: defaultPageSize, + order_direction: direction ?? orderDirection, + next_page_token: token, + }), + ) + .then((res) => { + setShares([...originShares, ...res.shares]); + if (res.pagination?.next_token) { + setNextPageToken(res.pagination.next_token); + } else { + setNextPageToken(undefined); + } + }) + .catch(() => { + setNextPageToken(undefined); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch, orderDirection, setSelected], + ); + + const refresh = (direction?: string) => { + loadNextPage([], "", direction)(); + }; + + const onShareDeleted = useCallback( + (id: string) => { + setShares((shares) => shares.filter((share) => share.id !== id)); + }, + [setShares], + ); + + const onSelectChange = useCallback( + (e: SelectChangeEvent) => { + setOrderDirection(e.target.value as string); + refresh(e.target.value as string); + }, + [refresh, setOrderDirection], + ); + + return ( + + + + + + + {t("application:share.createdAtDesc")} + + + + + {t("application:share.createdAtAsc")} + + + + + } + onRefresh={() => refresh()} + loading={loading} + title={t("application:navbar.myShare")} + /> + + + {shares.map((share) => ( + + ))} + {nextPageToken != undefined && ( + <> + {[...Array(4)].map((_, i) => ( + + ))} + + )} + + + {nextPageToken == undefined && shares.length == 0 && ( + + + + )} + + + ); +}; + +export default ShareList; diff --git a/src/component/Pages/Tasks/DownloadFileList.tsx b/src/component/Pages/Tasks/DownloadFileList.tsx new file mode 100644 index 0000000..5da3478 --- /dev/null +++ b/src/component/Pages/Tasks/DownloadFileList.tsx @@ -0,0 +1,213 @@ +import { + Box, + Button, + Grow, + Table, + TableCell, + TableContainer, + TableRow, + Tooltip, + Typography, + useTheme, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TableVirtuoso } from "react-virtuoso"; +import { sendCancelDownloadTask, sendSetDownloadTarget } from "../../../api/api.ts"; +import { FileType } from "../../../api/explorer.ts"; +import { DownloadTaskFile, TaskSummary } from "../../../api/workflow.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { confirmOperation } from "../../../redux/thunks/dialog.ts"; +import { fileBase, sizeToString } from "../../../util"; +import { + NoWrapTableCell, + NoWrapTypography, + StyledCheckbox, + StyledTableContainerPaper, +} from "../../Common/StyledComponents.tsx"; +import FileIcon from "../../FileManager/Explorer/FileIcon.tsx"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import { getProgressColor } from "./TaskCard.tsx"; + +export interface DownloadFileListProps { + taskId: string; + summary?: TaskSummary; + downloading?: boolean; + readonly?: boolean; +} + +const DownloadFileList = ({ taskId, summary, downloading, readonly }: DownloadFileListProps) => { + const [selectedMask, setSelectedMask] = useState<{ + [key: number]: boolean; + }>({}); + const [changeApplied, setChangeApplied] = useState(true); + const [loading, setLoading] = useState(false); + const files = summary?.props?.download?.files; + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + + const progressColor = useMemo(() => { + return getProgressColor(theme); + }, [theme]); + + const setFileSelected = (value: DownloadTaskFile, selected: boolean) => { + setSelectedMask((prev) => { + return { + ...prev, + [value.index]: selected, + }; + }); + setChangeApplied(false); + }; + + const submitChanges = () => { + setLoading(true); + dispatch( + sendSetDownloadTarget(taskId, { + files: Object.keys(selectedMask).map((key) => ({ + index: parseInt(key), + download: selectedMask[parseInt(key)], + })), + }), + ) + .then(() => { + setChangeApplied(true); + enqueueSnackbar({ + message: t("download.operationSubmitted"), + variant: "success", + }); + }) + .finally(() => { + setLoading(false); + }); + }; + + const cancelTask = () => { + dispatch(confirmOperation(t("download.cancelTaskConfirm"))).then(() => { + setLoading(true); + dispatch(sendCancelDownloadTask(taskId)) + .then(() => { + enqueueSnackbar({ + message: t("download.taskCanceled"), + variant: "success", + }); + }) + .finally(() => { + setLoading(false); + }); + }); + }; + + return ( + + {files && ( + + , + // eslint-disable-next-line react/display-name + TableRow: (props) => { + const index = props["data-index"]; + const percentage = (files[index]?.progress ?? 0) * 100; + const progressBgColor = theme.palette.background.default; + return ( + + ); + }, + }} + data={files} + itemContent={(_index, value) => ( + <> + + { + setFileSelected(value, e.target.checked); + }} + disabled={!downloading || readonly} + disableRipple + checked={selectedMask[value.index] ?? value.selected} + size="small" + /> + + + + + + {fileBase(value.name)} + + + + + + {sizeToString(value.size)} + + + + + {((value.progress ?? 0) * 100).toFixed(2)} % + + + + )} + /> + + )} + {downloading && !readonly && summary?.phase == "monitor" && ( + + + + + + + + + )} + + ); +}; + +export default DownloadFileList; diff --git a/src/component/Pages/Tasks/DownloadList.tsx b/src/component/Pages/Tasks/DownloadList.tsx new file mode 100644 index 0000000..2a08ec0 --- /dev/null +++ b/src/component/Pages/Tasks/DownloadList.tsx @@ -0,0 +1,203 @@ +import * as React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { ListTaskCategory, TaskResponse } from "../../../api/workflow.ts"; +import { + Box, + Container, + FormControlLabel, + FormGroup, + Switch, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PageHeader from "../PageHeader.tsx"; +import { getTasks } from "../../../api/api.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import TaskCard from "./TaskCard.tsx"; +import PageContainer from "../PageContainer.tsx"; + +const defaultPageSize = 25; +const autoRefreshInterval = 20 * 1000; + +const DownloadList = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const [nextPageToken, setNextPageToken] = useState(""); + const [tasks, setTasks] = useState([]); + const [downloadingTasks, setDownloadingTasks] = useState< + TaskResponse[] | undefined + >(undefined); + const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(true); + const interval = React.useRef(); + const downloadingListHash = useRef(""); + + useEffect(() => { + if (autoRefresh && !interval.current) { + interval.current = setInterval(() => { + refresh(); + }, autoRefreshInterval); + } else { + clearInterval(interval.current); + interval.current = undefined; + } + }, [autoRefresh]); + + useEffect(() => { + return () => { + clearInterval(interval.current); + interval.current = undefined; + }; + }, []); + + const loadNextPage = useCallback( + (originTasks: TaskResponse[], token?: string) => () => { + setLoading(true); + dispatch( + getTasks({ + page_size: defaultPageSize, + category: ListTaskCategory.downloaded, + next_page_token: token, + }), + ) + .then((res) => { + if (token) { + setAutoRefresh(false); + } + setTasks([...originTasks, ...res.tasks]); + if (res.pagination?.next_token) { + setNextPageToken(res.pagination.next_token); + } else { + setNextPageToken(undefined); + } + }) + .catch(() => { + setNextPageToken(undefined); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch, setAutoRefresh, setTasks], + ); + + const loadDownloading = useCallback(() => { + setLoading(true); + dispatch( + getTasks({ + page_size: defaultPageSize, + category: ListTaskCategory.downloading, + }), + ) + .then((res) => { + setDownloadingTasks(res.tasks); + // New hash = id of first downloading task + id of last downloading task + length of downloading tasks + const newHash = `${res.tasks[0]?.id ?? ""}-${ + res.tasks[res.tasks.length - 1]?.id ?? "" + }-${res.tasks.length}`; + + if ( + downloadingListHash.current != "" && + downloadingListHash.current != newHash + ) { + loadNextPage([], "")(); + } + downloadingListHash.current = newHash; + }) + .catch(() => {}) + .finally(() => { + setLoading(false); + }); + }, [dispatch, setAutoRefresh, setDownloadingTasks]); + + const refresh = () => { + loadDownloading(); + }; + + const toggleAutoRefresh = (event: React.ChangeEvent) => { + if (event.target.checked && tasks.length > defaultPageSize) { + loadNextPage([], "")(); + } + setAutoRefresh(event.target.checked); + }; + + return ( + + + + + } + label={ + + {t("setting.autoRefresh")} + + } + /> + + } + onRefresh={refresh} + loading={loading} + title={t("application:navbar.remoteDownload")} + /> + + {t("download.active")} + + {downloadingTasks != undefined && downloadingTasks.length == 0 && ( + + + + )} + {downloadingTasks == undefined && ( + + )} + + {downloadingTasks && + downloadingTasks.map((task) => ( + + ))} + + {t("download.finished")} + + {tasks.map((task) => ( + + ))} + {nextPageToken != undefined && ( + + )} + + {nextPageToken == undefined && tasks.length == 0 && ( + + + + )} + + + ); +}; + +export default DownloadList; diff --git a/src/component/Pages/Tasks/PieceProgress.tsx b/src/component/Pages/Tasks/PieceProgress.tsx new file mode 100644 index 0000000..b7b0c9f --- /dev/null +++ b/src/component/Pages/Tasks/PieceProgress.tsx @@ -0,0 +1,65 @@ +import { memo, useEffect, useRef } from "react"; +import { Box, useTheme } from "@mui/material"; + +export interface PieceProgressProps { + pieces: string; + total: number; +} + +const PieceProgress: React.FC = memo( + ({ pieces, total }) => { + const theme = useTheme(); + const canvasRef = useRef(null); + useEffect(() => { + if (!pieces || !canvasRef.current || !total) { + return; + } + + const canvas = canvasRef.current; + const context = canvas.getContext("2d"); + if (!context) { + return; + } + + context.clearRect(0, 0, canvas.width, canvas.height); + context.strokeStyle = theme.palette.primary.light; + + const bits = Uint8Array.from(atob(pieces), (c) => c.charCodeAt(0)); + for (let i = 0; i < canvas.width; i++) { + let bitIndex = Math.floor(((i + 1) / canvas.width) * total); + // Read bool from unit8 array + let bit = bits[Math.floor(bitIndex / 8)] & (1 << bitIndex % 8); + if (bit) { + context.beginPath(); + context.moveTo(i, 0); + context.lineTo(i, canvas.height); + context.stroke(); + } + } + }, [pieces, theme, total]); + return ( + theme.palette.action.hover, + }} + > + + + ); + }, +); + +export default PieceProgress; diff --git a/src/component/Pages/Tasks/StepProgressBar.tsx b/src/component/Pages/Tasks/StepProgressBar.tsx new file mode 100644 index 0000000..3d04bfd --- /dev/null +++ b/src/component/Pages/Tasks/StepProgressBar.tsx @@ -0,0 +1,49 @@ +import { Box, Skeleton, Typography } from "@mui/material"; +import BorderLinearProgress from "../../Common/BorderLinearProgress.tsx"; + +export interface StepProgressBarProps { + title?: string; + secondary?: string; + caption?: string; + progress?: number; + loading?: boolean; + indeterminate?: boolean; +} + +const StepProgressBar = ({ + title, + secondary, + progress, + loading, + indeterminate, +}: StepProgressBarProps) => { + return ( + + + + {loading ? : title} + + {secondary && ( + + {secondary} + + )} + + {loading ? ( + + ) : ( + + )} + + ); +}; + +export default StepProgressBar; diff --git a/src/component/Pages/Tasks/StepProgressPopover.tsx b/src/component/Pages/Tasks/StepProgressPopover.tsx new file mode 100644 index 0000000..23fa46b --- /dev/null +++ b/src/component/Pages/Tasks/StepProgressPopover.tsx @@ -0,0 +1,206 @@ +import { Box, Divider, PopoverProps, Typography } from "@mui/material"; +import HoverPopover from "material-ui-popup-state/HoverPopover"; +import { useCallback, useEffect, useState } from "react"; +import { + TaskProgress, + TaskProgresses, + TaskResponse, +} from "../../../api/workflow.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { getTasksPhaseProgress } from "../../../api/api.ts"; +import { useTranslation } from "react-i18next"; +import StepProgressBar from "./StepProgressBar.tsx"; +import { sizeToString } from "../../../util"; + +export interface StepProgressPopoverProps extends PopoverProps { + task: TaskResponse; +} + +export const ProgressKeys = { + relocate: "relocate", + upload: "upload", + upload_single_: "upload_single_", + archive_count: "archive_count", + archive_size: "archive_size", + upload_count: "upload_count", + extract_count: "extract_count", + extract_size: "extract_size", + download: "download", +}; + +const ProgressBar = ({ pkey, p }: { pkey: string; p: TaskProgress }) => { + const { t } = useTranslation(); + if (pkey == ProgressKeys.relocate) { + return ( + + ); + } + + if (pkey.startsWith(ProgressKeys.upload_single_)) { + return ( + + ); + } + + if (pkey == ProgressKeys.archive_count) { + let secondary = `${p.current}`; + if (p.total != 0) { + secondary += ` / ${p.total}`; + } + return ( + + ); + } + + if (pkey == ProgressKeys.archive_size) { + let secondary = sizeToString(p.current); + if (p.total != 0) { + secondary += ` / ${sizeToString(p.total)}`; + } + return ( + + ); + } + + if (pkey == ProgressKeys.upload) { + return ( + + ); + } + + if (pkey == ProgressKeys.extract_size) { + let secondary = sizeToString(p.current); + if (p.total != 0) { + secondary += ` / ${sizeToString(p.total)}`; + } + return ( + + ); + } + + if (pkey == ProgressKeys.extract_count) { + let secondary = `${p.current}`; + if (p.total != 0) { + secondary += ` / ${p.total}`; + } + return ( + + ); + } + + if (pkey == ProgressKeys.upload_count) { + let secondary = `${p.current}`; + if (p.total != 0) { + secondary += ` / ${p.total}`; + } + return ( + + ); + } + + if (pkey == ProgressKeys.download) { + return ( + + ); + } +}; + +const StepProgressPopover = ({ + task, + open, + ...rest +}: StepProgressPopoverProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const stopPropagation = useCallback((e: any) => e.stopPropagation(), []); + const [progress, setProgress] = useState( + undefined, + ); + + useEffect(() => { + if (open) { + dispatch(getTasksPhaseProgress(task.id)).then((res) => setProgress(res)); + } + }, [open, task]); + + return ( + + + {!progress && } + {progress && + Object.keys(progress).map((key, index) => ( + <> + + {index < Object.keys(progress).length - 1 && ( + + )} + + ))} + {progress && Object.keys(progress).length == 0 && ( + + {t("setting.progressNotAvailable")} + + )} + + + ); +}; + +export default StepProgressPopover; diff --git a/src/component/Pages/Tasks/TaskCard.tsx b/src/component/Pages/Tasks/TaskCard.tsx new file mode 100644 index 0000000..7fb3c2b --- /dev/null +++ b/src/component/Pages/Tasks/TaskCard.tsx @@ -0,0 +1,220 @@ +import { + Box, + darken, + lighten, + Skeleton, + styled, + SvgIconProps, + Theme, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import MuiAccordion, { AccordionProps } from "@mui/material/Accordion"; +import MuiAccordionDetails from "@mui/material/AccordionDetails"; +import MuiAccordionSummary, { AccordionSummaryProps } from "@mui/material/AccordionSummary"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useInView } from "react-intersection-observer"; +import { FileType } from "../../../api/explorer.ts"; +import { TaskResponse, TaskType } from "../../../api/workflow.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { DefaultButton } from "../../Common/StyledComponents.tsx"; +import FileIcon from "../../FileManager/Explorer/FileIcon.tsx"; +import Archive from "../../Icons/Archive.tsx"; +import ArchiveArrow from "../../Icons/ArchiveArrow.tsx"; +import StorageOutlined from "../../Icons/StorageOutlined.tsx"; +import TaskDetail from "./TaskDetail.tsx"; +import TaskSummaryStatus from "./TaskSummaryStatus.tsx"; +import TaskSummaryTitle from "./TaskSummaryTitle.tsx"; + +const Accordion = styled((props: AccordionProps) => )(({ + theme, + expanded, +}) => { + const bgColor = expanded + ? theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)" + : "initial"; + return { + borderRadius: theme.shape.borderRadius, + backgroundColor: bgColor, + "&::before": { + display: "none", + }, + boxShadow: expanded ? `0 0 0 1px ${theme.palette.divider}` : "none", + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + marginBottom: theme.spacing(1), + }; +}); + +const AccordionSummary = styled((props: AccordionSummaryProps) => )(() => ({ + flexDirection: "row-reverse", + minHeight: 0, + padding: 0, + "& .MuiAccordionSummary-content": { + margin: 0, + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + padding: theme.spacing(2), + borderRadius: `0 0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px`, + backgroundColor: theme.palette.background.default, +})); + +export const getProgressColor = (theme: Theme) => + theme.palette.mode === "dark" ? darken(theme.palette.primary.main, 0.4) : lighten(theme.palette.primary.main, 0.85); + +export const SummaryButton = styled(DefaultButton)<{ + expanded: boolean; + percentage?: number; +}>(({ theme, expanded, percentage }) => { + percentage = percentage ?? 0; + const bgColor = theme.palette.mode == "light" ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.09)"; + const progressColor = getProgressColor(theme); + const progressBgColor = !expanded ? bgColor : "rgba(0,0,0,0)"; + return { + minHeight: 48, + justifyContent: "flex-start", + transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms", + borderRadius: expanded + ? `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0 0` + : `${theme.shape.borderRadius}px`, + backgroundColor: bgColor, + background: `linear-gradient(to right, ${progressColor} 0%,${progressColor} ${percentage}%,${progressBgColor} ${percentage}%,${progressBgColor} 100%)`, + "&:hover": { + backgroundColor: theme.palette.mode == "light" ? "rgba(0, 0, 0, 0.09)" : "rgba(255, 255, 255, 0.13)", + }, + }; +}); + +export interface TaskCardProps { + loading?: boolean; + showProgress?: boolean; + task?: TaskResponse; + onLoad?: () => void; +} + +const taskIconsMap: { + [key: string]: (props: SvgIconProps) => JSX.Element; +} = { + [TaskType.create_archive]: Archive, + [TaskType.extract_archive]: ArchiveArrow, + [TaskType.relocate]: StorageOutlined, +}; + +const TaskCard = ({ loading, showProgress, onLoad, task }: TaskCardProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const dispatch = useAppDispatch(); + const { ref, inView } = useInView({ + rootMargin: "200px 0px", + triggerOnce: true, + skip: !loading, + }); + + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (!inView) { + return; + } + + if (onLoad) { + onLoad(); + } + }, [inView]); + + const handleChange = (_event: React.SyntheticEvent, newExpanded: boolean) => { + if (loading) { + return; + } + setExpanded(newExpanded); + }; + + const TaskIcon = useMemo(() => { + return taskIconsMap[task?.type ?? ""] ?? Archive; + }, [task?.type]); + + return ( + + + + ) : task?.type === TaskType.remote_download ? ( + 1 ? FileType.folder : FileType.file, + name: task?.summary?.props.download?.name ?? "", + }} + /> + ) : ( + + ) + } + > + + + {loading || !task ? ( + + ) : ( + + + + )} + + + + {loading || !task ? ( + + ) : ( + + )} + + + + + {task && } + + ); +}; + +export default TaskCard; diff --git a/src/component/Pages/Tasks/TaskDetail.tsx b/src/component/Pages/Tasks/TaskDetail.tsx new file mode 100644 index 0000000..e4bb401 --- /dev/null +++ b/src/component/Pages/Tasks/TaskDetail.tsx @@ -0,0 +1,91 @@ +import { + Alert, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { TaskResponse, TaskStatus } from "../../../api/workflow.ts"; +import { StyledTableContainerPaper } from "../../Common/StyledComponents.tsx"; +import DownloadFileList from "./DownloadFileList.tsx"; +import TaskProgress from "./TaskProgress.tsx"; +import TaskProps from "./TaskProps.tsx"; + +export interface TaskDetailProps { + task: TaskResponse; + downloading?: boolean; +} + +const TaskDetail = ({ task, downloading }: TaskDetailProps) => { + const { t } = useTranslation(); + return ( + + + {task.summary?.props?.download && ( + <> + + {t("setting.fileList")} + + + + + )} + + {t("setting.taskProgress")} + + {!!task.summary?.props?.failed && ( + + {t("setting.partialSuccessWarning", { + num: task.summary?.props?.failed, + })} + + )} + {task.status == TaskStatus.error && {task.error}} + + + + + + {t("setting.taskDetails")} + + + {task.error_history && } + + {task.error_history && ( + + + {t("setting.retryErrorHistory")} + + +
+ + + # + {t("common:error")} + + + + {task.error_history.map((error, index) => ( + + + {index + 1} + + {error} + + ))} + +
+
+ + )} + + ); +}; + +export default TaskDetail; diff --git a/src/component/Pages/Tasks/TaskList.tsx b/src/component/Pages/Tasks/TaskList.tsx new file mode 100644 index 0000000..9cb77bd --- /dev/null +++ b/src/component/Pages/Tasks/TaskList.tsx @@ -0,0 +1,124 @@ +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; +import { ListTaskCategory, TaskResponse } from "../../../api/workflow.ts"; +import { Box, Container, FormControlLabel, FormGroup, Switch, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PageHeader from "../PageHeader.tsx"; +import { getTasks } from "../../../api/api.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import Nothing from "../../Common/Nothing.tsx"; +import TaskCard from "./TaskCard.tsx"; +import PageContainer from "../PageContainer.tsx"; + +const defaultPageSize = 25; +const autoRefreshInterval = 20 * 1000; + +const TaskList = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const policyOption = useAppSelector((state) => state.globalState.policyOptionCache); + const [nextPageToken, setNextPageToken] = useState(""); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(true); + const interval = React.useRef(); + + useEffect(() => { + if (autoRefresh && !interval.current) { + interval.current = setInterval(() => { + refresh(); + }, autoRefreshInterval); + } else { + clearInterval(interval.current); + interval.current = undefined; + } + }, [autoRefresh]); + + useEffect(() => { + return () => { + clearInterval(interval.current); + interval.current = undefined; + }; + }, []); + + const loadNextPage = useCallback( + (originTasks: TaskResponse[], token?: string) => () => { + setLoading(true); + dispatch( + getTasks({ + page_size: defaultPageSize, + category: ListTaskCategory.general, + next_page_token: token, + }), + ) + .then((res) => { + if (token) { + setAutoRefresh(false); + } + setTasks([...originTasks, ...res.tasks]); + if (res.pagination?.next_token) { + setNextPageToken(res.pagination.next_token); + } else { + setNextPageToken(undefined); + } + }) + .catch(() => { + setNextPageToken(undefined); + }) + .finally(() => { + setLoading(false); + }); + }, + [dispatch, setAutoRefresh, setTasks], + ); + + const refresh = () => { + loadNextPage([], "")(); + }; + + const toggleAutoRefresh = (event: React.ChangeEvent) => { + if (event.target.checked && tasks.length > defaultPageSize) { + loadNextPage([], "")(); + } + setAutoRefresh(event.target.checked); + }; + + return ( + + + + } + label={ + + {t("setting.autoRefresh")} + + } + /> + + } + onRefresh={refresh} + loading={loading} + title={t("application:navbar.taskQueue")} + /> + {tasks.map((task) => ( + + ))} + {nextPageToken != undefined && ( + + )} + + {nextPageToken == undefined && tasks.length == 0 && ( + + + + )} + + + ); +}; + +export default TaskList; diff --git a/src/component/Pages/Tasks/TaskProgress.tsx b/src/component/Pages/Tasks/TaskProgress.tsx new file mode 100644 index 0000000..af37666 --- /dev/null +++ b/src/component/Pages/Tasks/TaskProgress.tsx @@ -0,0 +1,246 @@ +import { + NodeTypes, + TaskResponse, + TaskStatus, + TaskType, +} from "../../../api/workflow.ts"; +import { useEffect, useMemo, useState } from "react"; +import { Box, Stepper, useMediaQuery, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import TaskProgressStep from "./TaskProgressStep.tsx"; +import PieceProgress from "./PieceProgress.tsx"; + +export interface TaskProgressProps { + task: TaskResponse; +} + +interface StepModel { + title: string; + state: string; + description?: string; + supportProgress?: boolean; +} + +const queueingStep: StepModel = { + title: "setting.queueToStart", + state: "", +}; + +const completedStep: StepModel = { + title: "setting.finished", + state: "", +}; + +const stepOptions: { + [key: string]: StepModel[][]; +} = { + [TaskType.remote_download]: [ + // Master + [ + queueingStep, + { + title: "fileManager.download", + state: "monitor", + description: "setting.downloadDes", + }, + { + title: "setting.transferring", + state: "transfer", + description: "setting.downloadTransferDes", + supportProgress: true, + }, + { + title: "setting.awaitSeeding", + state: "seeding", + description: "setting.awaitSeedingDes", + }, + completedStep, + ], + // Slave + [ + queueingStep, + { + title: "fileManager.download", + state: "monitor", + description: "setting.downloadDes", + }, + { + title: "setting.transferring", + state: "transfer", + description: "setting.downloadTransferDes", + supportProgress: true, + }, + { + title: "setting.awaitSeeding", + state: "seeding", + description: "setting.awaitSeedingDes", + }, + completedStep, + ], + ], + [TaskType.relocate]: [ + // Master + [ + queueingStep, + { + title: "setting.indexingFiles", + state: "", + description: "setting.indexingFilesDes", + }, + { + title: "setting.transferring", + state: "transfer", + description: "setting.transferringRelocateDes", + supportProgress: true, + }, + { + title: "setting.committingChanges", + state: "finish", + description: "setting.relocateFinishing", + }, + ], + ], + [TaskType.extract_archive]: [ + // Master + [ + queueingStep, + { + title: "setting.downloadingZip", + description: "setting.downloadingZipDes", + state: "download_zip", + supportProgress: true, + }, + { + title: "setting.extractingFiles", + state: "", + description: "setting.extractingFilesDes", + supportProgress: true, + }, + ], + // Slave + [ + queueingStep, + { + title: "setting.sendTask", + description: "setting.sendTaskDes", + state: "", + }, + { + title: "setting.extractingFiles", + state: "await_slave_complete", + description: "setting.extractingFilesDes", + supportProgress: true, + }, + ], + ], + [TaskType.create_archive]: [ + // Master + [ + queueingStep, + { + title: "setting.prepare", + state: "", + description: "setting.preparingWorkspaceDes", + }, + { + title: "setting.compressFiles", + state: "compress_files", + description: "setting.compressFilesDes", + supportProgress: true, + }, + { + title: "setting.transferring", + state: "upload_archive", + description: "setting.uploadArchiveFileDes", + supportProgress: true, + }, + ], + // Slave + [ + queueingStep, + { + title: "setting.indexingFiles", + state: "", + description: "setting.indexForArchiveDes", + }, + { + title: "setting.compressFiles", + state: "await_slave_compressing", + description: "setting.compressFilesDes", + supportProgress: true, + }, + { + title: "setting.transferring", + state: "await_slave_uploading", + description: "setting.uploadArchiveFileDes", + supportProgress: true, + }, + { + title: "setting.committingChanges", + state: "complete_upload", + description: "setting.createArchiveFinishing", + }, + ], + ], +}; + +const TaskProgress = ({ task }: TaskProgressProps) => { + const { t } = useTranslation(); + const [activeStep, setActiveStep] = useState(0); + const steps = useMemo((): StepModel[] => { + return ( + stepOptions[task.type]?.[task.node?.type == NodeTypes.slave ? 1 : 0] ?? [] + ); + }, [task.id, task.node?.type]); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + useEffect(() => { + if (task.status == TaskStatus.queued) { + setActiveStep(0); + return; + } + if (task.status == TaskStatus.completed) { + setActiveStep(steps.length); + return; + } + let active = 1; + for (let i = 1; i < steps.length; i++) { + if (steps[i].state == task.summary?.phase) { + active = i; + } + } + + setActiveStep(active); + }, [steps, task]); + + return ( + + + {steps.map((step, index) => ( + + ))} + + {task.type == TaskType.remote_download && + task.summary?.props.download?.pieces && + task.summary?.phase == "monitor" && ( + + )} + + ); +}; + +export default TaskProgress; diff --git a/src/component/Pages/Tasks/TaskProgressStep.tsx b/src/component/Pages/Tasks/TaskProgressStep.tsx new file mode 100644 index 0000000..17c6583 --- /dev/null +++ b/src/component/Pages/Tasks/TaskProgressStep.tsx @@ -0,0 +1,124 @@ +import { TaskResponse, TaskStatus } from "../../../api/workflow.ts"; +import { + Step, + StepIcon, + StepIconProps, + StepLabel, + Typography, +} from "@mui/material"; +import { StepProps } from "@mui/material/Step/Step"; +import { useTranslation } from "react-i18next"; +import FacebookCircularProgress from "../../Common/CircularProgress.tsx"; +import DismissCircleFilled from "../../Icons/DismissCircleFilled.tsx"; +import { useMemo } from "react"; +import CheckCircleFilled from "../../Icons/CheckCircleFilled.tsx"; +import { usePopupState } from "material-ui-popup-state/hooks"; +import { bindHover, bindPopover } from "material-ui-popup-state"; +import StepProgressPopover from "./StepProgressPopover.tsx"; + +interface ProgressStepIconProps extends StepIconProps {} + +const ProgressStepIcon = + (task: TaskResponse) => (props: ProgressStepIconProps) => { + const { active, completed, icon, ...rest } = props; + + let newIcon = icon; + if (active) { + newIcon = ; + if (task.status == TaskStatus.error) { + newIcon = ( + theme.palette.error.main }} + /> + ); + } + } else if (completed) { + newIcon = ( + theme.palette.primary.main }} + /> + ); + } + + if (active && task.status == TaskStatus.error) { + newIcon = ( + theme.palette.error.main }} + /> + ); + } + + if (active && task.status == TaskStatus.canceled) { + newIcon = ( + theme.palette.action.active, + }} + /> + ); + } + + return ( + + ); + }; + +export interface TaskProgressStepProps extends StepProps { + task: TaskResponse; + title: string; + description?: string; + showProgress?: boolean; + progressing?: boolean; +} + +const TaskProgressStep = ({ + task, + title, + description, + showProgress, + progressing, + ...rest +}: TaskProgressStepProps) => { + const popupState = usePopupState({ + variant: "popover", + popupId: `progress_${task.id}_${title}`, + }); + const { open, ...restPopup } = bindPopover(popupState); + + const StepIconComponent = useMemo(() => { + return ProgressStepIcon(task); + }, [task.status]); + const { t } = useTranslation(); + return ( + <> + + {t(description)} + ) : undefined + } + slots={{ + stepIcon: StepIconComponent + }} + > + {t(title)} + + + {showProgress && progressing && ( + + )} + + ); +}; + +export default TaskProgressStep; diff --git a/src/component/Pages/Tasks/TaskProps.tsx b/src/component/Pages/Tasks/TaskProps.tsx new file mode 100644 index 0000000..5a6a5eb --- /dev/null +++ b/src/component/Pages/Tasks/TaskProps.tsx @@ -0,0 +1,169 @@ +import { Grid, Stack, Typography } from "@mui/material"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import { TFunction } from "i18next"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { FileType } from "../../../api/explorer.ts"; +import { TaskResponse, TaskStatus, TaskType } from "../../../api/workflow.ts"; +import { sizeToString } from "../../../util"; +import { formatDuration } from "../../../util/datetime.ts"; +import TimeBadge from "../../Common/TimeBadge.tsx"; +import FileBadge from "../../FileManager/FileBadge.tsx"; + +dayjs.extend(duration); + +export interface TaskPropsProps { + task: TaskResponse; +} + +interface TaskPropsBlockProps { + label: string; + value: React.ReactNode; +} + +const TaskPropsBlock = ({ label, value }: TaskPropsBlockProps) => { + return ( + + + + {label} + + + + + {value} + + + + ); +}; + +export const getTaskStatusText = (status: TaskStatus, t: TFunction) => { + switch (status) { + case TaskStatus.queued: + return t("application:setting.queueing"); + case TaskStatus.processing: + return t("application:setting.processing"); + case TaskStatus.suspending: + return t("application:setting.processing") + t("application:setting.suspended"); + case TaskStatus.canceled: + return t("application:setting.canceled"); + case TaskStatus.error: + return t("application:setting.failed"); + case TaskStatus.completed: + return t("application:setting.finished"); + default: + return t("application:uplaoder.unknownStatus"); + } +}; + +const TaskProps = ({ task }: TaskPropsProps) => { + const { t } = useTranslation(); + const status = useMemo(() => getTaskStatusText(task.status as TaskStatus, t), [task.status, t]); + + return ( + + } + /> + } + /> + + + {task.summary?.props.src && ( + + } + /> + )} + {task.summary?.props.src_str && } + {task.summary?.props.src_multiple && ( + + {task.summary?.props.src_multiple.map((src, index) => ( + + ))} + + } + /> + )} + {task.summary?.props.dst && ( + + } + /> + )} + + {task.resume_time && (task.status == TaskStatus.suspending || task.status == TaskStatus.processing) && ( + } + /> + )} + + {!!task.summary?.props.download?.num_pieces && ( + + )} + {!!task.summary?.props.download?.uploaded && ( + + )} + {!!task.summary?.props.download?.upload_speed && ( + + )} + {!!task.summary?.props.download?.hash && ( + + )} + + ); +}; + +export default TaskProps; diff --git a/src/component/Pages/Tasks/TaskSummaryStatus.tsx b/src/component/Pages/Tasks/TaskSummaryStatus.tsx new file mode 100644 index 0000000..e425793 --- /dev/null +++ b/src/component/Pages/Tasks/TaskSummaryStatus.tsx @@ -0,0 +1,152 @@ +import { Box, styled, Tooltip, Typography, useTheme } from "@mui/material"; +import { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; +import { TaskStatus, TaskSummary, TaskType } from "../../../api/workflow.ts"; +import { useAppDispatch } from "../../../redux/hooks.ts"; +import { sizeToString } from "../../../util"; +import ArrowSyncCircleFilled from "../../Icons/ArrowSyncCircleFilled.tsx"; +import CheckCircleFilled from "../../Icons/CheckCircleFilled.tsx"; +import CircleHintFilled from "../../Icons/CircleHintFilled.tsx"; +import DismissCircleFilled from "../../Icons/DismissCircleFilled.tsx"; + +const ArrowSyncCircleFilledSpin = styled(ArrowSyncCircleFilled)({ + animation: "spin 4s linear infinite", + "@keyframes spin": { + from: { + transform: "rotate(0deg)", + }, + to: { + transform: "rotate(360deg)", + }, + }, +}); + +export interface TaskSummaryStatusProps { + type: string; + status: string; + summary?: TaskSummary; + error?: string; + simplified?: boolean; +} + +interface TaskStatusContentProps { + color: string; + icon: React.ReactNode; + title: string; + [key: string]: any; +} +const TaskStatusContent = forwardRef(({ icon, title, color, ...props }: TaskStatusContentProps, ref) => { + return ( + + + {icon} + {title} + + + ); +}); + +const TaskSummaryStatus = ({ type, status, summary, error, simplified }: TaskSummaryStatusProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + + switch (status) { + case TaskStatus.completed: + return ( + } + color={theme.palette.success.main} + /> + ); + case TaskStatus.error: + return ( + + } + color={theme.palette.error.main} + /> + + ); + case TaskStatus.processing: + case TaskStatus.suspending: + if (type == TaskType.remote_download) { + if (summary?.phase == "monitor" && summary?.props.download) { + const downloadStatus = summary.props.download; + return ( + + {!simplified && + `${sizeToString(downloadStatus.download_speed)} /s - ${sizeToString( + downloadStatus.downloaded, + )} / ${sizeToString(downloadStatus.total)}`} + } + color={theme.palette.primary.main} + /> + + ); + } else if (summary?.phase == "seeding") { + return ( + } + color={theme.palette.primary.main} + /> + ); + } else if (summary?.phase == "transfer") { + return ( + } + color={theme.palette.primary.main} + /> + ); + } else { + return ( + } + color={theme.palette.primary.main} + /> + ); + } + } + return ( + } + color={theme.palette.primary.main} + /> + ); + case TaskStatus.queued: + return ( + } + color={theme.palette.action.active} + /> + ); + case TaskStatus.canceled: + return ( + } + color={theme.palette.action.active} + /> + ); + } +}; + +export default TaskSummaryStatus; diff --git a/src/component/Pages/Tasks/TaskSummaryTitle.tsx b/src/component/Pages/Tasks/TaskSummaryTitle.tsx new file mode 100644 index 0000000..e3de5b3 --- /dev/null +++ b/src/component/Pages/Tasks/TaskSummaryTitle.tsx @@ -0,0 +1,119 @@ +import { Box, Chip, styled, Typography } from "@mui/material"; +import { useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { FileType } from "../../../api/explorer.ts"; +import { TaskSummary, TaskType } from "../../../api/workflow.ts"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts"; +import { newMyUri } from "../../../util/uri.ts"; +import FileBadge from "../../FileManager/FileBadge.tsx"; + +export interface TaskSummaryTitleProps { + type: string; + summary?: TaskSummary; + isInDashboard?: boolean; +} + +const StyledFileBadge = styled(FileBadge)(() => ({ + paddingLeft: 8, + paddingRight: 8, + marginLeft: 4, + marginRight: 4, + maxWidth: "200px", +})); + +const StyledChip = styled(Chip)(() => ({ + marginLeft: 8, + height: "20px", +})); + +const TaskSummaryTitle = ({ type, summary, isInDashboard = false }: TaskSummaryTitleProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const policyOption = useAppSelector((state) => state.globalState.policyOptionCache); + + const selectedCount = useMemo(() => { + let selected = 0; + for (const file of summary?.props.download?.files ?? []) { + if (file.selected) { + selected++; + } + } + + return selected; + }, [summary?.props.download?.files]); + + switch (type) { + case TaskType.remote_download: + return ( + + + {isInDashboard && t("dashboard:task.remoteDownload")} + {summary?.props.download?.name ?? t("download.unknownTaskName")} + {selectedCount > 1 && } + + + ); + case TaskType.create_archive: + return ( + + {summary?.props.src_multiple?.slice(0, 3).map((src) => ( + + ))} + , + , + ]} + values={{ + more: (summary?.props.src_multiple?.length ?? 0) > 3 ? "..." : "", + }} + /> + ); + default: + return ( + , + , + ]} + values={{ + more: (summary?.props.src_multiple?.length ?? 0) > 3 ? "..." : "", + }} + /> + ); + } +}; + +export default TaskSummaryTitle; diff --git a/src/component/Placeholder/Captcha.js b/src/component/Placeholder/Captcha.js deleted file mode 100644 index d17542a..0000000 --- a/src/component/Placeholder/Captcha.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import ContentLoader from "react-content-loader"; - -const MyLoader = () => ( - - - -); - -function captchaPlacholder() { - return ; -} - -export default captchaPlacholder; diff --git a/src/component/Placeholder/DropFile.js b/src/component/Placeholder/DropFile.js deleted file mode 100644 index bef66c1..0000000 --- a/src/component/Placeholder/DropFile.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import Backdrop from "@material-ui/core/Backdrop"; -import { createStyles, makeStyles } from "@material-ui/core/styles"; -import UploadIcon from "@material-ui/icons/CloudUpload"; -import { Typography } from "@material-ui/core"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => - createStyles({ - backdrop: { - zIndex: theme.zIndex.drawer + 1, - color: "#fff", - flexDirection: "column", - }, - }) -); - -export function DropFileBackground({ open }) { - const classes = useStyles(); - const { t } = useTranslation(); - return ( - -
- -
-
- - {t("uploader.dropFileHere")} - -
-
- ); -} diff --git a/src/component/Placeholder/ErrorBoundary.js b/src/component/Placeholder/ErrorBoundary.js deleted file mode 100644 index ae4bab9..0000000 --- a/src/component/Placeholder/ErrorBoundary.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import { withStyles } from "@material-ui/core"; -import { withTranslation } from "react-i18next"; - -const styles = { - h1: { - color: "#a4a4a4", - margin: "5px 0px", - }, - h2: { - margin: "15px 0px", - }, -}; - -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false, error: null, errorInfo: null }; - } - - static getDerivedStateFromError() { - return { hasError: true }; - } - - componentDidCatch(error, errorInfo) { - this.setState({ - error: error, - errorInfo: errorInfo, - }); - } - - render() { - const { classes, t } = this.props; - if (this.state.hasError) { - return ( - <> -

:(

-

{t("renderError")}

- {this.state.error && - this.state.errorInfo && - this.state.errorInfo.componentStack && ( -
- {t("errorDetails")} -
-                                    {this.state.error.toString()}
-                                
-
-                                    
-                                        {this.state.errorInfo.componentStack}
-                                    
-                                
-
- )} - - ); - } - - return this.props.children; - } -} - -export default withTranslation(["common"])(withStyles(styles)(ErrorBoundary)); diff --git a/src/component/Placeholder/ListLoading.js b/src/component/Placeholder/ListLoading.js deleted file mode 100644 index bbca6d4..0000000 --- a/src/component/Placeholder/ListLoading.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { BulletList } from "react-content-loader"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - loader: { - width: "100%", - // padding: 40, - // [theme.breakpoints.down("md")]: { - // width: "100%", - // padding: 10 - // } - }, -})); - -const MyLoader = (props) => ( - -); - -function ListLoading() { - const theme = useTheme(); - const classes = useStyles(); - - return ( -
- -
- ); -} - -export default ListLoading; diff --git a/src/component/Placeholder/Nothing.js b/src/component/Placeholder/Nothing.js deleted file mode 100644 index 6ea2e24..0000000 --- a/src/component/Placeholder/Nothing.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import { PackageVariant } from "mdi-material-ui"; -import { makeStyles } from "@material-ui/core"; - -const useStyles = makeStyles((theme) => ({ - emptyContainer: { - bottom: "0", - - color: theme.palette.action.disabled, - textAlign: "center", - paddingTop: "20px", - }, - emptyInfoBig: { - fontSize: "25px", - color: theme.palette.action.disabled, - }, - emptyInfoSmall: { - color: theme.palette.action.disabled, - }, -})); - -export default function Nothing({ primary, secondary, top = 20, size = 1 }) { - const classes = useStyles(); - return ( -
- -
- {primary} -
- {secondary !== "" && ( -
{secondary}
- )} -
- ); -} diff --git a/src/component/Placeholder/PageLoading.js b/src/component/Placeholder/PageLoading.js deleted file mode 100644 index 411f662..0000000 --- a/src/component/Placeholder/PageLoading.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; -import { Facebook } from "react-content-loader"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - loader: { - width: "80%", - [theme.breakpoints.up("md")]: { - width: " 50%", - }, - - marginTop: 30, - }, -})); - -const MyLoader = (props) => { - return ( - - ); -}; - -function PageLoading() { - const theme = useTheme(); - const classes = useStyles(); - - return ( -
- -
- ); -} - -export default PageLoading; diff --git a/src/component/Placeholder/TextLoading.js b/src/component/Placeholder/TextLoading.js deleted file mode 100644 index da264ec..0000000 --- a/src/component/Placeholder/TextLoading.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { Code } from "react-content-loader"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - loader: { - width: "70%", - padding: 40, - [theme.breakpoints.down("md")]: { - width: "100%", - padding: 10, - }, - }, -})); - -const MyLoader = (props) => ( - -); - -function TextLoading() { - const theme = useTheme(); - const classes = useStyles(); - - return ( -
- -
- ); -} - -export default TextLoading; diff --git a/src/component/Setting/AppPromotion.js b/src/component/Setting/AppPromotion.js deleted file mode 100644 index 10ff334..0000000 --- a/src/component/Setting/AppPromotion.js +++ /dev/null @@ -1,210 +0,0 @@ -import React from "react"; -import { Grid, makeStyles, Typography } from "@material-ui/core"; -import { Trans, useTranslation } from "react-i18next"; -import { useTheme, fade } from "@material-ui/core/styles"; -import Box from "@material-ui/core/Box"; -import { useSelector } from "react-redux"; -import Link from "@material-ui/core/Link"; -import AppQRCode from "./AppQRCode"; - -const PhoneSkeleton = () => { - const theme = useTheme(); - - return ( - - - - - - ); -}; - -const useStyles = makeStyles((theme) => ({ - phoneContainer: { - maxWidth: 450, - position: "relative", - marginX: "auto", - perspective: 1500, - transformStyle: "preserve-3d", - perspectiveOrigin: 0, - }, - phoneFrame: { - position: "relative", - borderRadius: "2.75rem", - boxShadow: 1, - width: "75% !important", - marginX: "auto", - transform: "rotateY(-35deg) rotateX(15deg) translateZ(0)", - }, - phoneImage: { - objectFit: "cover", - borderRadius: "2.5rem", - filter: theme.palette.type === "dark" ? "brightness(0.7)" : "none", - }, - highlight: { - background: `linear-gradient(180deg, transparent 82%, ${fade( - theme.palette.secondary.main, - 0.3 - )} 0%)`, - }, - bold: { - fontWeight: 700, - }, - frameContainer: { - verticalAlign: "middle", - }, - grid: { - padding: theme.spacing(2), - paddingTop: 0, - paddingBottom: theme.spacing(4), - }, - "@global": { - ol: { - paddingInlineStart: 24, - }, - "li::marker": { - fontSize: "1.25rem", - }, - li: { - marginBottom: theme.spacing(2), - }, - }, -})); - -export default function AppPromotion() { - const { t } = useTranslation("application", { keyPrefix: "setting" }); - const theme = useTheme(); - const title = useSelector((state) => state.siteConfig.title); - - const classes = useStyles(); - - return ( - - - - - - , - ]} - /> - - - -
    -
  1. - - {t("downloadOurApp")} - - - - - - -
  2. -
  3. - - {t("fillInEndpoint")} - - - - -
  4. -
  5. - - {t("loginApp")} - -
  6. -
-
-
-
- - - - - - - - - - - - - - -
- ); -} diff --git a/src/component/Setting/AppQRCode.js b/src/component/Setting/AppQRCode.js deleted file mode 100644 index a3db9e9..0000000 --- a/src/component/Setting/AppQRCode.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { CircularProgress, makeStyles } from "@material-ui/core"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@material-ui/core/styles"; -import Box from "@material-ui/core/Box"; -import { QRCodeSVG } from "qrcode.react"; -import { randomStr } from "../../utils"; -import classNames from "classnames"; -import API from "../../middleware/Api"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../redux/explorer"; - -const useStyles = makeStyles((theme) => ({ - blur: { - filter: "blur(8px)", - }, - container: { - position: "relative", - width: 128, - }, - progress: { - position: "absolute", - top: "50%", - left: "50%", - marginLeft: -12, - marginTop: -12, - zIndex: 1, - }, - qrcode: { - transition: "all .3s ease-in", - }, -})); - -export default function AppQRCode() { - const [content, setContent] = useState(randomStr(32)); - const [loading, setLoading] = useState(true); - const { t } = useTranslation("application", { keyPrefix: "setting" }); - const theme = useTheme(); - const classes = useStyles(); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const refresh = () => { - setLoading(true); - API.get("/user/session") - .then((response) => { - setContent(response.data); - setLoading(false); - }) - .catch((error) => { - setLoading(false); - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - refresh(); - const interval = window.setInterval(refresh, 1000 * 45); - return () => { - window.clearInterval(interval); - }; - }, []); - - return ( - - {loading && ( - - )} - - - ); -} diff --git a/src/component/Setting/Authn.js b/src/component/Setting/Authn.js deleted file mode 100644 index 9f9e942..0000000 --- a/src/component/Setting/Authn.js +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - List, - ListItem, - ListItemIcon, - ListItemSecondaryAction, - ListItemText, - makeStyles, - Paper, - Typography, -} from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import RightIcon from "@material-ui/icons/KeyboardArrowRight"; -import { Add, Fingerprint, HighlightOff } from "@material-ui/icons"; -import API from "../../middleware/Api"; -import { bufferDecode, bufferEncode } from "../../utils"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - sectionTitle: { - paddingBottom: "10px", - paddingTop: "30px", - }, - rightIcon: { - marginTop: "4px", - marginRight: "10px", - color: theme.palette.text.secondary, - }, - desenList: { - paddingTop: 0, - paddingBottom: 0, - }, - iconFix: { - marginRight: "11px", - marginLeft: "7px", - minWidth: 40, - }, - flexContainer: { - display: "flex", - }, -})); - -export default function Authn(props) { - const { t } = useTranslation(); - const [selected, setSelected] = useState(""); - const [confirm, setConfirm] = useState(false); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const deleteCredential = (id) => { - API.patch("/user/setting/authn", { - id: id, - }) - .then(() => { - ToggleSnackbar( - "top", - "right", - t("setting.authenticatorRemoved"), - "success" - ); - props.remove(id); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }) - .then(() => { - setConfirm(false); - }); - }; - - const classes = useStyles(); - - const addCredential = () => { - if (!navigator.credentials) { - ToggleSnackbar( - "top", - "right", - t("setting.browserNotSupported"), - "warning" - ); - - return; - } - API.put("/user/authn", {}) - .then((response) => { - const credentialCreationOptions = response.data; - credentialCreationOptions.publicKey.challenge = bufferDecode( - credentialCreationOptions.publicKey.challenge - ); - credentialCreationOptions.publicKey.user.id = bufferDecode( - credentialCreationOptions.publicKey.user.id - ); - if (credentialCreationOptions.publicKey.excludeCredentials) { - for ( - let i = 0; - i < - credentialCreationOptions.publicKey.excludeCredentials - .length; - i++ - ) { - credentialCreationOptions.publicKey.excludeCredentials[ - i - ].id = bufferDecode( - credentialCreationOptions.publicKey - .excludeCredentials[i].id - ); - } - } - - return navigator.credentials.create({ - publicKey: credentialCreationOptions.publicKey, - }); - }) - .then((credential) => { - const attestationObject = credential.response.attestationObject; - const clientDataJSON = credential.response.clientDataJSON; - const rawId = credential.rawId; - return API.put( - "/user/authn/finish", - JSON.stringify({ - id: credential.id, - rawId: bufferEncode(rawId), - type: credential.type, - response: { - attestationObject: bufferEncode(attestationObject), - clientDataJSON: bufferEncode(clientDataJSON), - }, - }) - ); - }) - .then((response) => { - props.add(response.data); - ToggleSnackbar( - "top", - "right", - t("setting.authenticatorAdded"), - "success" - ); - return; - }) - .catch((error) => { - console.log(error); - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - return ( -
- setConfirm(false)}> - {t("setting.removedAuthenticator")} - - {t("setting.removedAuthenticatorConfirm")} - - - - - - - - - {t("setting.hardwareAuthenticator")} - - - - {props.list.map((v) => ( - <> - { - setConfirm(true); - setSelected(v.id); - }} - > - - - - - - deleteCredential(v.id)} - className={classes.flexContainer} - > - - - - - - ))} - addCredential()}> - - - - - - - - - - - -
- ); -} diff --git a/src/component/Setting/Profile.js b/src/component/Setting/Profile.js deleted file mode 100644 index 81b253d..0000000 --- a/src/component/Setting/Profile.js +++ /dev/null @@ -1,443 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import API from "../../middleware/Api"; - -import { - Avatar, - Grid, - Paper, - Tab, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tabs, - Typography, - withStyles, -} from "@material-ui/core"; -import { withRouter } from "react-router"; -import Pagination from "@material-ui/lab/Pagination"; -import { formatLocalTime } from "../../utils/datetime"; -import { toggleSnackbar } from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const styles = (theme) => ({ - layout: { - width: "auto", - marginTop: "50px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - marginBottom: "30px", - [theme.breakpoints.up("sm")]: { - width: 700, - marginLeft: "auto", - marginRight: "auto", - }, - }, - userNav: { - borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0 0`, - height: "270px", - backgroundColor: theme.palette.primary.main, - padding: "20px 20px 2em", - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='" + - theme.palette.primary.light.replace("#", "%23") + - "' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.dark.replace("#", "%23") + - "' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.light.replace("#", "%23") + - "' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.dark.replace("#", "%23") + - "' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='" + - theme.palette.primary.main.replace("#", "%23") + - "' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.light.replace("#", "%23") + - "' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.main.replace("#", "%23") + - "' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='" + - theme.palette.secondary.dark.replace("#", "%23") + - "' points='943 900 1210 900 971 687'/%3E%3C/svg%3E\")", - backgroundSize: "cover", - backgroundPosition: "bottom", - }, - avatarContainer: { - height: "80px", - width: "80px", - borderRaidus: "50%", - margin: "auto", - marginTop: "50px", - boxShadow: - "0 2px 5px 0 rgba(0,0,0,0.16), 0 2px 10px 0 rgba(0,0,0,0.12)", - border: "2px solid #fff", - }, - nickName: { - width: "200px", - margin: "auto", - textAlign: "center", - marginTop: "1px", - fontSize: "25px", - color: "#ffffff", - opacity: "0.81", - }, - th: { - minWidth: "106px", - }, - mobileHide: { - [theme.breakpoints.down("md")]: { - display: "none", - }, - }, - tableLink: { - cursor: "pointer", - }, - navigator: { - padding: theme.spacing(2), - }, - pageInfo: { - marginTop: "14px", - marginLeft: "23px", - }, - infoItem: { - paddingLeft: "46px!important", - paddingBottom: "20px!important", - }, - infoContainer: { - marginTop: "30px", - }, - tableContainer: { - overflowX: "auto", - }, -}); -const mapStateToProps = () => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -class ProfileCompoment extends Component { - state = { - listType: 0, - shareList: [], - page: 1, - user: null, - total: 0, - }; - - handleChange = (event, listType) => { - this.setState({ listType }); - if (listType === 1) { - this.loadList(1, "hot"); - } else if (listType === 0) { - this.loadList(1, "default"); - } - }; - - componentDidMount = () => { - this.loadList(1, "default"); - }; - - loadList = (page, order) => { - API.get( - "/user/profile/" + - this.props.match.params.id + - "?page=" + - page + - "&type=" + - order - ) - .then((response) => { - this.setState({ - shareList: response.data.items, - user: response.data.user, - total: response.data.total, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - loadNext = () => { - this.loadList( - this.state.page + 1, - this.state.listType === 0 ? "default" : "hot" - ); - }; - - loadPrev = () => { - this.loadList( - this.state.page - 1, - this.state.listType === 0 ? "default" : "hot" - ); - }; - - render() { - const { classes, t } = this.props; - - return ( -
- {this.state.user === null &&
} - {this.state.user !== null && ( - -
-
- -
-
- - {this.state.user.nick} - -
-
- - - - - - {this.state.listType === 2 && ( -
- - - - {t("setting.uid")} - - - {this.state.user.id} - - - - - {t("setting.nickname")} - - - {this.state.user.nick} - - - - - {t("setting.group")} - - - {this.state.user.group} - - - - - {t("setting.totalShares")} - - - {this.state.total} - - - - - {t("setting.regTime")} - - - {this.state.user.date} - - - -
- )} - {(this.state.listType === 0 || - this.state.listType === 1) && ( -
-
- - - - - {t("setting.fileName")} - - - {t("setting.shareDate")} - - - {t( - "setting.downloadNumber" - )} - - - {t("setting.viewNumber")} - - - - - {this.state.shareList.map( - (row, id) => ( - - this.props.history.push( - "/s/" + row.key - ) - } - > - - - {row.source - ? row.source - .name - : "[" + - t( - "share.expired" - ) + - "]"} - - - - {formatLocalTime( - row.create_date - )} - - - {row.downloads} - - - {row.views} - - - ) - )} - -
-
- {this.state.shareList.length !== 0 && - this.state.listType === 0 && ( -
- - this.loadList( - v, - this.state.listType === - 0 - ? "default" - : "hot" - ) - } - color="secondary" - /> -
- )} -
- )} -
- )} -
- ); - } -} - -const Profile = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(ProfileCompoment)))); - -export default Profile; diff --git a/src/component/Setting/Tasks.js b/src/component/Setting/Tasks.js deleted file mode 100644 index 8aedf34..0000000 --- a/src/component/Setting/Tasks.js +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles, Typography } from "@material-ui/core"; -import { useDispatch } from "react-redux"; -import Paper from "@material-ui/core/Paper"; -import Table from "@material-ui/core/Table"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TableCell from "@material-ui/core/TableCell"; -import TableBody from "@material-ui/core/TableBody"; -import API from "../../middleware/Api"; -import { getTaskProgress, getTaskStatus, getTaskType } from "../../config"; -import Pagination from "@material-ui/lab/Pagination"; -import { formatLocalTime } from "../../utils/datetime"; -import { toggleSnackbar } from "../../redux/explorer"; -import Nothing from "../Placeholder/Nothing"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "50px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: "50px", - }, - content: { - marginTop: theme.spacing(4), - overflowX: "auto", - }, - cardContent: { - padding: theme.spacing(2), - }, - tableContainer: { - overflowX: "auto", - }, - create: { - marginTop: theme.spacing(2), - }, - noWrap: { - wordBreak: "keepAll", - }, - footer: { - padding: theme.spacing(2), - }, -})); - -export default function Tasks() { - const { t } = useTranslation(); - const [tasks, setTasks] = useState([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const loadList = (page) => { - API.get("/user/setting/tasks?page=" + page) - .then((response) => { - setTasks(response.data.tasks); - setTotal(response.data.total); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - useEffect(() => { - loadList(page); - // eslint-disable-next-line - }, [page]); - - const getError = (error) => { - if (error === "") { - return "-"; - } - try { - const res = JSON.parse(error); - return `${res.msg}: ${res.error}`; - } catch (e) { - return t("uploader.unknownStatus"); - } - }; - - const classes = useStyles(); - - return ( -
- - {t("navbar.taskQueue")} - - - - - - - {t("setting.createdAt")} - - - {t("setting.taskType")} - - - {t("setting.taskStatus")} - - - {t("setting.lastProgress")} - - - {t("setting.errorDetails")} - - - - - {tasks.map((row, id) => ( - - - {formatLocalTime(row.create_date)} - - - {getTaskType(row.type)} - - - {getTaskStatus(row.status)} - - - {getTaskProgress(row.type, row.progress)} - - - {getError(row.error)} - - - ))} - -
- {tasks.length === 0 && ( - - )} -
- setPage(v)} - color="secondary" - /> -
-
-
- ); -} diff --git a/src/component/Setting/UserSetting.js b/src/component/Setting/UserSetting.js deleted file mode 100644 index d89c93b..0000000 --- a/src/component/Setting/UserSetting.js +++ /dev/null @@ -1,1409 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import PhotoIcon from "@material-ui/icons/InsertPhoto"; -import GroupIcon from "@material-ui/icons/Group"; -import DateIcon from "@material-ui/icons/DateRange"; -import EmailIcon from "@material-ui/icons/Email"; -import HomeIcon from "@material-ui/icons/Home"; -import LinkIcon from "@material-ui/icons/Phonelink"; -import InputIcon from "@material-ui/icons/Input"; -import SecurityIcon from "@material-ui/icons/Security"; -import NickIcon from "@material-ui/icons/PermContactCalendar"; -import LockIcon from "@material-ui/icons/Lock"; -import VerifyIcon from "@material-ui/icons/VpnKey"; -import ColorIcon from "@material-ui/icons/Palette"; -import axios from "axios"; -import FingerprintIcon from "@material-ui/icons/Fingerprint"; -import ToggleButton from "@material-ui/lab/ToggleButton"; -import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup"; -import RightIcon from "@material-ui/icons/KeyboardArrowRight"; -import { - ListItemIcon, - withStyles, - Button, - Divider, - TextField, - Avatar, - Paper, - Typography, - List, - ListItem, - ListItemSecondaryAction, - ListItemText, - ListItemAvatar, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Switch, -} from "@material-ui/core"; -import { blue, green, yellow } from "@material-ui/core/colors"; -import API from "../../middleware/Api"; -import Auth from "../../middleware/Auth"; -import { withRouter } from "react-router"; -import { QRCodeSVG } from "qrcode.react"; -import { - Brightness3, - GitHub, - Home, - ListAlt, - PermContactCalendar, - Schedule, - Translate, -} from "@material-ui/icons"; -import Authn from "./Authn"; -import { formatLocalTime, timeZone } from "../../utils/datetime"; -import TimeZoneDialog from "../Modals/TimeZone"; -import { - applyThemes, - changeViewMethod, - toggleDaylightMode, - toggleSnackbar, -} from "../../redux/explorer"; -import { Trans, withTranslation } from "react-i18next"; -import { selectLanguage } from "../../redux/viewUpdate/action"; -import OptionSelector from "../Modals/OptionSelector"; - -const styles = (theme) => ({ - layout: { - width: "auto", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 700, - marginLeft: "auto", - marginRight: "auto", - }, - }, - sectionTitle: { - paddingBottom: "10px", - paddingTop: "30px", - }, - rightIcon: { - marginTop: "4px", - marginRight: "10px", - color: theme.palette.text.secondary, - }, - uploadFromFile: { - backgroundColor: blue[100], - color: blue[600], - }, - userGravatar: { - backgroundColor: yellow[100], - color: yellow[800], - }, - policySelected: { - backgroundColor: green[100], - color: green[800], - }, - infoText: { - marginRight: "17px", - [theme.breakpoints.down("xs")]: { - maxWidth: 100, - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - }, - }, - infoTextWithIcon: { - marginRight: "17px", - marginTop: "1px", - [theme.breakpoints.down("xs")]: { - maxWidth: 100, - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - }, - }, - rightIconWithText: { - marginTop: "0px", - marginRight: "10px", - color: theme.palette.text.secondary, - }, - iconFix: { - marginRight: "11px", - marginLeft: "7px", - minWidth: 40, - }, - flexContainer: { - display: "flex", - }, - desenList: { - paddingTop: 0, - paddingBottom: 0, - }, - flexContainerResponse: { - display: "flex", - [theme.breakpoints.down("sm")]: { - display: "initial", - }, - }, - desText: { - marginTop: "10px", - }, - secondColor: { - height: "20px", - width: "20px", - backgroundColor: theme.palette.secondary.main, - borderRadius: "50%", - marginRight: "17px", - }, - firstColor: { - height: "20px", - width: "20px", - backgroundColor: theme.palette.primary.main, - borderRadius: "50%", - marginRight: "6px", - }, - themeBlock: { - height: "20px", - width: "20px", - }, - paddingBottom: { - marginBottom: "30px", - }, - paddingText: { - paddingRight: theme.spacing(2), - }, - qrcode: { - width: 128, - marginTop: 16, - marginRight: 16, - }, -}); - -const mapStateToProps = (state) => { - return { - title: state.siteConfig.title, - authn: state.siteConfig.authn, - viewMethod: state.viewUpdate.explorerViewMethod, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - applyThemes: (color) => { - dispatch(applyThemes(color)); - }, - toggleDaylightMode: () => { - dispatch(toggleDaylightMode()); - }, - changeView: (method) => { - dispatch(changeViewMethod(method)); - }, - selectLanguage: () => { - dispatch(selectLanguage()); - }, - }; -}; - -class UserSettingCompoment extends Component { - constructor(props) { - super(props); - this.fileInput = React.createRef(); - } - - state = { - avatarModal: false, - nickModal: false, - changePassword: false, - loading: "", - oldPwd: "", - newPwd: "", - webdavPwd: "", - newPwdRepeat: "", - twoFactor: false, - authCode: "", - changeTheme: false, - chosenTheme: null, - showWebDavUrl: false, - showWebDavUserName: false, - changeWebDavPwd: false, - groupBackModal: false, - changePolicy: false, - changeTimeZone: false, - settings: { - uid: 0, - group_expires: 0, - policy: { - current: { - name: "-", - id: "", - }, - options: [], - }, - qq: "", - homepage: true, - two_factor: "", - two_fa_secret: "", - prefer_theme: "", - themes: {}, - authn: [], - }, - }; - - handleClose = () => { - this.setState({ - avatarModal: false, - nickModal: false, - changePassword: false, - loading: "", - twoFactor: false, - changeTheme: false, - showWebDavUrl: false, - showWebDavUserName: false, - changeWebDavPwd: false, - groupBackModal: false, - changePolicy: false, - }); - }; - - componentDidMount() { - this.loadSetting(); - } - - toggleViewMethod = () => { - const newMethod = - this.props.viewMethod === "icon" - ? "list" - : this.props.viewMethod === "list" - ? "smallIcon" - : "icon"; - Auth.SetPreference("view_method", newMethod); - this.props.changeView(newMethod); - }; - - loadSetting = () => { - API.get("/user/setting") - .then((response) => { - const theme = JSON.parse(response.data.themes); - response.data.themes = theme; - this.setState({ - settings: response.data, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - useGravatar = () => { - this.setState({ - loading: "gravatar", - }); - API.put("/user/setting/avatar") - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.avatarUpdated"), - "success" - ); - this.setState({ - loading: "", - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - changeNick = () => { - this.setState({ - loading: "nick", - }); - API.patch("/user/setting/nick", { - nick: this.state.nick, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.nickChanged"), - "success" - ); - this.setState({ - loading: "", - }); - this.handleClose(); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - uploadAvatar = () => { - this.setState({ - loading: "avatar", - }); - const formData = new FormData(); - formData.append("avatar", this.fileInput.current.files[0]); - API.post("/user/setting/avatar", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.avatarUpdated"), - "success" - ); - this.setState({ - loading: "", - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - handleToggle = () => { - API.patch("/user/setting/homepage", { - status: !this.state.settings.homepage, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.settingSaved"), - "success" - ); - this.setState({ - settings: { - ...this.state.settings, - homepage: !this.state.settings.homepage, - }, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - changhePwd = () => { - if (this.state.newPwd !== this.state.newPwdRepeat) { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("login.passwordNotMatch"), - "warning" - ); - return; - } - this.setState({ - loading: "changePassword", - }); - API.patch("/user/setting/password", { - old: this.state.oldPwd, - new: this.state.newPwd, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("login.passwordReset"), - "success" - ); - this.setState({ - loading: "", - }); - this.handleClose(); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - changeTheme = () => { - this.setState({ - loading: "changeTheme", - }); - API.patch("/user/setting/theme", { - theme: this.state.chosenTheme, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.themeColorChanged"), - "success" - ); - this.props.applyThemes(this.state.chosenTheme); - this.setState({ - loading: "", - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - changheWebdavPwd = () => { - this.setState({ - loading: "changheWebdavPwd", - }); - axios - .post("/Member/setWebdavPwd", { - pwd: this.state.webdavPwd, - }) - .then((response) => { - if (response.data.error === "1") { - this.props.toggleSnackbar( - "top", - "right", - response.data.msg, - "error" - ); - this.setState({ - loading: "", - }); - } else { - this.props.toggleSnackbar( - "top", - "right", - response.data.msg, - "success" - ); - this.setState({ - loading: "", - changeWebDavPwd: false, - }); - } - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - init2FA = () => { - if (this.state.settings.two_factor) { - this.setState({ twoFactor: true }); - return; - } - API.get("/user/setting/2fa") - .then((response) => { - this.setState({ - two_fa_secret: response.data, - twoFactor: true, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - twoFactor = () => { - this.setState({ - loading: "twoFactor", - }); - API.patch("/user/setting/2fa", { - code: this.state.authCode, - }) - .then(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("setting.settingSaved"), - "success" - ); - this.setState({ - loading: "", - settings: { - ...this.state.settings, - two_factor: !this.state.settings.two_factor, - }, - }); - this.handleClose(); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - this.setState({ - loading: "", - }); - }); - }; - - handleChange = (name) => (event) => { - this.setState({ [name]: event.target.value }); - }; - - handleAlignment = (event, chosenTheme) => this.setState({ chosenTheme }); - - toggleThemeMode = (current) => { - const newMode = - current === null ? "light" : current === "light" ? "dark" : null; - this.props.toggleDaylightMode(); - Auth.SetPreference("theme_mode", newMode); - }; - - render() { - const { classes, t } = this.props; - const user = Auth.GetUser(); - const dark = Auth.GetPreference("theme_mode"); - - return ( -
-
- - {t("setting.profile")} - - - - - this.setState({ avatarModal: true }) - } - > - - - - - - - - - - - - - - - - - - {this.state.settings.uid} - - - - - - this.setState({ nickModal: true }) - } - > - - - - - - - this.setState({ nickModal: true }) - } - className={classes.flexContainer} - > - - {user.nickname} - - - - - - - - - - - - - - {user.user_name} - - - - - - - - - - - - - {user.group.name} - - - - - - - - - - - - - {formatLocalTime(user.created_at)} - - - - - - - {t("setting.privacyAndSecurity")} - - - - - - - - - - - - - - - - this.setState({ changePassword: true }) - } - > - - - - - - - - - - - this.init2FA()}> - - - - - - - - {!this.state.settings.two_factor - ? t("setting.disabled") - : t("setting.enabled")} - - - - - - - - { - this.setState({ - settings: { - ...this.state.settings, - authn: [ - ...this.state.settings.authn, - credential, - ], - }, - }); - }} - remove={(id) => { - let credentials = [...this.state.settings.authn]; - credentials = credentials.filter((v) => { - return v.id !== id; - }); - this.setState({ - settings: { - ...this.state.settings, - authn: credentials, - }, - }); - }} - /> - - - {t("setting.appearance")} - - - - - this.setState({ changeTheme: true }) - } - > - - - - - - -
-
- - - - this.toggleThemeMode(dark)} - > - - - - - - - - {dark && - (dark === "dark" - ? t("setting.enabled") - : t("setting.disabled"))} - {dark === null && - t("setting.syncWithSystem")} - - - - - - this.toggleViewMethod()} - > - - - - - - - - {this.props.viewMethod === "icon" && - t("fileManager.gridViewLarge")} - {this.props.viewMethod === "list" && - t("fileManager.listView")} - {this.props.viewMethod === - "smallIcon" && - t("fileManager.gridViewSmall")} - - - - - - - this.setState({ changeTimeZone: true }) - } - button - > - - - - - - - - {timeZone} - - - - - - this.props.selectLanguage()} - button - > - - - - - - - - - - - - {user.group.webdav && ( -
- - WebDAV - - - - - this.setState({ - showWebDavUrl: true, - }) - } - > - - - - - - - - - - - - this.setState({ - showWebDavUserName: true, - }) - } - > - - - - - - - - - - - - this.props.history.push("/webdav?") - } - > - - - - - - - - - - - -
- )} - - - {t("setting.aboutCloudreve")} - - - - - window.open( - "https://github.com/cloudreve/cloudreve" - ) - } - > - - - - - - - - - - - - window.open("https://cloudreve.org") - } - > - - - - - - - - - - - - -
-
- this.setState({ changeTimeZone: false })} - open={this.state.changeTimeZone} - /> - - {t("setting.avatar")} - - - - - - - - - - - - - - - - - - - - - - - - - {t("setting.changeNick")} - - - - - - - - - - {t("login.resetPassword")} - -
- -
-
- -
-
- -
-
- - - - -
- - - {this.state.settings.two_factor - ? t("setting.disable2FA") - : t("setting.enable2FA")} - - -
- {!this.state.settings.two_factor && ( -
- - )} - {this.state.settings.two_factor && ( - - {t("setting.inputCurrent2FACode")} - - )} - -
-
-
- - - - -
- - {t("setting.themeColor")} - - - {Object.keys(this.state.settings.themes).map( - (value, key) => ( - -
- - ) - )} - - - - - - -
- - {t("setting.webdavServer")} - - - - - - - - - {t("setting.userName")} - - - - - - - - -
- ); - } -} - -const UserSetting = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(UserSettingCompoment)))); - -export default UserSetting; diff --git a/src/component/Setting/WebDAV.js b/src/component/Setting/WebDAV.js deleted file mode 100644 index fb52df3..0000000 --- a/src/component/Setting/WebDAV.js +++ /dev/null @@ -1,387 +0,0 @@ -import React, { useState, useCallback, useEffect } from "react"; -import { makeStyles, Typography } from "@material-ui/core"; -import { useDispatch, useSelector } from "react-redux"; -import Paper from "@material-ui/core/Paper"; -import Tabs from "@material-ui/core/Tabs"; -import Tab from "@material-ui/core/Tab"; -import Button from "@material-ui/core/Button"; -import TableContainer from "@material-ui/core/TableContainer"; -import Table from "@material-ui/core/Table"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TableCell from "@material-ui/core/TableCell"; -import TableBody from "@material-ui/core/TableBody"; -import Alert from "@material-ui/lab/Alert"; -import Auth from "../../middleware/Auth"; -import API from "../../middleware/Api"; -import IconButton from "@material-ui/core/IconButton"; -import { Cloud, CloudOff, Delete } from "@material-ui/icons"; -import CreateWebDAVAccount from "../Modals/CreateWebDAVAccount"; -import TimeAgo from "timeago-react"; -import Link from "@material-ui/core/Link"; -import { toggleSnackbar } from "../../redux/explorer"; -import Nothing from "../Placeholder/Nothing"; -import { useTranslation } from "react-i18next"; -import AppPromotion from "./AppPromotion"; -import Tooltip from "@material-ui/core/Tooltip"; -import ToggleIcon from "material-ui-toggle-icon"; -import { Pencil, PencilOff } from "mdi-material-ui"; - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "50px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: "50px", - }, - content: { - marginTop: theme.spacing(4), - }, - cardContent: { - padding: theme.spacing(2), - }, - tableContainer: { - overflowX: "auto", - }, - create: { - marginTop: theme.spacing(2), - }, - copy: { - marginLeft: 10, - }, -})); - -export default function WebDAV() { - const { t } = useTranslation(); - const [tab, setTab] = useState(0); - const [create, setCreate] = useState(false); - const [accounts, setAccounts] = useState([]); - - const appPromotion = useSelector((state) => state.siteConfig.app_promotion); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const copyToClipboard = (text) => { - if (navigator.clipboard) { - navigator.clipboard.writeText(text); - ToggleSnackbar("top", "center", t("setting.copied"), "success"); - } else { - ToggleSnackbar( - "top", - "center", - t("setting.pleaseManuallyCopy"), - "warning" - ); - } - }; - - const loadList = () => { - API.get("/webdav/accounts") - .then((response) => { - setAccounts(response.data.accounts); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - useEffect(() => { - loadList(); - // eslint-disable-next-line - }, []); - - const deleteAccount = (id) => { - const account = accounts[id]; - API.delete("/webdav/accounts/" + account.ID) - .then(() => { - let accountCopy = [...accounts]; - accountCopy = accountCopy.filter((v, i) => { - return i !== id; - }); - setAccounts(accountCopy); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const toggleAccountReadonly = (id) => { - const account = accounts[id]; - API.patch("/webdav/accounts", { - id: account.ID, - readonly: !account.Readonly, - }) - .then((response) => { - account.Readonly = response.data.readonly; - const accountCopy = [...accounts]; - setAccounts(accountCopy); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const toggleAccountUseProxy = (id) => { - const account = accounts[id]; - API.patch("/webdav/accounts", { - id: account.ID, - use_proxy: !account.UseProxy, - }) - .then((response) => { - account.UseProxy = response.data.use_proxy; - const accountCopy = [...accounts]; - setAccounts(accountCopy); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const addAccount = (account) => { - setCreate(false); - API.post("/webdav/accounts", { - path: account.path, - name: account.name, - }) - .then((response) => { - setAccounts([ - { - ID: response.data.id, - Password: response.data.password, - CreatedAt: response.data.created_at, - Name: account.name, - Root: account.path, - Readonly: account.Readonly, - }, - ...accounts, - ]); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const classes = useStyles(); - const user = Auth.GetUser(); - - return ( -
- setCreate(false)} - /> - - {t("navbar.connect")} - - - setTab(newValue)} - aria-label="disabled tabs example" - > - - {appPromotion && } - -
- {tab === 0 && ( -
- - {t("setting.webdavHint", { - url: window.location.origin + "/dav", - name: user.user_name, - })} - - - - - - - {t("setting.annotation")} - - - {t("login.password")} - - - {t("setting.rootFolder")} - - - {t("setting.createdAt")} - - - {t("setting.action")} - - - - - {accounts.map((row, id) => ( - - - {row.Name} - - - {row.Password} - - copyToClipboard( - row.Password - ) - } - href={"javascript:void"} - > - {t("copyToClipboard", { - ns: "common", - })} - - - - {row.Root} - - - - - - - toggleAccountReadonly( - id - ) - } - > - - - } - offIcon={ - - } - /> - - - {user.group.allowWebDAVProxy && ( - toggleAccountUseProxy( - id - ) - } - > - - - } - offIcon={ - - } - /> - - )} - - deleteAccount(id) - } - > - - - - - - - ))} - -
- {accounts.length === 0 && ( - - )} -
- -
- )} - {tab === 1 && } -
-
-
- ); -} diff --git a/src/component/Share/Creator.js b/src/component/Share/Creator.js deleted file mode 100644 index 900f8f4..0000000 --- a/src/component/Share/Creator.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import { Avatar, Typography } from "@material-ui/core"; -import { useHistory } from "react-router"; -import Link from "@material-ui/core/Link"; -import { formatLocalTime } from "../../utils/datetime"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - boxHeader: { - textAlign: "center", - padding: 24, - }, - avatar: { - backgroundColor: theme.palette.secondary.main, - margin: "0 auto", - width: 50, - height: 50, - cursor: "pointer", - }, - shareDes: { - marginTop: 12, - }, - shareInfo: { - color: theme.palette.text.disabled, - fontSize: 14, - }, -})); - -export default function Creator(props) { - const { t } = useTranslation("application", { keyPrefix: "share" }); - const classes = useStyles(); - const history = useHistory(); - - const getSecondDes = () => { - if (props.share.expire > 0) { - if (props.share.expire >= 24 * 3600) { - return t("expireInXDays", { - num: Math.round(props.share.expire / (24 * 3600)), - }); - } - - return t("expireInXHours", { - num: Math.round(props.share.expire / 3600), - }); - } - return formatLocalTime(props.share.create_date); - }; - - const userProfile = () => { - history.push("/profile/" + props.share.creator.key); - props.onClose && props.onClose(); - }; - - return ( -
- userProfile()} - /> - - {props.isFolder && ( - userProfile()} - href={"javascript:void(0)"} - color="inherit" - />, - ]} - /> - )} - {!props.isFolder && ( - userProfile()} - href={"javascript:void(0)"} - color="inherit" - />, - ]} - /> - )} - - - {t("statistics", { - views: props.share.views, - downloads: props.share.downloads, - time: getSecondDes(), - })} - -
- ); -} diff --git a/src/component/Share/LockedFile.js b/src/component/Share/LockedFile.js deleted file mode 100644 index b8a1f2e..0000000 --- a/src/component/Share/LockedFile.js +++ /dev/null @@ -1,145 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; - -import { - Avatar, - Button, - Card, - CardActions, - CardContent, - CardHeader, - Divider, - TextField, - withStyles, -} from "@material-ui/core"; -import { withRouter } from "react-router"; -import { formatLocalTime } from "../../utils/datetime"; -import { toggleSnackbar } from "../../redux/explorer"; -import { withTranslation } from "react-i18next"; - -const styles = (theme) => ({ - card: { - maxWidth: 400, - margin: "0 auto", - }, - actions: { - display: "flex", - }, - layout: { - width: "auto", - marginTop: "110px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - }, - continue: { - marginLeft: "auto", - marginRight: "10px", - marginRottom: "10px", - }, -}); -const mapStateToProps = () => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -class LockedFileCompoment extends Component { - constructor(props) { - super(props); - const query = new URLSearchParams(this.props.location.search); - this.state = { - pwd: query.get("password"), - }; - } - - handleChange = (name) => (event) => { - this.setState({ [name]: event.target.value }); - }; - - submit = (e) => { - e.preventDefault(); - if (this.state.pwd === "") { - return; - } - this.props.setPassowrd(this.state.pwd); - }; - - render() { - const { classes, t } = this.props; - - return ( -
- - - } - title={t("share.privateShareTitle", { - nick: this.props.share.creator.nick, - })} - subheader={formatLocalTime( - this.props.share.create_date - )} - /> - - -
- - -
- - - -
-
- ); - } -} - -const LockedFile = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(LockedFileCompoment)))); - -export default LockedFile; diff --git a/src/component/Share/MyShare.js b/src/component/Share/MyShare.js deleted file mode 100644 index 5b0da66..0000000 --- a/src/component/Share/MyShare.js +++ /dev/null @@ -1,515 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import OpenIcon from "@material-ui/icons/OpenInNew"; -import Pagination from "@material-ui/lab/Pagination"; -import FolderIcon from "@material-ui/icons/Folder"; -import LockIcon from "@material-ui/icons/Lock"; -import UnlockIcon from "@material-ui/icons/LockOpen"; -import EyeIcon from "@material-ui/icons/RemoveRedEye"; -import DeleteIcon from "@material-ui/icons/Delete"; - -import { - Avatar, - Button, - Card, - CardActions, - CardHeader, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, - IconButton, - TextField, - Tooltip, - Typography, - withStyles, -} from "@material-ui/core"; -import API from "../../middleware/Api"; -import TypeIcon from "../FileManager/TypeIcon"; -import Chip from "@material-ui/core/Chip"; -import Divider from "@material-ui/core/Divider"; -import { VisibilityOff, VpnKey } from "@material-ui/icons"; -import Select from "@material-ui/core/Select"; -import MenuItem from "@material-ui/core/MenuItem"; -import FormControl from "@material-ui/core/FormControl"; -import { withRouter } from "react-router-dom"; -import ToggleIcon from "material-ui-toggle-icon"; -import { formatLocalTime } from "../../utils/datetime"; -import { toggleSnackbar } from "../../redux/explorer"; -import Nothing from "../Placeholder/Nothing"; -import { withTranslation } from "react-i18next"; - -const styles = (theme) => ({ - cardContainer: { - padding: theme.spacing(1), - }, - card: { - maxWidth: 400, - margin: "0 auto", - }, - actions: { - display: "flex", - }, - layout: { - width: "auto", - marginTop: "50px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - }, - shareTitle: { - maxWidth: "200px", - }, - avatarFile: { - backgroundColor: theme.palette.primary.light, - }, - avatarFolder: { - backgroundColor: theme.palette.secondary.light, - }, - gird: { - marginTop: "30px", - }, - loadMore: { - textAlign: "right", - marginTop: "20px", - marginBottom: "40px", - }, - badge: { - marginLeft: theme.spacing(1), - height: 17, - }, - orderSelect: { - textAlign: "right", - marginTop: 5, - }, -}); -const mapStateToProps = () => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - }; -}; - -class MyShareCompoment extends Component { - state = { - page: 1, - total: 0, - shareList: [], - showPwd: null, - orderBy: "created_at DESC", - }; - - componentDidMount = () => { - this.loadList(1, this.state.orderBy); - }; - - showPwd = (pwd) => { - this.setState({ showPwd: pwd }); - }; - - handleClose = () => { - this.setState({ showPwd: null }); - }; - - removeShare = (id) => { - API.delete("/share/" + id) - .then(() => { - let oldList = this.state.shareList; - oldList = oldList.filter((value) => { - return value.key !== id; - }); - this.setState({ - shareList: oldList, - total: this.state.total - 1, - }); - this.props.toggleSnackbar( - "top", - "right", - this.props.t("share.shareCanceled"), - "success" - ); - if (oldList.length === 0) { - this.loadList(1, this.state.orderBy); - } - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - changePermission = (id) => { - const newPwd = Math.random().toString(36).substr(2).slice(2, 8); - const oldList = this.state.shareList; - const shareIndex = oldList.findIndex((value) => { - return value.key === id; - }); - API.patch("/share/" + id, { - prop: "password", - value: oldList[shareIndex].password === "" ? newPwd : "", - }) - .then((response) => { - oldList[shareIndex].password = response.data; - this.setState({ - shareList: oldList, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - changePreviewOption = (id) => { - const oldList = this.state.shareList; - const shareIndex = oldList.findIndex((value) => { - return value.key === id; - }); - API.patch("/share/" + id, { - prop: "preview_enabled", - value: oldList[shareIndex].preview ? "false" : "true", - }) - .then((response) => { - oldList[shareIndex].preview = response.data; - this.setState({ - shareList: oldList, - }); - }) - .catch((error) => { - this.props.toggleSnackbar( - "top", - "right", - error.message, - "error" - ); - }); - }; - - loadList = (page, orderBy) => { - const order = orderBy.split(" "); - API.get( - "/share?page=" + - page + - "&order_by=" + - order[0] + - "&order=" + - order[1] - ) - .then((response) => { - this.setState({ - total: response.data.total, - shareList: response.data.items, - }); - }) - .catch(() => { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("share.listLoadingError"), - "error" - ); - }); - }; - - handlePageChange = (event, value) => { - this.setState({ - page: value, - }); - this.loadList(value, this.state.orderBy); - }; - - handleOrderChange = (event) => { - this.setState({ - orderBy: event.target.value, - }); - this.loadList(this.state.page, event.target.value); - }; - - isExpired = (share) => { - return share.expire < -1 || share.remain_downloads === 0; - }; - - render() { - const { classes, t } = this.props; - - return ( -
- - - - {t("share.sharedFiles")} - - - - - - - - - - {this.state.shareList.length === 0 && ( - - )} - {this.state.shareList.map((value) => ( - - - - {!value.is_dir && ( - - )}{" "} - {value.is_dir && ( - - - - )} -
- } - title={ - - - {value.source - ? value.source.name - : t("share.sourceNotFound")} - - - } - subheader={ - - {formatLocalTime(value.create_date)} - {this.isExpired(value) && ( - - )} - - } - /> - - - - - this.props.history.push( - "/s/" + - value.key + - (value.password === "" - ? "" - : "?password=" + - value.password) - ) - } - > - - - {" "} - {value.password !== "" && ( - <> - - this.changePermission( - value.key - ) - } - > - - - - - - this.showPwd(value.password) - } - > - - - - - - )} - {value.password === "" && ( - - this.changePermission(value.key) - } - > - - - - - )} - - this.changePreviewOption(value.key) - } - > - - - } - offIcon={ - - } - /> - - - - this.removeShare(value.key) - } - > - - - - - - - - ))} - -
- -
{" "} - - {t("share.sharePassword")} {" "} - - - {" "} - - {" "} - {" "} - {" "} -
- ); - } -} - -const MyShare = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(MyShareCompoment)))); - -export default MyShare; diff --git a/src/component/Share/NotFound.js b/src/component/Share/NotFound.js deleted file mode 100644 index 0ce52ae..0000000 --- a/src/component/Share/NotFound.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import SentimentVeryDissatisfiedIcon from "@material-ui/icons/SentimentVeryDissatisfied"; -import { lighten, makeStyles } from "@material-ui/core/styles"; - -const useStyles = makeStyles((theme) => ({ - icon: { - fontSize: "160px", - }, - emptyContainer: { - bottom: "0", - height: "300px", - margin: "50px auto", - width: "300px", - color: lighten(theme.palette.text.disabled, 0.4), - textAlign: "center", - paddingTop: "20px", - }, - emptyInfoBig: { - fontSize: "25px", - color: lighten(theme.palette.text.disabled, 0.4), - }, -})); - -export default function Notice(props) { - const classes = useStyles(); - return ( -
- -
{props.msg}
-
- ); -} diff --git a/src/component/Share/ReadMe.js b/src/component/Share/ReadMe.js deleted file mode 100644 index d4b0d12..0000000 --- a/src/component/Share/ReadMe.js +++ /dev/null @@ -1,143 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; -import { MenuBook } from "@material-ui/icons"; -import { Typography } from "@material-ui/core"; -import Divider from "@material-ui/core/Divider"; -import Paper from "@material-ui/core/Paper"; -import TextLoading from "../Placeholder/TextLoading"; -import API from "../../middleware/Api"; -import { useDispatch } from "react-redux"; -import Editor from "for-editor"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - readMeContainer: { - marginTop: 30, - [theme.breakpoints.down("sm")]: { - marginTop: theme.spacing(2), - }, - }, - readMeHeader: { - padding: "10px 16px", - display: "flex", - color: theme.palette.text.secondary, - }, - readMeIcon: { - marginRight: 8, - }, - content: {}, - "@global": { - //如果嵌套主题,则应该定位[class * =“MuiButton-root”]。 - ".for-container": { - border: "none!important", - }, - ".for-container .for-editor .for-editor-edit": { - height: "0!important", - }, - ".for-container > div:first-child": { - borderTopLeftRadius: "0!important", - borderTopRightRadius: "0!important", - }, - ".for-container .for-editor .for-panel .for-preview": { - backgroundColor: theme.palette.background.paper + "!important", - color: theme.palette.text.primary + "!important", - }, - ".for-container .for-markdown-preview pre": { - backgroundColor: theme.palette.background.default + "!important", - color: - theme.palette.type === "dark" - ? "#fff !important" - : "rgba(0, 0, 0, 0.87);!important", - }, - - ".for-container .for-markdown-preview code": { - backgroundColor: theme.palette.background.default + "!important", - }, - ".for-container .for-markdown-preview a": { - color: - theme.palette.type === "dark" - ? "#67aeff !important" - : "#0366d6 !important", - }, - ".for-container .for-markdown-preview table th": { - backgroundColor: theme.palette.background.default + "!important", - }, - }, -})); - -export default function ReadMe(props) { - const { t } = useTranslation(); - const classes = useStyles(); - const theme = useTheme(); - - const [loading, setLoading] = useState(true); - const [content, setContent] = useState(""); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const $vm = React.createRef(); - - useEffect(() => { - setLoading(true); - const previewPath = - props.file.path === "/" - ? props.file.path + props.file.name - : props.file.path + "/" + props.file.name; - API.get( - "/share/readme/" + - props.share.key + - "?path=" + - encodeURIComponent(previewPath) - ) - .then((response) => { - setContent(response.rawData.toString()); - }) - .catch((error) => { - ToggleSnackbar( - "top", - "right", - t("share.readmeError", { msg: error.message }), - "error" - ); - }) - .then(() => { - setLoading(false); - }); - // eslint-disable-next-line - }, [props.share, props.file]); - - return ( - -
- - {props.file.name} -
- - -
- {loading && } - {!loading && ( - setContent(value)} - preview - toolbar={{}} - /> - )} -
-
- ); -} diff --git a/src/component/Share/SearchResult.js b/src/component/Share/SearchResult.js deleted file mode 100644 index 1605a00..0000000 --- a/src/component/Share/SearchResult.js +++ /dev/null @@ -1,287 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import OpenIcon from "@material-ui/icons/OpenInNew"; -import Pagination from "@material-ui/lab/Pagination"; -import FolderIcon from "@material-ui/icons/Folder"; - -import { - Avatar, - Card, - CardHeader, - Grid, - IconButton, - Tooltip, - Typography, -} from "@material-ui/core"; -import API from "../../middleware/Api"; -import TypeIcon from "../FileManager/TypeIcon"; -import Select from "@material-ui/core/Select"; -import MenuItem from "@material-ui/core/MenuItem"; -import FormControl from "@material-ui/core/FormControl"; -import { useHistory } from "react-router-dom"; -import { makeStyles } from "@material-ui/core/styles"; -import { useLocation } from "react-router"; -import TimeAgo from "timeago-react"; -import { toggleSnackbar } from "../../redux/explorer"; -import Nothing from "../Placeholder/Nothing"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - cardContainer: { - padding: theme.spacing(1), - }, - card: { - maxWidth: 400, - margin: "0 auto", - }, - actions: { - display: "flex", - }, - layout: { - width: "auto", - marginTop: "50px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - }, - shareTitle: { - maxWidth: "200px", - }, - avatarFile: { - backgroundColor: theme.palette.primary.light, - }, - avatarFolder: { - backgroundColor: theme.palette.secondary.light, - }, - gird: { - marginTop: "30px", - }, - loadMore: { - textAlign: "right", - marginTop: "20px", - marginBottom: "40px", - }, - badge: { - marginLeft: theme.spacing(1), - height: 17, - }, - orderSelect: { - textAlign: "right", - marginTop: 5, - }, - cardAction: { - marginTop: 0, - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -export default function SearchResult() { - const { t } = useTranslation("application", { keyPrefix: "share" }); - const { t: tGlobal } = useTranslation(); - const classes = useStyles(); - const dispatch = useDispatch(); - - const query = useQuery(); - const location = useLocation(); - const history = useHistory(); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); - const [shareList, setShareList] = useState([]); - const [orderBy, setOrderBy] = useState("created_at DESC"); - - const search = useCallback((keywords, page, orderBy) => { - const order = orderBy.split(" "); - API.get( - "/share/search?page=" + - page + - "&order_by=" + - order[0] + - "&order=" + - order[1] + - "&keywords=" + - encodeURIComponent(keywords) - ) - .then((response) => { - setTotal(response.data.total); - setShareList(response.data.items); - }) - .catch(() => { - ToggleSnackbar("top", "right", t("listLoadingError"), "error"); - }); - }, []); - - useEffect(() => { - const keywords = query.get("keywords"); - if (keywords) { - search(keywords, page, orderBy); - } else { - ToggleSnackbar("top", "right", t("enterKeywords"), "warning"); - } - }, [location]); - - const handlePageChange = (event, value) => { - setPage(value); - const keywords = query.get("keywords"); - search(keywords, value, orderBy); - }; - - const handleOrderChange = (event) => { - setOrderBy(event.target.value); - const keywords = query.get("keywords"); - search(keywords, page, event.target.value); - }; - - return ( -
- - - - {t("searchResult")} - - - - - - - - - - {shareList.length === 0 && } - {shareList.map((value) => ( - - - - {!value.is_dir && ( - - )}{" "} - {value.is_dir && ( - - - - )} -
- } - action={ - - - history.push("/s/" + value.key) - } - > - - - - } - title={ - - - {value.source - ? value.source.name - : t("sourceNotFound")} - - - } - subheader={ - - , - ]} - /> - - } - /> - - - ))} - -
- -
{" "} -
- ); -} diff --git a/src/component/Share/SharePreload.js b/src/component/Share/SharePreload.js deleted file mode 100644 index 8c7c644..0000000 --- a/src/component/Share/SharePreload.js +++ /dev/null @@ -1,103 +0,0 @@ -import React, { Suspense, useCallback, useEffect, useState } from "react"; -import PageLoading from "../Placeholder/PageLoading"; -import { useParams } from "react-router"; -import API from "../../middleware/Api"; -import { changeSubTitle } from "../../redux/viewUpdate/action"; -import { useDispatch } from "react-redux"; -import Notice from "./NotFound"; -import LockedFile from "./LockedFile"; -import SharedFile from "./SharedFile"; -import SharedFolder from "./SharedFolder"; -import { toggleSnackbar } from "../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -export default function SharePreload() { - const { t } = useTranslation("application", { keyPrefix: "share" }); - const dispatch = useDispatch(); - const { id } = useParams(); - - const [share, setShare] = useState(undefined); - const [loading, setLoading] = useState(false); - const [password, setPassword] = useState(""); - - const SetSubTitle = useCallback( - (title) => dispatch(changeSubTitle(title)), - [dispatch] - ); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - if (share) { - if (share.locked) { - SetSubTitle( - t("privateShareTitle", { nick: share.creator.nick }) - ); - if (password !== "") { - ToggleSnackbar( - "top", - "right", - t("incorrectPassword"), - "warning" - ); - } - } else { - SetSubTitle(share.source.name); - } - } else { - SetSubTitle(); - } - }, [share, SetSubTitle, ToggleSnackbar]); - - useEffect(() => { - return () => { - SetSubTitle(); - }; - // eslint-disable-next-line - }, []); - - useEffect(() => { - setLoading(true); - let withPassword = ""; - if (password !== "") { - withPassword = "?password=" + password; - } - API.get("/share/info/" + id + withPassword) - .then((response) => { - setShare(response.data); - setLoading(false); - }) - .catch((error) => { - setLoading(false); - if (error.code === 404) { - setShare(null); - } else { - ToggleSnackbar("top", "right", error.message, "error"); - } - }); - }, [id, password, ToggleSnackbar]); - - return ( - }> - {share === undefined && } - {share === null && } - {share && share.locked && ( - - )} - {share && !share.locked && !share.is_dir && ( - - )} - {share && !share.locked && share.is_dir && ( - - )} - - ); -} diff --git a/src/component/Share/SharedFile.js b/src/component/Share/SharedFile.js deleted file mode 100644 index d0e677c..0000000 --- a/src/component/Share/SharedFile.js +++ /dev/null @@ -1,295 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { sizeToString, vhCheck } from "../../utils"; -import { isPreviewable } from "../../config"; -import { Button, Typography, withStyles } from "@material-ui/core"; -import Divider from "@material-ui/core/Divider"; -import TypeIcon from "../FileManager/TypeIcon"; -import Auth from "../../middleware/Auth"; -import { withRouter } from "react-router-dom"; -import Creator from "./Creator"; -import pathHelper from "../../utils/page"; -import { - openMusicDialog, - openResaveDialog, - setSelectedTarget, - showAudioPreview, - showImgPreivew, - toggleSnackbar, -} from "../../redux/explorer"; -import { startDownload } from "../../redux/explorer/action"; -import { withTranslation } from "react-i18next"; - -vhCheck(); -const styles = (theme) => ({ - layout: { - width: "auto", - marginTop: "90px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginTop: "90px", - marginLeft: "auto", - marginRight: "auto", - }, - [theme.breakpoints.down("sm")]: { - marginTop: 0, - marginLeft: 0, - marginRight: 0, - }, - justifyContent: "center", - display: "flex", - }, - player: { - borderRadius: "4px", - }, - fileCotainer: { - width: "200px", - margin: "0 auto", - }, - buttonCotainer: { - width: "400px", - margin: "0 auto", - textAlign: "center", - marginTop: "20px", - }, - paper: { - padding: theme.spacing(2), - }, - icon: { - borderRadius: "10%", - marginTop: 2, - }, - - box: { - width: "100%", - maxWidth: 440, - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius, - boxShadow: "0 8px 16px rgba(29,39,55,.25)", - [theme.breakpoints.down("sm")]: { - height: "calc(var(--vh, 100vh) - 56px)", - borderRadius: 0, - maxWidth: 1000, - }, - display: "flex", - flexDirection: "column", - }, - boxContent: { - padding: 24, - display: "flex", - flex: "1", - }, - fileName: { - marginLeft: 20, - }, - fileSize: { - color: theme.palette.text.disabled, - fontSize: 14, - }, - boxFooter: { - display: "flex", - padding: "20px 16px", - justifyContent: "space-between", - }, - downloadButton: { - marginLeft: 8, - }, -}); -const mapStateToProps = () => { - return {}; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - openMusicDialog: (first) => { - dispatch(showAudioPreview(first)); - }, - setSelectedTarget: (targets) => { - dispatch(setSelectedTarget(targets)); - }, - showImgPreivew: (first) => { - dispatch(showImgPreivew(first)); - }, - openResave: (key) => { - dispatch(openResaveDialog(key)); - }, - startDownload: (share, file) => { - dispatch(startDownload(share, file)); - }, - }; -}; - -const Modals = React.lazy(() => import("../FileManager/Modals")); -const ImgPreview = React.lazy(() => import("../FileManager/ImgPreview")); - -class SharedFileCompoment extends Component { - state = { - anchorEl: null, - open: false, - purchaseCallback: null, - loading: false, - }; - - downloaded = false; - - // TODO merge into react thunk - preview = () => { - if (pathHelper.isSharePage(this.props.location.pathname)) { - const user = Auth.GetUser(); - if (!Auth.Check() && user && !user.group.shareDownload) { - this.props.toggleSnackbar( - "top", - "right", - this.props.t("share.pleaseLogin"), - "warning" - ); - return; - } - } - - switch (isPreviewable(this.props.share.source.name)) { - case "img": - this.props.showImgPreivew({ - key: this.props.share.key, - name: this.props.share.source.name, - }); - return; - case "msDoc": - this.props.history.push( - this.props.share.key + - "/doc?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - case "audio": - this.props.openMusicDialog({ - key: this.props.share.key, - name: this.props.share.source.name, - type: "share", - }); - return; - case "video": - this.props.history.push( - this.props.share.key + - "/video?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - case "edit": - this.props.history.push( - this.props.share.key + - "/text?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - case "pdf": - this.props.history.push( - this.props.share.key + - "/pdf?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - case "code": - this.props.history.push( - this.props.share.key + - "/code?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - case "epub": - this.props.history.push( - this.props.share.key + - "/epub?name=" + - encodeURIComponent(this.props.share.source.name) - ); - return; - default: - this.props.toggleSnackbar( - "top", - "right", - this.props.t("share.cannotShare"), - "warning" - ); - return; - } - }; - - componentWillUnmount() { - this.props.setSelectedTarget([]); - } - - scoreHandle = (callback) => (event) => { - callback(event); - }; - - download = () => { - this.props.startDownload(this.props.share, null); - }; - - render() { - const { classes, t } = this.props; - return ( -
- - -
- - -
- -
- - {this.props.share.source.name} - - - {sizeToString(this.props.share.source.size)} - -
-
- -
-
- {this.props.share.preview && ( - - )} -
-
- -
-
-
-
- ); - } -} - -const SharedFile = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(withTranslation()(SharedFileCompoment)))); - -export default SharedFile; diff --git a/src/component/Share/SharedFolder.js b/src/component/Share/SharedFolder.js deleted file mode 100644 index 3316740..0000000 --- a/src/component/Share/SharedFolder.js +++ /dev/null @@ -1,153 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { Typography, withStyles } from "@material-ui/core"; -import { withRouter } from "react-router-dom"; -import FileManager from "../FileManager/FileManager"; -import Paper from "@material-ui/core/Paper"; -import Popover from "@material-ui/core/Popover"; -import Creator from "./Creator"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; -import pathHelper from "../../utils/page"; -import { - openMusicDialog, - openResaveDialog, - setSelectedTarget, - setShareUserPopover, - showImgPreivew, - toggleSnackbar, -} from "../../redux/explorer"; -import { setShareInfo } from "../../redux/viewUpdate/action"; - -const styles = (theme) => ({ - layout: { - width: "auto", - marginTop: 30, - marginBottom: 30, - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - [theme.breakpoints.down("sm")]: { - marginTop: theme.spacing(2), - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - }, - }, - managerContainer: { - overflowY: "auto", - }, -}); - -const ReadMe = React.lazy(() => import("./ReadMe")); - -const mapStateToProps = (state) => { - return { - anchorEl: state.viewUpdate.shareUserPopoverAnchorEl, - fileList: state.explorer.fileList, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - toggleSnackbar: (vertical, horizontal, msg, color) => { - dispatch(toggleSnackbar(vertical, horizontal, msg, color)); - }, - openMusicDialog: () => { - dispatch(openMusicDialog()); - }, - setSelectedTarget: (targets) => { - dispatch(setSelectedTarget(targets)); - }, - showImgPreivew: (first) => { - dispatch(showImgPreivew(first)); - }, - openResave: (key) => { - dispatch(openResaveDialog(key)); - }, - setShareUserPopover: (e) => { - dispatch(setShareUserPopover(e)); - }, - setShareInfo: (s) => { - dispatch(setShareInfo(s)); - }, - }; -}; - -class SharedFolderComponent extends Component { - state = {}; - - UNSAFE_componentWillMount() { - this.props.setShareInfo(this.props.share); - } - - componentWillUnmount() { - this.props.setShareInfo(null); - this.props.setSelectedTarget([]); - } - - handleClickAway = (e) => { - const ignore = e && e.clientY && e.clientY <= 64; - if (!pathHelper.isMobile() && !ignore) { - this.props.setSelectedTarget([]); - } - }; - - render() { - const { classes } = this.props; - let readmeShowed = false; - const id = this.props.anchorEl !== null ? "simple-popover" : undefined; - - return ( -
- - - - - - {/* eslint-disable-next-line */} - {this.props.fileList.map((value) => { - if ( - (value.name.toLowerCase() === "readme.md" || - value.name.toLowerCase() === "readme.txt") && - !readmeShowed - ) { - readmeShowed = true; - return ; - } - })} - this.props.setShareUserPopover(null)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - transformOrigin={{ - vertical: "top", - horizontal: "center", - }} - > - - this.props.setShareUserPopover(null)} - share={this.props.share} - /> - - -
- ); - } -} - -const SharedFolder = connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(withRouter(SharedFolderComponent))); - -export default SharedFolder; diff --git a/src/component/Uploader/DropFile.tsx b/src/component/Uploader/DropFile.tsx new file mode 100644 index 0000000..246b67d --- /dev/null +++ b/src/component/Uploader/DropFile.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import Backdrop from "@mui/material/Backdrop"; +import { Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import CloudArrowIUp from "../Icons/CloudArrowIUp.tsx"; + +export function DropFileBackground({ open }: { open: boolean }) { + const { t } = useTranslation(); + return ( + theme.zIndex.drawer + 1, + color: "#fff", + flexDirection: "column", + }} + open={open} + > +
+ +
+
+ {t("uploader.dropFileHere")} +
+
+ ); +} diff --git a/src/component/Uploader/PasteUploadDialog.tsx b/src/component/Uploader/PasteUploadDialog.tsx new file mode 100644 index 0000000..2a72b4b --- /dev/null +++ b/src/component/Uploader/PasteUploadDialog.tsx @@ -0,0 +1,94 @@ +import React, { useCallback, useEffect } from "react"; +import { Box, DialogContent, styled, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import DraggableDialog from "../Dialogs/DraggableDialog.tsx"; +import { setUploadFromClipboardDialog } from "../../redux/globalStateSlice.ts"; +import Clipboard from "../Icons/Clipboard.tsx"; + +export interface PasteUploadDialogProps { + onFilePasted: (files: File[]) => void; +} + +export interface PasteTargetProps extends PasteUploadDialogProps { + onClose: () => void; +} + +const PasteTargetContainer = styled(Box)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + backgroundColor: + theme.palette.mode == "light" + ? "rgba(0, 0, 0, 0.06)" + : "rgba(255, 255, 255, 0.09)", + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", + padding: theme.spacing(2), + color: theme.palette.text.secondary, +})); + +const PasteTarget = ({ onFilePasted, onClose }: PasteTargetProps) => { + const { t } = useTranslation(); + const pasteHandler = (e: ClipboardEvent) => { + e.preventDefault(); + if (!e.clipboardData?.files.length) { + return; + } + + onFilePasted(Array.from(e.clipboardData.files)); + onClose(); + }; + + useEffect(() => { + // listen to onpaste event for window + window.addEventListener("paste", pasteHandler); + return () => { + window.removeEventListener("paste", pasteHandler); + }; + }, []); + + const disableDefault = useCallback( + (e: React.MouseEvent | React.KeyboardEvent | React.ClipboardEvent) => { + e.preventDefault(); + return false; + }, + [], + ); + return ( + + + {t("uploader.pasteFilesHere")} + + ); +}; + +export default function PasteUploadDialog({ + onFilePasted, +}: PasteUploadDialogProps) { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const open = useAppSelector( + (state) => state.globalState.uploadFromClipboardDialogOpen, + ); + + const onClose = useCallback(() => { + dispatch(setUploadFromClipboardDialog(false)); + }, []); + + return ( + + + + + + ); +} diff --git a/src/component/Uploader/Popup/ConcurrentOptionDialog.tsx b/src/component/Uploader/Popup/ConcurrentOptionDialog.tsx new file mode 100644 index 0000000..e73f8b9 --- /dev/null +++ b/src/component/Uploader/Popup/ConcurrentOptionDialog.tsx @@ -0,0 +1,60 @@ +import React, { useCallback, useState } from "react"; +import { DialogContent, FilledInput, InputLabel } from "@mui/material"; +import FormControl from "@mui/material/FormControl"; +import { useTranslation } from "react-i18next"; +import SessionManager, { UserSettings } from "../../../session"; +import DraggableDialog from "../../Dialogs/DraggableDialog.tsx"; + +export interface ConcurrentOptionDialogProps { + open: boolean; + onClose: () => void; + onSave: (count: string) => void; +} + +export default function ConcurrentOptionDialog({ + open, + onClose, + onSave, +}: ConcurrentOptionDialogProps) { + const { t } = useTranslation(); + const [count, setCount] = useState( + SessionManager.getWithFallback(UserSettings.ConcurrentLimit), + ); + + const onAccept = useCallback(() => { + onSave(count); + }, [count]); + + return ( + + + + + {t("uploader.concurrentTaskNumber")} + + setCount(e.target.value)} + /> + + + + ); +} diff --git a/src/component/Uploader/Popup/MoreActions.js b/src/component/Uploader/Popup/MoreActions.js deleted file mode 100644 index 13b5089..0000000 --- a/src/component/Uploader/Popup/MoreActions.js +++ /dev/null @@ -1,210 +0,0 @@ -import { - Icon, - ListItemIcon, - makeStyles, - Menu, - MenuItem, - Tooltip, -} from "@material-ui/core"; -import React, { useCallback, useMemo, useState } from "react"; -import { useDispatch } from "react-redux"; -import API from "../../../middleware/Api"; -import { TaskType } from "../core/types"; -import { refreshStorage, toggleSnackbar } from "../../../redux/explorer"; -import Divider from "@material-ui/core/Divider"; -import CheckIcon from "@material-ui/icons/Check"; -import { DeleteEmpty } from "mdi-material-ui"; -import DeleteIcon from "@material-ui/icons/Delete"; -import ConcurrentOptionDialog from "../../Modals/ConcurrentOption"; -import Auth from "../../../middleware/Auth"; -import { ClearAll, Replay } from "@material-ui/icons"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - icon: { - minWidth: 38, - }, -})); - -export default function MoreActions({ - anchorEl, - onClose, - uploadManager, - deleteTask, - useAvgSpeed, - setUseAvgSpeed, - filter, - setFilter, - sorter, - setSorter, - cleanFinished, - retryFailed, -}) { - const { t } = useTranslation("application", { keyPrefix: "uploader" }); - const classes = useStyles(); - const dispatch = useDispatch(); - const [concurrentDialog, setConcurrentDialog] = useState(false); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const RefreshStorage = useCallback( - () => dispatch(refreshStorage()), - [dispatch] - ); - - const actionClicked = (next) => () => { - onClose(); - next(); - }; - - const cleanupSessions = () => { - uploadManager.cleanupSessions(); - API.delete("/file/upload") - .then((response) => { - if (response.rawData.code === 0) { - ToggleSnackbar( - "top", - "right", - t("uploadSessionCleaned"), - "success" - ); - } else { - ToggleSnackbar( - "top", - "right", - response.rawData.msg, - "warning" - ); - } - deleteTask((u) => u.task.type !== TaskType.resumeHint); - RefreshStorage(); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const open = Boolean(anchorEl); - const id = open ? "uploader-action-popover" : undefined; - - const listItems = useMemo( - () => [ - { - tooltip: t("hideCompletedTooltip"), - onClick: () => - setFilter(filter === "default" ? "ongoing" : "default"), - icon: filter !== "default" ? : , - text: t("hideCompleted"), - divider: true, - }, - { - tooltip: t("addTimeAscTooltip"), - onClick: () => setSorter("default"), - icon: sorter === "default" ? : , - text: t("addTimeAsc"), - divider: false, - }, - { - tooltip: t("addTimeDescTooltip"), - onClick: () => setSorter("reverse"), - icon: sorter === "reverse" ? : , - text: t("addTimeDesc"), - divider: true, - }, - { - tooltip: t("showInstantSpeedTooltip"), - onClick: () => setUseAvgSpeed(false), - icon: useAvgSpeed ? : , - text: t("showInstantSpeed"), - divider: false, - }, - { - tooltip: t("showAvgSpeedTooltip"), - onClick: () => setUseAvgSpeed(true), - icon: !useAvgSpeed ? : , - text: t("showAvgSpeed"), - divider: true, - }, - { - tooltip: t("cleanAllSessionTooltip"), - onClick: () => cleanupSessions(), - icon: , - text: t("cleanAllSession"), - divider: false, - }, - { - tooltip: t("cleanCompletedTooltip"), - onClick: () => cleanFinished(), - icon: , - text: t("cleanCompleted"), - divider: false, - }, - { - tooltip: t("retryFailedTasksTooltip"), - onClick: () => retryFailed(), - icon: , - text: t("retryFailedTasks"), - divider: true, - }, - { - tooltip: t("setConcurrentTooltip"), - onClick: () => setConcurrentDialog(true), - icon: , - text: t("setConcurrent"), - divider: false, - }, - ], - [ - useAvgSpeed, - setUseAvgSpeed, - sorter, - setSorter, - filter, - setFilter, - cleanFinished, - ] - ); - - const onConcurrentLimitSave = (val) => { - val = parseInt(val); - if (val > 0) { - Auth.SetPreference("concurrent_limit", val); - uploadManager.changeConcurrentLimit(parseInt(val)); - } - setConcurrentDialog(false); - }; - - return ( - <> - - {listItems.map((item) => ( - <> - - - - {item.icon} - - {item.text} - - - {item.divider && } - - ))} - - setConcurrentDialog(false)} - onSave={onConcurrentLimitSave} - /> - - ); -} diff --git a/src/component/Uploader/Popup/MoreActions.tsx b/src/component/Uploader/Popup/MoreActions.tsx new file mode 100644 index 0000000..312d052 --- /dev/null +++ b/src/component/Uploader/Popup/MoreActions.tsx @@ -0,0 +1,166 @@ +import { Icon, ListItemIcon, Menu, MenuItem, Tooltip } from "@mui/material"; +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DenseDivider } from "../../FileManager/ContextMenu/ContextMenu.tsx"; +import Checkmark from "../../Icons/Checkmark.tsx"; +import TextGrammarLighting from "../../Icons/TextGrammarLighting.tsx"; +import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; +import ArrowClockwise from "../../Icons/ArrowClockwise.tsx"; +import ConcurrentOptionDialog from "./ConcurrentOptionDialog.tsx"; +import SessionManager, { UserSettings } from "../../../session"; +import UploadManager from "../core"; + +export interface MoreActionsProps { + anchorEl: null | HTMLElement; + onClose: () => void; + uploadManager: UploadManager; + useAvgSpeed: boolean; + setUseAvgSpeed: (val: boolean) => void; + filter: string; + setFilter: (val: string) => void; + sorter: string; + setSorter: (val: string) => void; + cleanFinished: () => void; + retryFailed: () => void; +} + +export default function MoreActions({ + anchorEl, + onClose, + uploadManager, + useAvgSpeed, + setUseAvgSpeed, + filter, + setFilter, + sorter, + setSorter, + cleanFinished, + retryFailed, +}: MoreActionsProps) { + const { t } = useTranslation("application", { keyPrefix: "uploader" }); + const [concurrentDialog, setConcurrentDialog] = useState(false); + const [overwrite, setOverwrite] = useState( + SessionManager.getWithFallback(UserSettings.UploadOverwrite), + ); + const actionClicked = (next: () => void) => () => { + onClose(); + next(); + }; + + const toggleOverwrite = (val: boolean) => { + SessionManager.set(UserSettings.UploadOverwrite, val); + uploadManager.overwrite = val; + setOverwrite(val); + }; + + const open = Boolean(anchorEl); + const id = open ? "uploader-action-popover" : undefined; + + const listItems = useMemo( + () => [ + { + tooltip: t("overwriteTooltip"), + onClick: () => toggleOverwrite(!overwrite), + icon: overwrite ? : , + text: t("overwrite"), + divider: true, + }, + { + tooltip: t("hideCompletedTooltip"), + onClick: () => setFilter(filter === "default" ? "ongoing" : "default"), + icon: filter !== "default" ? : , + text: t("hideCompleted"), + divider: true, + }, + { + tooltip: t("addTimeAscTooltip"), + onClick: () => setSorter("default"), + icon: sorter === "default" ? : , + text: t("addTimeAsc"), + divider: false, + }, + { + tooltip: t("addTimeDescTooltip"), + onClick: () => setSorter("reverse"), + icon: sorter === "reverse" ? : , + text: t("addTimeDesc"), + divider: true, + }, + { + tooltip: t("showInstantSpeedTooltip"), + onClick: () => setUseAvgSpeed(false), + icon: useAvgSpeed ? : , + text: t("showInstantSpeed"), + divider: false, + }, + { + tooltip: t("showAvgSpeedTooltip"), + onClick: () => setUseAvgSpeed(true), + icon: !useAvgSpeed ? : , + text: t("showAvgSpeed"), + divider: true, + }, + { + tooltip: t("cleanCompletedTooltip"), + onClick: () => cleanFinished(), + icon: , + text: t("cleanCompleted"), + divider: false, + }, + { + tooltip: t("retryFailedTasksTooltip"), + onClick: () => retryFailed(), + icon: , + text: t("retryFailedTasks"), + divider: true, + }, + { + tooltip: t("setConcurrentTooltip"), + onClick: () => setConcurrentDialog(true), + icon: , + text: t("setConcurrent"), + divider: false, + }, + ], + [ + useAvgSpeed, + setUseAvgSpeed, + sorter, + setSorter, + filter, + setFilter, + cleanFinished, + overwrite, + ], + ); + + const onConcurrentLimitSave = (val: string) => { + const valNum = parseInt(val); + if (valNum > 0) { + SessionManager.set(UserSettings.ConcurrentLimit, valNum); + uploadManager.changeConcurrentLimit(valNum); + } + setConcurrentDialog(false); + }; + + return ( + <> + + {listItems.map((item) => [ + + + {item.icon} + {item.text} + + , + item.divider && , + ])} + + setConcurrentDialog(false)} + onSave={onConcurrentLimitSave} + /> + + ); +} diff --git a/src/component/Uploader/Popup/TaskDetail.js b/src/component/Uploader/Popup/TaskDetail.js deleted file mode 100644 index db92912..0000000 --- a/src/component/Uploader/Popup/TaskDetail.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core"; -import Grid from "@material-ui/core/Grid"; -import { sizeToString } from "../../../utils"; -import Link from "@material-ui/core/Link"; -import TimeAgo from "timeago-react"; -import { Status } from "../core/uploader/base"; -import { Trans, useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - infoTitle: { - fontWeight: 700, - }, - infoValue: { - color: theme.palette.text.secondary, - wordBreak: "break-all", - }, -})); - -export default function TaskDetail({ uploader, navigateToDst, error }) { - const { t } = useTranslation(); - const classes = useStyles(); - const items = [ - { - name: t("uploader.fileName"), - value: uploader.task.name, - }, - { - name: t("uploader.fileSize"), - value: `${sizeToString(uploader.task.size)} ${ - uploader.task.session && uploader.task.session.chunkSize > 0 - ? t("uploader.chunkDescription", { - total: uploader.task.chunkProgress.length, - size: sizeToString(uploader.task.session.chunkSize), - }) - : t("uploader.noChunks") - }`, - }, - { - name: t("fileManager.storagePolicy"), - value: uploader.task.policy.name, - }, - { - name: t("uploader.destination"), - value: ( - navigateToDst(uploader.task.dst)} - > - {uploader.task.dst === "/" - ? t("uploader.rootFolder") - : uploader.task.dst} - - ), - }, - uploader.task.session - ? { - name: t("uploader.uploadSession"), - value: ( - <> - , - ]} - /> - - ), - } - : null, - uploader.status === Status.error - ? { - name: t("uploader.errorDetails"), - value: error, - } - : null, - ]; - return ( - - {items.map((i) => ( - <> - {i && ( - - - {i.name} - - - {i.value} - - - )} - - ))} - - ); -} diff --git a/src/component/Uploader/Popup/TaskDetail.tsx b/src/component/Uploader/Popup/TaskDetail.tsx new file mode 100644 index 0000000..90e705b --- /dev/null +++ b/src/component/Uploader/Popup/TaskDetail.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import Grid from "@mui/material/Grid"; +import TimeAgo from "timeago-react"; +import Base, { Status } from "../core/uploader/base"; +import { Trans, useTranslation } from "react-i18next"; +import { sizeToString } from "../../../util"; +import FileBadge from "../../FileManager/FileBadge.tsx"; +import { FileType } from "../../../api/explorer.ts"; + +export interface TaskDetailProps { + uploader: Base; + error: string; +} + +export default function TaskDetail({ uploader, error }: TaskDetailProps) { + const { t } = useTranslation(); + const items = [ + { + name: t("uploader.fileName"), + value: uploader.task.name, + }, + { + name: t("uploader.fileSize"), + value: `${sizeToString(uploader.task.size)} ${ + uploader.task.session && uploader.task.session.chunk_size > 0 + ? t("uploader.chunkDescription", { + total: uploader.task.chunkProgress.length, + size: sizeToString(uploader.task.session.chunk_size), + }) + : t("uploader.noChunks") + }`, + }, + { + name: t("uploader.storagePolicy"), + value: uploader.task.policy.name, + }, + { + name: t("uploader.destination"), + value: ( + + ), + }, + uploader.task.session + ? { + name: t("uploader.uploadSession"), + value: ( + <> + , + ]} + /> + + ), + } + : null, + uploader.status === Status.error + ? { + name: t("uploader.errorDetails"), + value: error, + } + : null, + ]; + return ( + theme.typography.body2.fontSize }} + > + {items.map((i) => ( + <> + {i && ( + + + {i.name} + + theme.palette.text.secondary, + wordBreak: "break-all", + }} + > + {i.value} + + + )} + + ))} + + ); +} diff --git a/src/component/Uploader/Popup/TaskList.js b/src/component/Uploader/Popup/TaskList.js deleted file mode 100644 index ac7a00b..0000000 --- a/src/component/Uploader/Popup/TaskList.js +++ /dev/null @@ -1,359 +0,0 @@ -import React, { useMemo, useState } from "react"; -import { - Accordion, - AccordionDetails, - AppBar, - Dialog, - DialogContent, - Fade, - IconButton, - makeStyles, - Slide, - Toolbar, - Tooltip, - Typography, -} from "@material-ui/core"; -import { useTheme } from "@material-ui/core/styles"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import CloseIcon from "@material-ui/icons/Close"; -import ExpandMoreIcon from "@material-ui/icons/ExpandLess"; -import AddIcon from "@material-ui/icons/Add"; -import classnames from "classnames"; -import UploadTask from "./UploadTask"; -import { MoreHoriz } from "@material-ui/icons"; -import MoreActions from "./MoreActions"; -import { useSelector } from "react-redux"; -import { Virtuoso } from "react-virtuoso"; -import Nothing from "../../Placeholder/Nothing"; -import { lighten } from "@material-ui/core/styles/colorManipulator"; -import { Status } from "../core/uploader/base"; -import Auth from "../../../middleware/Auth"; -import { useTranslation } from "react-i18next"; -import { useUpload } from "../UseUpload"; - -const Transition = React.forwardRef(function Transition(props, ref) { - return ; -}); - -const useStyles = makeStyles((theme) => ({ - rootOverwrite: { - top: "auto!important", - left: "auto!important", - }, - appBar: { - position: "relative", - }, - flex: { - flex: 1, - }, - popup: { - alignItems: "flex-end", - justifyContent: "flex-end", - }, - dialog: { - margin: 0, - right: 10, - bottom: 10, - zIndex: 9999, - position: "fixed", - inset: "-1!important", - }, - paddingZero: { - padding: 0, - }, - dialogContent: { - [theme.breakpoints.up("md")]: { - width: 500, - minHeight: 300, - maxHeight: "calc(100vh - 140px)", - }, - padding: 0, - paddingTop: "0!important", - }, - virtualList: { - height: "100%", - maxHeight: "calc(100vh - 56px)", - [theme.breakpoints.up("md")]: { - minHeight: 300, - maxHeight: "calc(100vh - 140px)", - }, - }, - expandIcon: { - transform: "rotate(0deg)", - transition: theme.transitions.create("transform", { - duration: theme.transitions.duration.shortest, - }), - }, - expandIconExpanded: { - transform: "rotate(180deg)", - }, - toolbar: { - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), - }, - progress: { - transition: "width .4s linear", - zIndex: -1, - height: "100%", - position: "absolute", - left: 0, - top: 0, - }, -})); - -const sorters = { - default: (a, b) => a.id - b.id, - reverse: (a, b) => b.id - a.id, -}; - -const filters = { - default: (u) => true, - ongoing: (u) => u.status < Status.finished, -}; - -export default function TaskList({ - open, - onClose, - selectFile, - taskList, - onCancel, - uploadManager, - progress, - setUploaders, -}) { - const { t } = useTranslation("application", { keyPrefix: "uploader" }); - const classes = useStyles(); - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const path = useSelector((state) => state.navigator.path); - const [expanded, setExpanded] = useState(true); - const [useAvgSpeed, setUseAvgSpeed] = useState( - Auth.GetPreferenceWithDefault("use_avg_speed", true) - ); - const [anchorEl, setAnchorEl] = useState(null); - const [filter, setFilter] = useState( - Auth.GetPreferenceWithDefault("task_filter", "default") - ); - const [sorter, setSorter] = useState( - Auth.GetPreferenceWithDefault("task_sorter", "default") - ); - const [refreshList, setRefreshList] = useState(false); - - const handleActionClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleActionClose = () => { - setAnchorEl(null); - }; - - const close = (e, reason) => { - if (reason !== "backdropClick") { - onClose(); - } else { - setExpanded(false); - } - }; - const handlePanelChange = (event, isExpanded) => { - setExpanded(isExpanded); - }; - - useMemo(() => { - if (open) { - setExpanded(true); - } - }, [taskList]); - - const progressBar = useMemo( - () => - progress.totalSize > 0 ? ( - 0 && !expanded}> -
-
-
- - ) : null, - [progress, expanded, classes, theme] - ); - - const list = useMemo(() => { - const currentList = taskList - .filter(filters[filter]) - .sort(sorters[sorter]); - if (currentList.length === 0) { - return ; - } - - return ( - ( - setRefreshList((r) => !r)} - /> - )} - /> - ); - }, [ - classes, - taskList, - useAvgSpeed, - fullScreen, - filter, - sorter, - refreshList, - ]); - - const retryFailed = () => { - taskList.forEach((task) => { - if (task.status === Status.error) { - task.reset(); - task.start(); - } - }); - }; - - return ( - <> - { - Auth.SetPreference("use_avg_speed", v); - setUseAvgSpeed(v); - }} - filter={filter} - sorter={sorter} - setFilter={(v) => { - Auth.SetPreference("task_filter", v); - setFilter(v); - }} - setSorter={(v) => { - Auth.SetPreference("task_sorter", v); - setSorter(v); - }} - retryFailed={retryFailed} - cleanFinished={() => - setUploaders((u) => u.filter(filters["ongoing"])) - } - /> - - - - {progressBar} - - - - - - - - {t("uploadTasks")} - - - - - - - - selectFile(path)} - > - - - - {!fullScreen && ( - - setExpanded(!expanded)} - > - - - - )} - - - - - {list} - - - - - - ); -} diff --git a/src/component/Uploader/Popup/TaskList.tsx b/src/component/Uploader/Popup/TaskList.tsx new file mode 100644 index 0000000..560391c --- /dev/null +++ b/src/component/Uploader/Popup/TaskList.tsx @@ -0,0 +1,350 @@ +import React, { useMemo, useState } from "react"; +import { + Accordion, + AccordionDetails, + alpha, + AppBar, + Box, + Dialog, + DialogContent, + Fade, + IconButton, + Slide, + SlideProps, + styled, + Toolbar, + Tooltip, + Typography, +} from "@mui/material"; +import { lighten, useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import UploadTask from "./UploadTask.tsx"; +import MoreActions from "./MoreActions.js"; +import { Virtuoso } from "react-virtuoso"; +import Base, { Status } from "../core/uploader/base"; +import { useTranslation } from "react-i18next"; +import { useAppSelector } from "../../../redux/hooks.ts"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; +import SessionManager, { UserSettings } from "../../../session"; +import Nothing from "../../Common/Nothing.tsx"; +import Dismiss from "../../Icons/Dismiss.tsx"; +import MoreHorizontal from "../../Icons/MoreHorizontal.tsx"; +import Add from "../../Icons/Add.tsx"; +import { ExpandMoreRounded } from "@mui/icons-material"; +import { UploadProgressTotal } from "../../../redux/globalStateSlice.ts"; + +const Transition = React.forwardRef(function Transition( + props: SlideProps, + ref, +) { + return ; +}); + +const StyledDialog = styled(Dialog)(({ fullScreen, theme }) => ({ + "& .MuiDialog-container": { + alignItems: "flex-end", + justifyContent: "flex-end", + }, + "& .MuiDialog-paper": { + border: `1px solid ${theme.palette.divider}`, + }, + ...(fullScreen + ? { + margin: 0, + position: "fixed", + inset: "-1!important", + } + : { top: "auto!important", left: "auto!important" }), +})); + +const StyledDialogContent = styled(DialogContent)(({ theme }) => ({ + [theme.breakpoints.up("md")]: { + width: 500, + minHeight: 300, + maxHeight: "calc(100vh - 140px)", + }, + padding: 0, + paddingTop: "0!important", +})); + +const CaretDownIcon = styled(ExpandMoreRounded)<{ expanded: boolean }>( + ({ theme, expanded }) => ({ + transform: `rotate(${expanded ? 0 : 180}deg)`, + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeInOut, + }), + }), +); + +const sorters: { + [key: string]: (a: Base, b: Base) => number; +} = { + default: (a, b) => a.id - b.id, + reverse: (a, b) => b.id - a.id, +}; + +const filters: { + [key: string]: (u: Base) => boolean; +} = { + default: (_u) => true, + ongoing: (u) => u.status < Status.finished, +}; + +export interface TaskListProps { + open: boolean; + onClose: () => void; + selectFile: (path: string) => void; + taskList: Base[]; + onCancel: (filter: (u: any) => boolean) => void; + uploadManager: any; + progress?: UploadProgressTotal; + setUploaders: (f: (u: Base[]) => Base[]) => void; +} + +export default function TaskList({ + open, + onClose, + selectFile, + taskList, + onCancel, + uploadManager, + progress, + setUploaders, +}: TaskListProps) { + const { t } = useTranslation("application", { keyPrefix: "uploader" }); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down("md")); + const path = useAppSelector((state) => state.fileManager[0].pure_path); + const [expanded, setExpanded] = useState(true); + const [useAvgSpeed, setUseAvgSpeed] = useState( + SessionManager.getWithFallback(UserSettings.UseAvgSpeed), + ); + const [anchorEl, setAnchorEl] = useState(null); + const [filter, setFilter] = useState( + SessionManager.getWithFallback(UserSettings.TaskFilter), + ); + const [sorter, setSorter] = useState( + SessionManager.getWithFallback(UserSettings.TaskSorter), + ); + const [refreshList, setRefreshList] = useState(false); + + const handleActionClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleActionClose = () => { + setAnchorEl(null); + }; + + const close = (_e: any, reason: string) => { + if (reason !== "backdropClick") { + onClose(); + } else { + setExpanded(false); + } + }; + const handlePanelChange = (_event: any, isExpanded: boolean) => { + setExpanded(isExpanded); + }; + + useMemo(() => { + if (open) { + setExpanded(true); + } + }, [taskList]); + + const stopPop = + (func: (e: React.MouseEvent) => void) => + (e: React.MouseEvent) => { + e.stopPropagation(); + func(e); + }; + + const progressBar = useMemo( + () => + progress && progress.totalSize > 0 ? ( + 0 && !expanded}> +
+ +
+
+ ) : null, + [progress, expanded, theme], + ); + + const list = useMemo(() => { + const currentList = taskList.filter(filters[filter]).sort(sorters[sorter]); + if (currentList.length === 0) { + return ; + } + + return ( + ( + setRefreshList((r) => !r)} + /> + )} + /> + ); + }, [taskList, useAvgSpeed, fullScreen, filter, sorter, refreshList]); + + const retryFailed = () => { + taskList.forEach((task) => { + if (task.status === Status.error) { + task.retry(); + } + }); + }; + + return ( + <> + { + SessionManager.set(UserSettings.UseAvgSpeed, v); + setUseAvgSpeed(v); + }} + filter={filter} + sorter={sorter} + setFilter={(v) => { + SessionManager.set(UserSettings.TaskFilter, v); + setFilter(v); + }} + setSorter={(v) => { + SessionManager.set(UserSettings.TaskSorter, v); + setSorter(v); + }} + retryFailed={retryFailed} + cleanFinished={() => setUploaders((u) => u.filter(filters["ongoing"]))} + /> + + + setExpanded(!expanded)} + sx={{ + boxShadow: "none", + position: "relative", + cursor: "pointer", + }} + > + {progressBar} + + + close(null, ""))} + aria-label="Close" + > + + + + + {t("uploadTasks")} + + + + + + + + {path && ( + + selectFile(path ?? defaultPath))} + > + + + + )} + {!fullScreen && ( + + setExpanded(!expanded))} + > + + + + )} + + + + + {list} + + + + + ); +} diff --git a/src/component/Uploader/Popup/UploadTask.js b/src/component/Uploader/Popup/UploadTask.js deleted file mode 100644 index 9cfb817..0000000 --- a/src/component/Uploader/Popup/UploadTask.js +++ /dev/null @@ -1,428 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { - Divider, - Grow, - IconButton, - ListItem, - ListItemText, - makeStyles, - Tooltip, -} from "@material-ui/core"; -import TypeIcon from "../../FileManager/TypeIcon"; -import { useUpload } from "../UseUpload"; -import { Status } from "../core/uploader/base"; -import { UploaderError } from "../core/errors"; -import { filename, sizeToString } from "../../../utils"; -import { darken, lighten } from "@material-ui/core/styles/colorManipulator"; -import { useTheme } from "@material-ui/core/styles"; -import Chip from "@material-ui/core/Chip"; -import DeleteIcon from "@material-ui/icons/Delete"; -import RefreshIcon from "@material-ui/icons/Refresh"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import { useDispatch } from "react-redux"; -import Link from "@material-ui/core/Link"; -import PlayArrow from "@material-ui/icons/PlayArrow"; -import withStyles from "@material-ui/core/styles/withStyles"; -import MuiExpansionPanel from "@material-ui/core/ExpansionPanel"; -import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; -import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; -import TaskDetail from "./TaskDetail"; -import { SelectType } from "../core"; -import { navigateTo } from "../../../redux/explorer"; -import { useTranslation } from "react-i18next"; - -const useStyles = makeStyles((theme) => ({ - progressContent: { - position: "relative", - zIndex: 9, - }, - progress: { - transition: "width .4s linear", - zIndex: 1, - height: "100%", - position: "absolute", - left: 0, - top: 0, - }, - progressContainer: { - position: "relative", - }, - listAction: { - marginLeft: 20, - marginRight: 20, - }, - wordBreak: { - wordBreak: "break-all", - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }, - successStatus: { - color: theme.palette.success.main, - }, - errorStatus: { - color: theme.palette.warning.main, - wordBreak: "break-all", - [theme.breakpoints.up("sm")]: { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }, - }, - disabledBadge: { - marginLeft: theme.spacing(1), - height: 18, - }, - delete: { - zIndex: 9, - }, - dstLink: { - color: theme.palette.success.main, - fontWeight: 600, - }, - fileNameContainer: { - display: "flex", - alignItems: "center", - }, -})); - -const ExpansionPanel = withStyles({ - root: { - maxWidth: "100%", - boxShadow: "none", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:before": { - display: "none", - }, - "&$expanded": { - margin: 0, - }, - }, - expanded: {}, -})(MuiExpansionPanel); - -const ExpansionPanelSummary = withStyles({ - root: { - minHeight: 0, - padding: 0, - display: "block", - "&$expanded": {}, - }, - content: { - margin: 0, - display: "block", - "&$expanded": { - margin: "0", - }, - }, - expanded: {}, -})(MuiExpansionPanelSummary); - -const ExpansionPanelDetails = withStyles((theme) => ({ - root: { - paddingLeft: 16, - paddingRight: 16, - paddingTop: 8, - paddingBottom: 8, - display: "block", - backgroundColor: theme.palette.background.default, - }, -}))(MuiExpansionPanelDetails); - -const getSpeedText = (speed, speedAvg, useSpeedAvg) => { - let displayedSpeed = speedAvg; - if (!useSpeedAvg) { - displayedSpeed = speed; - } - - return `${sizeToString(displayedSpeed ? displayedSpeed : 0)} /s`; -}; - -const getErrMsg = (error) => { - let errMsg = error.message; - if (error instanceof UploaderError) { - errMsg = error.Message(""); - } - - return errMsg; -}; - -export default function UploadTask({ - uploader, - useAvgSpeed, - onCancel, - onClose, - selectFile, - onRefresh, -}) { - const { t } = useTranslation("application", { keyPrefix: "uploader" }); - const classes = useStyles(); - const theme = useTheme(); - const [taskHover, setTaskHover] = useState(false); - const [expanded, setExpanded] = useState(false); - const { status, error, progress, speed, speedAvg, retry } = useUpload( - uploader - ); - const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - const [loading, setLoading] = useState(false); - const dispatch = useDispatch(); - const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]); - const navigateToDst = (path) => { - onClose(null, "backdropClick"); - NavigateTo(path); - }; - - const toggleDetail = (event, newExpanded) => { - setExpanded(!!newExpanded); - }; - - useEffect(() => { - if (status >= Status.finished) { - onRefresh(); - } - }, [status]); - - const statusText = useMemo(() => { - const parent = filename(uploader.task.dst); - switch (status) { - case Status.added: - case Status.initialized: - case Status.queued: - return
{t("pendingInQueue")}
; - case Status.preparing: - return
{t("preparing")}
; - case Status.error: - return ( -
- {getErrMsg(error)} -
-
- ); - case Status.finishing: - return
{t("processing")}
; - case Status.resumable: - return ( -
- {t("progressDescription", { - uploaded: sizeToString(progress.total.loaded), - total: sizeToString(progress.total.size), - percentage: progress.total.percent.toFixed(2), - })} -
- ); - case Status.processing: - if (progress) { - return ( -
- {t("progressDescriptionFull", { - speed: getSpeedText( - speed, - speedAvg, - useAvgSpeed - ), - uploaded: sizeToString(progress.total.loaded), - total: sizeToString(progress.total.size), - percentage: progress.total.percent.toFixed(2), - })} -
- ); - } - return
{t("progressDescriptionPlaceHolder")}
; - case Status.finished: - return ( -
- {t("uploadedTo")} - - navigateToDst(uploader.task.dst)} - > - {parent === "" ? t("rootFolder") : parent} - - -
-
- ); - default: - return
{t("unknownStatus")}
; - } - }, [status, error, progress, speed, speedAvg, useAvgSpeed]); - - const resumeLabel = useMemo( - () => - uploader.task.resumed && !fullScreen ? ( - - ) : null, - [status, fullScreen] - ); - - const continueLabel = useMemo( - () => - status === Status.resumable && !fullScreen ? ( - - ) : null, - [status, fullScreen] - ); - - const progressBar = useMemo( - () => - (status === Status.processing || - status === Status.finishing || - status === Status.resumable) && - progress ? ( -
- ) : null, - [status, progress, theme] - ); - - const taskDetail = useMemo(() => { - return ( - - ); - }, [uploader, expanded]); - - const cancel = () => { - setLoading(true); - uploader.cancel().then(() => { - setLoading(false); - onCancel((u) => u.id != uploader.id); - }); - }; - - const stopRipple = (e) => { - e.stopPropagation(); - }; - - const secondaryAction = useMemo(() => { - if (!taskHover && !fullScreen) { - return <>; - } - - const actions = [ - { - show: status === Status.error, - title: t("retry"), - click: retry, - icon: , - loading: false, - }, - { - show: true, - title: - status === Status.finished - ? t("deleteTask") - : t("cancelAndDelete"), - click: cancel, - icon: , - loading: loading, - }, - { - show: status === Status.resumable, - title: t("selectAndResume"), - click: () => - selectFile(uploader.task.dst, SelectType.File, uploader), - icon: , - loading: false, - }, - ]; - - return ( - <> - {actions.map((a) => ( - <> - {a.show && ( - - - { - e.stopPropagation(); - a.click(); - }} - > - {a.icon} - - - - )} - - ))} - - ); - }, [status, loading, taskHover, fullScreen, uploader]); - - const fileIcon = useMemo(() => { - if (!fullScreen) { - return ; - } - }, [uploader, fullScreen]); - - return ( - <> - - -
setTaskHover(false)} - onMouseOver={() => setTaskHover(true)} - > - {progressBar} - - {fileIcon} - -
- {uploader.task.name} -
-
{resumeLabel}
-
{continueLabel}
-
- } - secondary={ -
- {statusText} -
- } - /> - {secondaryAction} - -
- - {taskDetail} - - - - ); -} diff --git a/src/component/Uploader/Popup/UploadTask.tsx b/src/component/Uploader/Popup/UploadTask.tsx new file mode 100644 index 0000000..a310c04 --- /dev/null +++ b/src/component/Uploader/Popup/UploadTask.tsx @@ -0,0 +1,423 @@ +import { alpha, Box, Divider, Grow, IconButton, ListItemButton, ListItemText, styled, Tooltip } from "@mui/material"; +import MuiAccordion from "@mui/material/Accordion"; +import MuiAccordionDetails from "@mui/material/AccordionDetails"; +import MuiAccordionSummary from "@mui/material/AccordionSummary"; +import Chip from "@mui/material/Chip"; +import { lighten, useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FileType } from "../../../api/explorer.ts"; +import { navigateToPath } from "../../../redux/thunks/filemanager.ts"; +import { sizeToString } from "../../../util"; +import { NoWrapBox } from "../../Common/StyledComponents.tsx"; +import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx"; +import ArrowClockwiseFilled from "../../Icons/ArrowClockwiseFilled.tsx"; +import Delete from "../../Icons/Delete.tsx"; +import DocumentArrowDownFilled from "../../Icons/DocumentArrowDownFilled.tsx"; +import Play from "../../Icons/Play.tsx"; +import RenameFilled from "../../Icons/RenameFilled.tsx"; +import { useUpload } from "../UseUpload.js"; +import { SelectType } from "../core"; +import { CreateUploadSessionError, UploaderError } from "../core/errors"; +import Base, { Status } from "../core/uploader/base"; +import TaskDetail from "./TaskDetail.js"; + +const Accordion = styled(MuiAccordion)(() => ({ + maxWidth: "100%", + boxShadow: "none", + "&:not(:last-child)": { + borderBottom: 0, + }, + "&:before": { + display: "none", + }, + "& .Mui-expanded": { + margin: "0!important", + }, +})); + +const AccordionSummary = styled(MuiAccordionSummary)(() => ({ + minHeight: "56px", + padding: 0, + display: "block", + "& .MuiAccordionSummary-content": { + margin: 0, + display: "block", + "& .Mui-expanded": { + margin: "0!important", + }, + }, +})); + +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ + "& .MuiAccordionActions-root": { + paddingLeft: 16, + paddingRight: 16, + paddingTop: 8, + paddingBottom: 8, + display: "block", + backgroundColor: theme.palette.background.default, + }, +})); + +const getSpeedText = (speed: number, speedAvg: number, useSpeedAvg: boolean) => { + let displayedSpeed = speedAvg; + if (!useSpeedAvg) { + displayedSpeed = speed; + } + + return `${sizeToString(displayedSpeed ? displayedSpeed : 0)} /s`; +}; + +const getErrMsg = (error?: Error) => { + if (error) { + let errMsg = error.message; + if (error instanceof UploaderError) { + errMsg = error.Message(); + } + + return errMsg; + } +}; + +export interface UploadTaskProps { + uploader: Base; + useAvgSpeed: boolean; + onCancel: (filter: (u: Base) => boolean) => void; + onClose: (u: any, reason: string) => void; + selectFile: (path: string, type: SelectType, uploader: Base) => void; + onRefresh: () => void; +} + +export default function UploadTask({ + uploader, + useAvgSpeed, + onCancel, + onClose, + selectFile, + onRefresh, +}: UploadTaskProps) { + const { t } = useTranslation("application", { keyPrefix: "uploader" }); + const theme = useTheme(); + const [taskHover, setTaskHover] = useState(false); + const [expanded, setExpanded] = useState(false); + const { status, error, progress, speed, speedAvg, retry } = useUpload(uploader); + const fullScreen = useMediaQuery(theme.breakpoints.down("md")); + const [loading, setLoading] = useState(false); + const navigateToDst = (path: string) => { + onClose(null, "backdropClick"); + navigateToPath(0, path); + }; + + const toggleDetail = (_event: any, newExpanded: boolean) => { + setExpanded(!!newExpanded); + }; + + useEffect(() => { + if (status >= Status.finished) { + onRefresh(); + } + }, [status]); + + const statusText = useMemo(() => { + // const parent = filename(uploader.task.dst); + const parent = "TODO: parent"; + switch (status) { + case Status.added: + case Status.initialized: + case Status.queued: + return
{t("pendingInQueue")}
; + case Status.preparing: + return
{t("preparing")}
; + case Status.error: + return ( + + {getErrMsg(error)} +
+
+ ); + case Status.finishing: + return
{t("processing")}
; + case Status.resumable: + return ( + progress && ( +
+ {t("progressDescription", { + uploaded: sizeToString(progress.total.loaded), + total: sizeToString(progress.total.size), + percentage: progress.total.percent.toFixed(2), + })} +
+ ) + ); + case Status.processing: + if (progress) { + return ( +
+ {t("progressDescriptionFull", { + speed: getSpeedText(speed, speedAvg, useAvgSpeed), + uploaded: sizeToString(progress.total.loaded), + total: sizeToString(progress.total.size), + percentage: progress.total.percent.toFixed(2), + })} +
+ ); + } + return
{t("progressDescriptionPlaceHolder")}
; + case Status.finished: + return ( + + {t("uploaded")} +
+
+ ); + default: + return
{t("unknownStatus")}
; + } + }, [status, error, progress, speed, speedAvg, useAvgSpeed]); + + const resumeLabel = useMemo( + () => + uploader.task.resumed && !fullScreen ? ( + + ) : null, + [status, fullScreen], + ); + + const continueLabel = useMemo( + () => + status === Status.resumable && !fullScreen ? ( + theme.typography.caption.fontSize, + }} + size="small" + color={"secondary"} + label={t("resumable")} + /> + ) : null, + [status, fullScreen], + ); + + const progressBar = useMemo( + () => + (status === Status.processing || status === Status.finishing || status === Status.resumable) && progress ? ( + + ) : null, + [status, progress, theme], + ); + + const taskDetail = useMemo(() => { + return ; + }, [uploader, expanded]); + + const cancel = () => { + setLoading(true); + uploader.cancel().then(() => { + setLoading(false); + onCancel((u) => u.id != uploader.id); + }); + }; + + const stopRipple = (e: React.MouseEvent | React.TouchEvent) => { + e.stopPropagation(); + }; + + const secondaryAction = useMemo(() => { + if (!taskHover && !fullScreen) { + return <>; + } + + const isConflict = error instanceof CreateUploadSessionError && error.IsConflictError(); + + const actions: { + show: boolean; + title: string; + click: () => void; + icon: JSX.Element; + loading: boolean; + }[] = [ + { + show: status === Status.error && !isConflict, + title: t("retry"), + click: () => retry(), + icon: , + loading: false, + }, + { + show: status === Status.error && isConflict, + title: t("rename"), + click: () => retry({ new_prefix: t("fileCopyName") }), + icon: , + loading: false, + }, + { + show: status === Status.error && isConflict, + title: t("overwrite"), + click: () => retry({ overwrite: true }), + icon: , + loading: false, + }, + { + show: true, + title: status === Status.finished ? t("deleteTask") : t("cancelAndDelete"), + click: cancel, + icon: , + loading: loading, + }, + { + show: status === Status.resumable, + title: t("selectAndResume"), + click: () => selectFile(uploader.task.dst, SelectType.File, uploader), + icon: , + loading: false, + }, + ]; + + return ( + <> + {actions.map((a) => ( + <> + {a.show && ( + + + { + e.stopPropagation(); + a.click(); + }} + size="small" + > + {a.icon} + + + + )} + + ))} + + ); + }, [status, loading, taskHover, fullScreen, uploader, t]); + + const fileIcon = useMemo(() => { + if (!fullScreen) { + return ; + } + }, [uploader, fullScreen]); + + return ( + <> + + + setTaskHover(false)} + onMouseOver={() => setTaskHover(true)} + > + {progressBar} + + {fileIcon} + + + {uploader.task.name} + +
{resumeLabel}
+
{continueLabel}
+
+ } + secondary={ + + {statusText} + + } + slotProps={{ + primary: { + variant: "body2", + }, + + secondary: { + variant: "caption", + }, + }} + /> + {secondaryAction} + +
+ + {taskDetail} + + + + ); +} diff --git a/src/component/Uploader/TaskListIconButton.tsx b/src/component/Uploader/TaskListIconButton.tsx new file mode 100644 index 0000000..4261dca --- /dev/null +++ b/src/component/Uploader/TaskListIconButton.tsx @@ -0,0 +1,36 @@ +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import { Badge, Box, IconButton, Tooltip } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import UploadFilled from "../Icons/UploadFilled.tsx"; +import { useCallback } from "react"; +import { openUploadTaskList } from "../../redux/globalStateSlice.ts"; + +export const TaskListIconButton = () => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const queued = useAppSelector((state) => state.globalState.uploadTaskCount); + + const openList = useCallback(() => { + dispatch(openUploadTaskList()); + }, [dispatch]); + + if (!queued) { + return null; + } + + return ( + + + + theme.typography.body2.fontSize }} + badgeContent={queued} + color={"secondary"} + > + + + + + + ); +}; diff --git a/src/component/Uploader/Uploader.js b/src/component/Uploader/Uploader.js deleted file mode 100644 index 6af0e43..0000000 --- a/src/component/Uploader/Uploader.js +++ /dev/null @@ -1,242 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import UploadManager, { SelectType } from "./core"; -import { useDispatch, useSelector } from "react-redux"; -import UploadButton from "../Dial/Create"; -import pathHelper from "../../utils/page"; -import { useLocation } from "react-router-dom"; -import { UploaderError } from "./core/errors"; -import TaskList from "./Popup/TaskList"; -import { Status } from "./core/uploader/base"; -import { DropFileBackground } from "../Placeholder/DropFile"; -import { - refreshFileList, - refreshStorage, - toggleSnackbar, -} from "../../redux/explorer"; -import Auth from "../../middleware/Auth"; -import { useTranslation } from "react-i18next"; - -let totalProgressCollector = null; -let lastProgressStart = -1; -let dragCounter = 0; - -export default function Uploader() { - const { t } = useTranslation("application", { keyPrefix: "uploader" }); - const [uploaders, setUploaders] = useState([]); - const [taskListOpen, setTaskListOpen] = useState(false); - const [dropBgOpen, setDropBgOpen] = useState(0); - const [totalProgress, setTotalProgress] = useState({ - totalSize: 0, - processedSize: 0, - total: 0, - processed: 0, - }); - const search = useSelector((state) => state.explorer.search); - const policy = useSelector((state) => state.explorer.currentPolicy); - const isLogin = useSelector((state) => state.viewUpdate.isLogin); - const path = useSelector((state) => state.navigator.path); - const fileSelectCounter = useSelector( - (state) => state.viewUpdate.openFileSelector - ); - const folderSelectCounter = useSelector( - (state) => state.viewUpdate.openFolderSelector - ); - const location = useLocation(); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const RefreshFileList = useCallback(() => dispatch(refreshFileList()), [ - dispatch, - ]); - const RefreshStorage = useCallback(() => dispatch(refreshStorage()), [ - dispatch, - ]); - - const enableUploader = useMemo( - () => pathHelper.isHomePage(location.pathname) && isLogin && !search, - [location.pathname, isLogin, search] - ); - - const taskAdded = (original = null) => (tasks) => { - if (original !== null) { - if (tasks.length !== 1 || tasks[0].key() !== original.key()) { - ToggleSnackbar( - "top", - "right", - t("fileNotMatchError"), - "warning" - ); - return; - } - } - - tasks.forEach((t) => t.start()); - - setTaskListOpen(true); - setUploaders((uploaders) => { - if (original !== null) { - uploaders = uploaders.filter((u) => u.key() !== original.key()); - } - - return [...uploaders, ...tasks]; - }); - }; - - const uploadManager = useMemo(() => { - return new UploadManager({ - logLevel: "INFO", - concurrentLimit: parseInt( - Auth.GetPreferenceWithDefault("concurrent_limit", "5") - ), - dropZone: document.querySelector("body"), - onToast: (type, msg) => { - ToggleSnackbar("top", "right", msg, type); - }, - onDropOver: (e) => { - dragCounter++; - setDropBgOpen((value) => !value); - }, - onDropLeave: (e) => { - dragCounter--; - setDropBgOpen((value) => !value); - }, - onDropFileAdded: taskAdded(), - }); - }, []); - - useEffect(() => { - uploadManager.setPolicy(policy, path); - }, [policy]); - - useEffect(() => { - const unfinished = uploadManager.resumeTasks(); - setUploaders((uploaders) => [...uploaders, ...unfinished]); - if (!totalProgressCollector) { - totalProgressCollector = setInterval(() => { - const progress = { - totalSize: 0, - processedSize: 0, - total: 0, - processed: 0, - }; - setUploaders((uploaders) => { - uploaders.forEach((u) => { - if (u.id <= lastProgressStart) { - return; - } - - progress.totalSize += u.task.size; - progress.total += 1; - - if ( - u.status === Status.finished || - u.status === Status.canceled || - u.status === Status.error - ) { - progress.processedSize += u.task.size; - progress.processed += 1; - } - - if ( - u.status === Status.added || - u.status === Status.initialized || - u.status === Status.queued || - u.status === Status.preparing || - u.status === Status.processing || - u.status === Status.finishing - ) { - progress.processedSize += u.progress - ? u.progress.total.loaded - : 0; - } - }); - - if ( - progress.total > 0 && - progress.processed === progress.total - ) { - lastProgressStart = uploaders[uploaders.length - 1].id; - } - return uploaders; - }); - - if ( - progress.total > 0 && - progress.total === progress.processed - ) { - RefreshFileList(); - RefreshStorage(); - } - - setTotalProgress(progress); - }, 2000); - } - }, []); - - const selectFile = (path, type = SelectType.File, original = null) => { - setTaskListOpen(true); - - // eslint-disable-next-line no-unreachable - uploadManager - .select(path, type) - .then(taskAdded(original)) - .catch((e) => { - if (e instanceof UploaderError) { - ToggleSnackbar("top", "right", e.Message(), "warning"); - } else { - ToggleSnackbar( - "top", - "right", - t("unknownError", { msg: e.message }), - "error" - ); - } - }); - }; - - useEffect(() => { - if (fileSelectCounter > 0) { - selectFile(path); - } - }, [fileSelectCounter]); - - useEffect(() => { - if (folderSelectCounter > 0) { - selectFile(path, SelectType.Directory); - } - }, [folderSelectCounter]); - - const deleteTask = (filter) => { - setUploaders((uploaders) => uploaders.filter(filter)); - }; - - return ( - <> - {enableUploader && ( - <> - 0} /> - setTaskListOpen(true)} - /> - setTaskListOpen(false)} - setUploaders={setUploaders} - /> - - )} - - ); -} diff --git a/src/component/Uploader/Uploader.tsx b/src/component/Uploader/Uploader.tsx new file mode 100644 index 0000000..b86a868 --- /dev/null +++ b/src/component/Uploader/Uploader.tsx @@ -0,0 +1,230 @@ +import dayjs from "dayjs"; +import { useSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { closeUploadTaskList, openUploadTaskList, setUploadProgress } from "../../redux/globalStateSlice.ts"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts"; +import { refreshFileList, updateUserCapacity } from "../../redux/thunks/filemanager.ts"; +import SessionManager, { UserSettings } from "../../session"; +import UploadManager, { SelectType } from "./core"; +import { UploaderError } from "./core/errors"; +import Base, { Status } from "./core/uploader/base.ts"; +import { DropFileBackground } from "./DropFile.tsx"; +import PasteUploadDialog from "./PasteUploadDialog.tsx"; +import TaskList from "./Popup/TaskList.tsx"; + +let totalProgressCollector: NodeJS.Timeout | null = null; +let lastProgressStart = -1; +let dragCounter = 0; + +const defaultClipboardImageName = "image.png"; + +const Uploader = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { enqueueSnackbar } = useSnackbar(); + const [uploaders, setUploaders] = useState([]); + const [dropBgOpen, setDropBgOpen] = useState(false); + + const totalProgress = useAppSelector((state) => state.globalState.uploadProgress); + const taskListOpen = useAppSelector((state) => state.globalState.uploadTaskListOpen); + const parent = useAppSelector((state) => state.fileManager[0].list?.parent); + const path = useAppSelector((state) => state.fileManager[0].pure_path); + const policy = useAppSelector((state) => state.fileManager[0].list?.storage_policy); + const selectFileSignal = useAppSelector((state) => state.globalState.uploadFileSignal); + const selectFolderSignal = useAppSelector((state) => state.globalState.uploadFolderSignal); + + const taskAdded = useCallback( + (original?: Base) => (tasks: Base[]) => { + if (original !== undefined) { + if (tasks.length !== 1 || tasks[0].key() !== original.key()) { + enqueueSnackbar(t("uploader.fileNotMatchError"), { + variant: "warning", + }); + return; + } + } + + tasks.forEach((t) => t.start()); + + dispatch(openUploadTaskList()); + setUploaders((uploaders) => { + if (original !== undefined) { + uploaders = uploaders.filter((u) => u.key() !== original.key()); + } + + return [...uploaders, ...tasks]; + }); + }, + [enqueueSnackbar, dispatch, setUploaders], + ); + + const uploadManager = useMemo(() => { + return new UploadManager({ + logLevel: "INFO", + concurrentLimit: parseInt(SessionManager.getWithFallback(UserSettings.ConcurrentLimit)), + overwrite: SessionManager.getWithFallback(UserSettings.UploadOverwrite), + dropZone: document.querySelector("body"), + onToast: (type, msg) => { + enqueueSnackbar(msg, { variant: type }); + }, + onDropOver: (_e) => { + dragCounter++; + setDropBgOpen((value) => !value); + }, + onDropLeave: (_e) => { + dragCounter--; + setDropBgOpen((value) => !value); + }, + onProactiveFileAdded: taskAdded(), + onPoolEmpty: () => { + dispatch(refreshFileList(0)); + dispatch(updateUserCapacity(0)); + }, + }); + }, []); + + useEffect(() => { + uploadManager.setPolicy(policy, path); + }, [policy]); + + const handleUploaderError = useCallback( + (e: any) => { + if (e instanceof UploaderError) { + enqueueSnackbar(e.Message(), { variant: "warning" }); + } else { + enqueueSnackbar(t("uploader:unknownError", { msg: e.message }), { + variant: "error", + }); + } + }, + [enqueueSnackbar, t], + ); + + const selectFile = useCallback( + (path: string, type = SelectType.File, original?: Base) => { + dispatch(openUploadTaskList()); + + // eslint-disable-next-line no-unreachable + uploadManager + .select(path, type) + .then(taskAdded(original)) + .catch((e) => { + handleUploaderError(e); + }); + }, + [uploadManager, taskAdded, handleUploaderError, dispatch], + ); + + const getClipboardFileName = useCallback( + (f: File) => { + if (f.type.startsWith("image") && f.name == defaultClipboardImageName) { + return t("uploader.clipboardDefaultFileName", { + date: dayjs().valueOf(), + }); + } + return f.name; + }, + [t], + ); + + const addRawFiles = useCallback( + (files: File[]) => { + uploadManager.addRawFiles(files, getClipboardFileName).catch((e) => { + handleUploaderError(e); + }); + }, + [uploadManager, taskAdded, handleUploaderError, dispatch, getClipboardFileName], + ); + + useEffect(() => { + const unfinished = uploadManager.resumeTasks(); + setUploaders((uploaders) => [ + ...uploaders, + ...unfinished.filter((u) => uploaders.find((v) => v.key() === u.key()) === undefined), + ]); + if (!totalProgressCollector) { + totalProgressCollector = setInterval(() => { + let progress = { + totalSize: 0, + processedSize: 0, + total: 0, + processed: 0, + }; + setUploaders((uploaders) => { + uploaders.forEach((u) => { + if (u.id <= lastProgressStart) { + return; + } + + progress.totalSize += u.task.size; + progress.total += 1; + + if (u.status === Status.finished || u.status === Status.canceled || u.status === Status.error) { + progress.processedSize += u.task.size; + progress.processed += 1; + } + + if ( + u.status === Status.added || + u.status === Status.initialized || + u.status === Status.queued || + u.status === Status.preparing || + u.status === Status.processing || + u.status === Status.finishing + ) { + progress.processedSize += u.progress ? u.progress.total.loaded : 0; + } + }); + + if (progress.total > 0 && progress.processed === progress.total) { + lastProgressStart = uploaders[uploaders.length - 1].id; + } + return uploaders; + }); + + dispatch( + setUploadProgress({ + progress, + count: progress.total, + }), + ); + }, 2000); + } + }, []); + + useEffect(() => { + if (selectFileSignal && selectFileSignal > 0 && path) { + selectFile(path); + } + }, [selectFileSignal]); + + useEffect(() => { + if (selectFolderSignal && selectFolderSignal > 0 && path) { + selectFile(path, SelectType.Directory); + } + }, [selectFolderSignal]); + + const deleteTask = useCallback((filter: (b: Base) => boolean) => { + setUploaders((uploaders) => uploaders.filter(filter)); + }, []); + + return ( + <> + + + dispatch(closeUploadTaskList())} + setUploaders={setUploaders} + /> + + ); +}; + +export default Uploader; diff --git a/src/component/Uploader/UseUpload.js b/src/component/Uploader/UseUpload.js deleted file mode 100644 index 90e4b66..0000000 --- a/src/component/Uploader/UseUpload.js +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useDispatch } from "react-redux"; -import { toggleSnackbar } from "../../redux/explorer"; - -export function useUpload(uploader) { - const startLoadedRef = useRef(0); - const [status, setStatus] = useState(uploader.status); - const [error, setError] = useState(uploader.error); - const [progress, setProgress] = useState(uploader.progress); - const dispatch = useDispatch(); - - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - /* eslint-disable @typescript-eslint/no-empty-function */ - uploader.subscribe({ - onTransition: (newStatus) => { - setStatus(newStatus); - }, - onError: (err) => { - setError(err); - setStatus(uploader.status); - }, - onProgress: (data) => { - setProgress(data); - }, - onMsg: (msg, color) => { - ToggleSnackbar("top", "right", msg, color); - }, - }); - }, []); - - // 获取上传速度 - const [speed, speedAvg] = React.useMemo(() => { - if ( - progress == null || - progress.total == null || - progress.total.loaded == null - ) - return [0, 0]; - const duration = (Date.now() - (uploader.lastTime || 0)) / 1000; - const durationTotal = (Date.now() - (uploader.startTime || 0)) / 1000; - const res = - progress.total.loaded > startLoadedRef.current - ? Math.floor( - (progress.total.loaded - startLoadedRef.current) / - duration - ) - : 0; - const resAvg = - progress.total.loaded > 0 - ? Math.floor(progress.total.loaded / durationTotal) - : 0; - - startLoadedRef.current = progress.total.loaded; - uploader.lastTime = Date.now(); - return [res, resAvg]; - }, [progress]); - - const retry = () => { - uploader.reset(); - uploader.start(); - }; - - return { status, error, progress, speed, speedAvg, retry }; -} diff --git a/src/component/Uploader/UseUpload.tsx b/src/component/Uploader/UseUpload.tsx new file mode 100644 index 0000000..0037c72 --- /dev/null +++ b/src/component/Uploader/UseUpload.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useRef, useState } from "react"; +import Base, { RetryOption } from "./core/uploader/base.ts"; +import { useSnackbar } from "notistack"; + +export function useUpload(uploader: Base) { + const startLoadedRef = useRef(0); + const [status, setStatus] = useState(uploader.status); + const [error, setError] = useState(uploader.error); + const [progress, setProgress] = useState(uploader.progress); + const { enqueueSnackbar } = useSnackbar(); + + useEffect(() => { + /* eslint-disable @typescript-eslint/no-empty-function */ + uploader.subscribe({ + onTransition: (newStatus) => { + setStatus(newStatus); + }, + onError: (err) => { + setError(err); + setStatus(uploader.status); + }, + onProgress: (data) => { + setProgress(data); + }, + onMsg: (msg, color) => { + enqueueSnackbar(msg, { variant: color }); + }, + }); + }, []); + + // 获取上传速度 + const [speed, speedAvg] = React.useMemo(() => { + if ( + progress == undefined || + progress.total == null || + progress.total.loaded == null + ) + return [0, 0]; + const duration = (Date.now() - (uploader.lastTime || 0)) / 1000; + const durationTotal = (Date.now() - (uploader.startTime || 0)) / 1000; + const res = + progress.total.loaded > startLoadedRef.current + ? Math.floor( + (progress.total.loaded - startLoadedRef.current) / duration, + ) + : 0; + const resAvg = + progress.total.loaded > 0 + ? Math.floor(progress.total.loaded / durationTotal) + : 0; + + startLoadedRef.current = progress.total.loaded; + uploader.lastTime = Date.now(); + return [res, resAvg]; + }, [progress?.total.loaded]); + + const retry = (opt?: RetryOption) => { + uploader.retry(opt); + }; + + return { status, error, progress, speed, speedAvg, retry }; +} diff --git a/src/component/Uploader/core/api/index.ts b/src/component/Uploader/core/api/index.ts index e01aa60..7272728 100644 --- a/src/component/Uploader/core/api/index.ts +++ b/src/component/Uploader/core/api/index.ts @@ -1,444 +1,426 @@ import { - OneDriveChunkResponse, - QiniuChunkResponse, - QiniuFinishUploadRequest, - QiniuPartsInfo, - S3Part, - UploadCredential, - UploadSessionRequest, + OneDriveChunkResponse, + QiniuChunkResponse, + QiniuFinishUploadRequest, + QiniuPartsInfo, + S3Part, } from "../types"; -import { OBJtoXML, request, requestAPI } from "../utils"; +import { OBJtoXML, request } from "../utils"; import { - COSUploadCallbackError, - COSUploadError, - CreateUploadSessionError, - DeleteUploadSessionError, - HTTPError, - LocalChunkUploadError, - OneDriveChunkError, - OneDriveFinishUploadError, - QiniuChunkError, - QiniuFinishUploadError, - S3LikeChunkError, - S3LikeFinishUploadError, - S3LikeUploadCallbackError, - SlaveChunkUploadError, - UpyunUploadError, + CreateUploadSessionError, + DeleteUploadSessionError, + HTTPError, + LocalChunkUploadError, + ObsFinishUploadError, + OneDriveChunkError, + OneDriveFinishUploadError, + QiniuChunkError, + QiniuFinishUploadError, + S3LikeChunkError, + S3LikeFinishUploadError, + S3LikeUploadCallbackError, + SlaveChunkUploadError, + UpyunUploadError, } from "../errors"; import { ChunkInfo, ChunkProgress } from "../uploader/chunk"; import { Progress } from "../uploader/base"; import { CancelToken } from "axios"; +import { + UploadCredential, + UploadSessionRequest, +} from "../../../../api/explorer.ts"; +import { store } from "../../../../redux/store.ts"; +import { + sendCreateUploadSession, + sendDeleteUploadSession, + sendOneDriveCompleteUpload, + sendS3LikeCompleteUpload, + sendUploadChunk, +} from "../../../../api/api.ts"; +import { AppError } from "../../../../api/request.ts"; export async function createUploadSession( - req: UploadSessionRequest, - cancel: CancelToken + req: UploadSessionRequest, + _cancel: CancelToken, ): Promise { - const res = await requestAPI("file/upload", { - method: "put", - data: req, - cancelToken: cancel, - }); - - if (res.data.code != 0) { - throw new CreateUploadSessionError(res.data); + try { + return await store.dispatch(sendCreateUploadSession(req)); + } catch (e) { + if (e instanceof AppError) { + throw new CreateUploadSessionError(e.response); } - return res.data.data; + throw e; + } } -export async function deleteUploadSession(id: string): Promise { - const res = await requestAPI(`file/upload/${id}`, { - method: "delete", - }); - - if (res.data.code != 0) { - throw new DeleteUploadSessionError(res.data); +export async function deleteUploadSession( + id: string, + uri: string, +): Promise { + try { + return await store.dispatch(sendDeleteUploadSession({ id, uri })); + } catch (e) { + if (e instanceof AppError) { + throw new DeleteUploadSessionError(e.response); } - return res.data.data; + throw e; + } } export async function localUploadChunk( - sessionID: string, - chunk: ChunkInfo, - onProgress: (p: Progress) => void, - cancel: CancelToken + sessionID: string, + chunk: ChunkInfo, + onProgress: (p: Progress) => void, + cancel: CancelToken, ): Promise { - const res = await requestAPI( - `file/upload/${sessionID}/${chunk.index}`, - { - method: "post", - headers: { "content-type": "application/octet-stream" }, - data: chunk.chunk, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - } + try { + return await store.dispatch( + sendUploadChunk( + sessionID, + chunk.chunk, + chunk.index, + cancel, + (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + ), ); - - if (res.data.code != 0) { - throw new LocalChunkUploadError(res.data, chunk.index); + } catch (e) { + if (e instanceof AppError) { + throw new LocalChunkUploadError(e.response, chunk.index); } - return res.data.data; + throw e; + } } export async function slaveUploadChunk( - url: string, - credential: string, - chunk: ChunkInfo, - onProgress: (p: Progress) => void, - cancel: CancelToken + url: string, + credential: string, + chunk: ChunkInfo, + onProgress: (p: Progress) => void, + cancel: CancelToken, ): Promise { - const res = await request(`${url}?chunk=${chunk.index}`, { - method: "post", - headers: { - "content-type": "application/octet-stream", - Authorization: credential, - }, - data: chunk.chunk, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - }); + const res = await request(`${url}?chunk=${chunk.index}`, { + method: "post", + headers: { + "content-type": "application/octet-stream", + Authorization: credential, + }, + data: chunk.chunk, + onUploadProgress: (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + cancelToken: cancel, + }); - if (res.data.code != 0) { - throw new SlaveChunkUploadError(res.data, chunk.index); - } + if (res.data.code != 0) { + throw new SlaveChunkUploadError(res.data, chunk.index); + } - return res.data.data; + return res.data.data; } export async function oneDriveUploadChunk( - url: string, - range: string, // if range is empty, this will be an request to query the session status - chunk: ChunkInfo, - onProgress: (p: Progress) => void, - cancel: CancelToken + url: string, + range: string, // if range is empty, this will be an request to query the session status + chunk: ChunkInfo, + onProgress: (p: Progress) => void, + cancel: CancelToken, ): Promise { - const res = await request(url, { - method: range === "" ? "get" : "put", - headers: { - "content-type": "application/octet-stream", - ...(range !== "" && { "content-range": range }), - }, - data: chunk.chunk, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new OneDriveChunkError(e.response.data); - } + const res = await request(url, { + method: range === "" ? "get" : "put", + headers: { + "content-type": "application/octet-stream", + ...(range !== "" && { "content-range": range }), + }, + data: chunk.chunk, + onUploadProgress: (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + cancelToken: cancel, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new OneDriveChunkError(e.response.data); + } - throw e; - }); + throw e; + }); - return res.data; + return res.data; } export async function finishOneDriveUpload( - sessionID: string, - cancel: CancelToken -): Promise { - const res = await requestAPI( - `callback/onedrive/finish/${sessionID}`, - { - method: "post", - data: "{}", - cancelToken: cancel, - } + sessionID: string, + sessionKey: string, +): Promise { + try { + return await store.dispatch( + sendOneDriveCompleteUpload(sessionID, sessionKey), ); - - if (res.data.code != 0) { - throw new OneDriveFinishUploadError(res.data); + } catch (e) { + if (e instanceof AppError) { + throw new OneDriveFinishUploadError(e.response); } - return res.data.data; + throw e; + } } export async function s3LikeUploadChunk( - url: string, - chunk: ChunkInfo, - onProgress: (p: Progress) => void, - cancel: CancelToken + url: string, + chunk: ChunkInfo, + onProgress: (p: Progress) => void, + cancel: CancelToken, ): Promise { - const res = await request(url, { - method: "put", - headers: { - "content-type": "application/octet-stream", - }, - data: chunk.chunk, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - responseType: "document", - transformResponse: undefined, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new S3LikeChunkError(e.response.data); - } + const res = await request(url, { + method: "put", + headers: { + "content-type": "application/octet-stream", + }, + data: chunk.chunk, + onUploadProgress: (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + cancelToken: cancel, + responseType: "document", + transformResponse: undefined, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new S3LikeChunkError(e.response.data); + } - throw e; - }); + throw e; + }); - return res.headers.etag; + return res.headers["etag"]; +} + +export async function obsFinishUpload( + url: string, + chunks: ChunkProgress[], + cancel: CancelToken, +): Promise { + let body = encodePartsXML(chunks); + const res = await request(url, { + method: "post", + cancelToken: cancel, + transformResponse: undefined, + data: body, + headers: { + "content-type": "application/octet-stream", + }, + validateStatus: function (status) { + return status == 200; + }, + }).catch((e) => { + if (e instanceof HTTPError && e.response?.data?.message) { + throw new ObsFinishUploadError(e.response.data); + } + + if (e instanceof HTTPError && e.response?.data) { + // Decode xml + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(e.response.data, "text/xml"); + throw new S3LikeFinishUploadError(xmlDoc); + } + + throw e; + }); + + return res.data; } export async function s3LikeFinishUpload( - url: string, - isOss: boolean, - chunks: ChunkProgress[], - cancel: CancelToken + url: string, + isOss: boolean, + chunks: ChunkProgress[], + cancel: CancelToken, + headers?: { [key: string]: string }, ): Promise { - let body = ""; - if (!isOss) { - body += ""; - chunks.forEach((chunk) => { - body += ""; - const part: S3Part = { - PartNumber: chunk.index + 1, - ETag: chunk.etag!, - }; - body += OBJtoXML(part); - body += ""; - }); - body += ""; + let body = ""; + if (!isOss) { + body += encodePartsXML(chunks); + } + + const res = await request(url, { + method: "post", + cancelToken: cancel, + responseType: "document", + transformResponse: undefined, + data: body, + headers: { + "content-type": "application/octet-stream", + ...headers, + }, + validateStatus: function (status) { + return status == 200; + }, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new S3LikeFinishUploadError(e.response.data); } - const res = await request(url, { - method: "post", - cancelToken: cancel, - responseType: "document", - transformResponse: undefined, - data: body, - headers: isOss - ? { - "content-type": "application/octet-stream", - "x-oss-forbid-overwrite": "true", - "x-oss-complete-all": "yes", - } - : { - "content-type": "application/xhtml+xml", - }, - validateStatus: function (status) { - return status == 200; - }, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new S3LikeFinishUploadError(e.response.data); - } + throw e; + }); - throw e; - }); - - return res.data; + return res.data; } export async function qiniuDriveUploadChunk( - url: string, - upToken: string, - chunk: ChunkInfo, - onProgress: (p: Progress) => void, - cancel: CancelToken + url: string, + upToken: string, + chunk: ChunkInfo, + onProgress: (p: Progress) => void, + cancel: CancelToken, ): Promise { - const res = await request(`${url}/${chunk.index + 1}`, { - method: "put", - headers: { - "content-type": "application/octet-stream", - authorization: "UpToken " + upToken, - }, - data: chunk.chunk, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new QiniuChunkError(e.response.data); - } + const res = await request(`${url}/${chunk.index + 1}`, { + method: "put", + headers: { + "content-type": "application/octet-stream", + authorization: "UpToken " + upToken, + }, + data: chunk.chunk, + onUploadProgress: (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + cancelToken: cancel, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new QiniuChunkError(e.response.data); + } - throw e; - }); + throw e; + }); - return res.data; + return res.data; } export async function qiniuFinishUpload( - url: string, - upToken: string, - chunks: ChunkProgress[], - cancel: CancelToken + url: string, + upToken: string, + chunks: ChunkProgress[], + cancel: CancelToken, + mimeType?: string, ): Promise { - const content: QiniuFinishUploadRequest = { - parts: chunks.map( - (chunk): QiniuPartsInfo => { - return { - etag: chunk.etag!, - partNumber: chunk.index + 1, - }; - } - ), - }; + const content: QiniuFinishUploadRequest = { + mimeType, + parts: chunks.map((chunk): QiniuPartsInfo => { + return { + etag: chunk.etag!, + partNumber: chunk.index + 1, + }; + }), + }; - const res = await request(`${url}`, { - method: "post", - headers: { - "content-type": "application/json", - authorization: "UpToken " + upToken, - }, - data: content, - cancelToken: cancel, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new QiniuFinishUploadError(e.response.data); - } - - throw e; - }); - - return res.data; -} - -export async function cosFormUploadChunk( - url: string, - file: File, - policy: string, - path: string, - callback: string, - sessionID: string, - keyTime: string, - credential: string, - ak: string, - onProgress: (p: Progress) => void, - cancel: CancelToken -): Promise { - const bodyFormData = new FormData(); - bodyFormData.append("policy", policy); - bodyFormData.append("key", path); - bodyFormData.append("x-cos-meta-callback", callback); - bodyFormData.append("x-cos-meta-key", sessionID); - bodyFormData.append("q-sign-algorithm", "sha1"); - bodyFormData.append("q-key-time", keyTime); - bodyFormData.append("q-ak", ak); - bodyFormData.append("q-signature", credential); - bodyFormData.append("name", file.name); - // File must be the last element in the form - bodyFormData.append("file", file); - - const res = await request(`${url}`, { - method: "post", - headers: { - "content-type": "multipart/form-data", - }, - data: bodyFormData, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - responseType: "document", - transformResponse: undefined, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new COSUploadError(e.response.data); - } - - throw e; - }); - - return res.data; -} - -export async function cosUploadCallback( - sessionID: string, - cancel: CancelToken -): Promise { - const res = await requestAPI(`callback/cos/${sessionID}`, { - method: "get", - data: "{}", - cancelToken: cancel, - }); - - if (res.data.code != 0) { - throw new COSUploadCallbackError(res.data); + const res = await request(`${url}`, { + method: "post", + headers: { + "content-type": "application/json", + authorization: "UpToken " + upToken, + }, + data: content, + cancelToken: cancel, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new QiniuFinishUploadError(e.response.data); } - return res.data.data; + throw e; + }); + + return res.data; } export async function upyunFormUploadChunk( - url: string, - file: File, - policy: string, - credential: string, - onProgress: (p: Progress) => void, - cancel: CancelToken + url: string, + file: File, + policy: string, + credential: string, + onProgress: (p: Progress) => void, + cancel: CancelToken, + mimeType?: string, ): Promise { - const bodyFormData = new FormData(); - bodyFormData.append("policy", policy); - bodyFormData.append("authorization", credential); - // File must be the last element in the form - bodyFormData.append("file", file); + const bodyFormData = new FormData(); + bodyFormData.append("policy", policy); + bodyFormData.append("authorization", credential); + if (mimeType) { + bodyFormData.append("content-type", mimeType); + } + // File must be the last element in the form + bodyFormData.append("file", file); - const res = await request(`${url}`, { - method: "post", - headers: { - "content-type": "multipart/form-data", - }, - data: bodyFormData, - onUploadProgress: (progressEvent) => { - onProgress({ - loaded: progressEvent.loaded, - total: progressEvent.total, - }); - }, - cancelToken: cancel, - }).catch((e) => { - if (e instanceof HTTPError && e.response) { - throw new UpyunUploadError(e.response.data); - } + const res = await request(`${url}`, { + method: "post", + headers: { + "content-type": "multipart/form-data", + }, + data: bodyFormData, + onUploadProgress: (progressEvent) => { + onProgress({ + loaded: progressEvent.loaded, + total: progressEvent.total, + }); + }, + cancelToken: cancel, + }).catch((e) => { + if (e instanceof HTTPError && e.response) { + throw new UpyunUploadError(e.response.data); + } - throw e; - }); + throw e; + }); - return res.data; + return res.data; } export async function s3LikeUploadCallback( - sessionID: string, - cancel: CancelToken + sessionID: string, + sessionKey: string, + policyType: string, ): Promise { - const res = await requestAPI(`callback/s3/${sessionID}`, { - method: "get", - data: "{}", - cancelToken: cancel, - }); - - if (res.data.code != 0) { - throw new S3LikeUploadCallbackError(res.data); + try { + return await store.dispatch( + sendS3LikeCompleteUpload(policyType, sessionID, sessionKey), + ); + } catch (e) { + if (e instanceof AppError) { + throw new S3LikeUploadCallbackError(e.response); } - return res.data.data; + throw e; + } } + +const encodePartsXML = (chunks: ChunkProgress[]): string => { + let body = ""; + body += ""; + chunks.forEach((chunk) => { + body += ""; + const part: S3Part = { + PartNumber: chunk.index + 1, + ETag: chunk.etag!, + }; + body += OBJtoXML(part); + body += ""; + }); + body += ""; + return body; +}; diff --git a/src/component/Uploader/core/errors/index.ts b/src/component/Uploader/core/errors/index.ts index fc5a4ac..e993a34 100644 --- a/src/component/Uploader/core/errors/index.ts +++ b/src/component/Uploader/core/errors/index.ts @@ -1,399 +1,424 @@ -import { - OneDriveError, - Policy, - QiniuError, - Response, - UpyunError, -} from "../types"; -import { sizeToString } from "../utils"; +import { OneDriveError, QiniuError, UpyunError } from "../types"; import i18next from "../../../../i18n"; -import { AppError } from "../../../../middleware/Api"; +import { AppError, Response } from "../../../../api/request.ts"; +import { StoragePolicy } from "../../../../api/explorer.ts"; +import { sizeToString } from "../../../../util"; export enum UploaderErrorName { - InvalidFile = "InvalidFile", - NoPolicySelected = "NoPolicySelected", - UnknownPolicyType = "UnknownPolicyType", - FailedCreateUploadSession = "FailedCreateUploadSession", - FailedDeleteUploadSession = "FailedDeleteUploadSession", - HTTPRequestFailed = "HTTPRequestFailed", - LocalChunkUploadFailed = "LocalChunkUploadFailed", - SlaveChunkUploadFailed = "SlaveChunkUploadFailed", - WriteCtxFailed = "WriteCtxFailed", - RemoveCtxFailed = "RemoveCtxFailed", - ReadCtxFailed = "ReadCtxFailed", - InvalidCtxData = "InvalidCtxData", - CtxExpired = "CtxExpired", - RequestCanceled = "RequestCanceled", - ProcessingTaskDuplicated = "ProcessingTaskDuplicated", - OneDriveChunkUploadFailed = "OneDriveChunkUploadFailed", - OneDriveEmptyFile = "OneDriveEmptyFile", - FailedFinishOneDriveUpload = "FailedFinishOneDriveUpload", - S3LikeChunkUploadFailed = "S3LikeChunkUploadFailed", - S3LikeUploadCallbackFailed = "S3LikeUploadCallbackFailed", - COSUploadCallbackFailed = "COSUploadCallbackFailed", - COSPostUploadFailed = "COSPostUploadFailed", - UpyunPostUploadFailed = "UpyunPostUploadFailed", - QiniuChunkUploadFailed = "QiniuChunkUploadFailed", - FailedFinishOSSUpload = "FailedFinishOSSUpload", - FailedFinishQiniuUpload = "FailedFinishQiniuUpload", - FailedTransformResponse = "FailedTransformResponse", + InvalidFile = "InvalidFile", + NoPolicySelected = "NoPolicySelected", + UnknownPolicyType = "UnknownPolicyType", + FailedCreateUploadSession = "FailedCreateUploadSession", + FailedDeleteUploadSession = "FailedDeleteUploadSession", + HTTPRequestFailed = "HTTPRequestFailed", + LocalChunkUploadFailed = "LocalChunkUploadFailed", + SlaveChunkUploadFailed = "SlaveChunkUploadFailed", + WriteCtxFailed = "WriteCtxFailed", + RemoveCtxFailed = "RemoveCtxFailed", + ReadCtxFailed = "ReadCtxFailed", + InvalidCtxData = "InvalidCtxData", + CtxExpired = "CtxExpired", + ProcessingTaskDuplicated = "ProcessingTaskDuplicated", + OneDriveChunkUploadFailed = "OneDriveChunkUploadFailed", + OneDriveEmptyFile = "OneDriveEmptyFile", + FailedFinishOneDriveUpload = "FailedFinishOneDriveUpload", + S3LikeChunkUploadFailed = "S3LikeChunkUploadFailed", + S3LikeUploadCallbackFailed = "S3LikeUploadCallbackFailed", + COSUploadCallbackFailed = "COSUploadCallbackFailed", + COSPostUploadFailed = "COSPostUploadFailed", + UpyunPostUploadFailed = "UpyunPostUploadFailed", + QiniuChunkUploadFailed = "QiniuChunkUploadFailed", + FailedFinishOSSUpload = "FailedFinishOSSUpload", + FailedFinishQiniuUpload = "FailedFinishQiniuUpload", + FailedTransformResponse = "FailedTransformResponse", } const RETRY_ERROR_LIST = [ - UploaderErrorName.FailedCreateUploadSession, - UploaderErrorName.HTTPRequestFailed, - UploaderErrorName.LocalChunkUploadFailed, - UploaderErrorName.SlaveChunkUploadFailed, - UploaderErrorName.RequestCanceled, - UploaderErrorName.ProcessingTaskDuplicated, - UploaderErrorName.FailedTransformResponse, + UploaderErrorName.FailedCreateUploadSession, + UploaderErrorName.HTTPRequestFailed, + UploaderErrorName.LocalChunkUploadFailed, + UploaderErrorName.SlaveChunkUploadFailed, + UploaderErrorName.ProcessingTaskDuplicated, + UploaderErrorName.FailedTransformResponse, ]; const RETRY_CODE_LIST = [-1]; +const CONFLICT_ERROR_CODE = 40004; export class UploaderError implements Error { - public stack: string | undefined; - constructor(public name: UploaderErrorName, public message: string) { - this.stack = new Error().stack; - } + public stack: string | undefined; + constructor( + public name: UploaderErrorName, + public message: string, + ) { + this.stack = new Error().stack; + } - public Message(): string { - return this.message; - } + public Message(): string { + return this.message; + } - public Retryable(): boolean { - return RETRY_ERROR_LIST.includes(this.name); - } + public Retryable(): boolean { + return RETRY_ERROR_LIST.includes(this.name); + } } // 文件未通过存储策略验证 export class FileValidateError extends UploaderError { - // 未通过验证的文件属性 - public field: "size" | "suffix"; + // 未通过验证的文件属性 + public field: "size" | "suffix"; - // 对应的存储策略 - public policy: Policy; + // 对应的存储策略 + public policy: StoragePolicy; - constructor(message: string, field: "size" | "suffix", policy: Policy) { - super(UploaderErrorName.InvalidFile, message); - this.field = field; - this.policy = policy; + constructor( + message: string, + field: "size" | "suffix", + policy: StoragePolicy, + ) { + super(UploaderErrorName.InvalidFile, message); + this.field = field; + this.policy = policy; + } + + public Message(): string { + if (this.field == "size") { + return i18next.t(`uploader.sizeExceedLimitError`, { + max: sizeToString(this.policy.max_size), + }); } - public Message(): string { - if (this.field == "size") { - return i18next.t(`uploader.sizeExceedLimitError`, { - max: sizeToString(this.policy.maxSize), - }); - } - - return i18next.t(`uploader.suffixNotAllowedError`, { - supported: this.policy.allowedSuffix - ? this.policy.allowedSuffix.join(",") - : "*", - }); - } + return i18next.t(`uploader.suffixNotAllowedError`, { + supported: this.policy.allowed_suffix + ? this.policy.allowed_suffix.join(",") + : "*", + }); + } } // 未知存储策略 export class UnknownPolicyError extends UploaderError { - // 对应的存储策略 - public policy: Policy; + // 对应的存储策略 + public policy: StoragePolicy; - constructor(message: string, policy: Policy) { - super(UploaderErrorName.UnknownPolicyType, message); - this.policy = policy; - } + constructor(message: string, policy: StoragePolicy) { + super(UploaderErrorName.UnknownPolicyType, message); + this.policy = policy; + } } // 后端 API 出错 export class APIError extends UploaderError { - private appError: AppError; - constructor( - name: UploaderErrorName, - message: string, - protected response: Response - ) { - super(name, message); - this.appError = new AppError(response.msg, response.code, response.msg); - } + private appError: AppError; + constructor( + name: UploaderErrorName, + message: string, + protected response: Response, + ) { + super(name, message); + this.appError = new AppError(response); + } - public Message(): string { - return `${this.message}: ${this.appError.message}`; - } + public Message(): string { + return `${this.message}: ${this.appError.message}`; + } - public Retryable(): boolean { - return ( - super.Retryable() && RETRY_CODE_LIST.includes(this.response.code) - ); - } + public Retryable(): boolean { + return super.Retryable() && RETRY_CODE_LIST.includes(this.response.code); + } + + public IsConflictError(): boolean { + return this.response.code == CONFLICT_ERROR_CODE; + } } // 无法创建上传会话 export class CreateUploadSessionError extends APIError { - constructor(response: Response) { - super(UploaderErrorName.FailedCreateUploadSession, "", response); - } + constructor(response: Response) { + super(UploaderErrorName.FailedCreateUploadSession, "", response); + } - public Message(): string { - this.message = i18next.t(`uploader.createUploadSessionError`); - return super.Message(); - } + public Message(): string { + this.message = i18next.t(`uploader.createUploadSessionError`); + return super.Message(); + } } // 无法删除上传会话 export class DeleteUploadSessionError extends APIError { - constructor(response: Response) { - super(UploaderErrorName.FailedDeleteUploadSession, "", response); - } + constructor(response: Response) { + super(UploaderErrorName.FailedDeleteUploadSession, "", response); + } - public Message(): string { - this.message = i18next.t(`uploader.deleteUploadSessionError`); - return super.Message(); - } + public Message(): string { + this.message = i18next.t(`uploader.deleteUploadSessionError`); + return super.Message(); + } } // HTTP 请求出错 export class HTTPError extends UploaderError { - public response?: any; - constructor(public axiosErr: any, protected url: string) { - super(UploaderErrorName.HTTPRequestFailed, axiosErr.message); - this.response = axiosErr.response; - } + public response?: any; + constructor( + public axiosErr: any, + protected url: string, + ) { + super(UploaderErrorName.HTTPRequestFailed, axiosErr.message); + this.response = axiosErr.response; + } - public Message(): string { - return i18next.t(`uploader.requestError`, { - msg: this.axiosErr, - url: this.url, - }); - } + public Message(): string { + return i18next.t(`uploader.requestError`, { + msg: this.axiosErr, + url: this.url, + }); + } } // 本地分块上传失败 export class LocalChunkUploadError extends APIError { - constructor(response: Response, protected chunkIndex: number) { - super(UploaderErrorName.LocalChunkUploadFailed, "", response); - } + constructor( + response: Response, + protected chunkIndex: number, + ) { + super(UploaderErrorName.LocalChunkUploadFailed, "", response); + } - public Message(): string { - this.message = i18next.t(`uploader.chunkUploadError`, { - index: this.chunkIndex, - }); - return super.Message(); - } -} - -// 无法创建上传会话 -export class RequestCanceledError extends UploaderError { - constructor() { - super(UploaderErrorName.RequestCanceled, "Request canceled"); - } + public Message(): string { + this.message = i18next.t(`uploader.chunkUploadError`, { + index: this.chunkIndex, + }); + return super.Message(); + } } // 从机分块上传失败 export class SlaveChunkUploadError extends APIError { - constructor(response: Response, protected chunkIndex: number) { - super(UploaderErrorName.SlaveChunkUploadFailed, "", response); - } + constructor( + response: Response, + protected chunkIndex: number, + ) { + super(UploaderErrorName.SlaveChunkUploadFailed, "", response); + } - public Message(): string { - this.message = i18next.t(`uploader.chunkUploadError`, { - index: this.chunkIndex, - }); - return super.Message(); - } + public Message(): string { + this.message = i18next.t(`uploader.chunkUploadError`, { + index: this.chunkIndex, + }); + return super.Message(); + } } // 上传任务冲突 export class ProcessingTaskDuplicatedError extends UploaderError { - constructor() { - super( - UploaderErrorName.ProcessingTaskDuplicated, - "Processing task duplicated" - ); - } + constructor() { + super( + UploaderErrorName.ProcessingTaskDuplicated, + "Processing task duplicated", + ); + } - public Message(): string { - return i18next.t(`uploader.conflictError`); - } + public Message(): string { + return i18next.t(`uploader.conflictError`); + } } // OneDrive 分块上传失败 export class OneDriveChunkError extends UploaderError { - constructor(public response: OneDriveError) { - super( - UploaderErrorName.OneDriveChunkUploadFailed, - response.error.message - ); - } + constructor(public response: OneDriveError) { + super(UploaderErrorName.OneDriveChunkUploadFailed, response.error.message); + } - public Message(): string { - let msg = i18next.t(`uploader.chunkUploadErrorWithMsg`, { - msg: this.message, + public Message(): string { + let msg = i18next.t(`uploader.chunkUploadErrorWithMsg`, { + msg: this.message, + }); + + if (this.response.error.retryAfterSeconds != undefined) { + msg += + " " + + i18next.t(`uploader.chunkUploadErrorWithRetryAfter`, { + retryAfter: this.response.error.retryAfterSeconds, }); - - if (this.response.error.retryAfterSeconds != undefined){ - msg += " "+i18next.t(`uploader.chunkUploadErrorWithRetryAfter`, { - retryAfter: this.response.error.retryAfterSeconds, - }) - } - - return msg; } - public Retryable(): boolean { - return ( - super.Retryable() || this.response.error.retryAfterSeconds != undefined - ); - } + return msg; + } + + public Retryable(): boolean { + return ( + super.Retryable() || this.response.error.retryAfterSeconds != undefined + ); + } } // OneDrive 选择了空文件上传 export class OneDriveEmptyFileSelected extends UploaderError { - constructor() { - super(UploaderErrorName.OneDriveEmptyFile, "empty file not supported"); - } + constructor() { + super(UploaderErrorName.OneDriveEmptyFile, "empty file not supported"); + } - public Message(): string { - return i18next.t("uploader.emptyFileError"); - } + public Message(): string { + return i18next.t("uploader.emptyFileError"); + } } // OneDrive 无法完成文件上传 export class OneDriveFinishUploadError extends APIError { - constructor(response: Response) { - super(UploaderErrorName.FailedFinishOneDriveUpload, "", response); - } + constructor(response: Response) { + super(UploaderErrorName.FailedFinishOneDriveUpload, "", response); + } - public Message(): string { - this.message = i18next.t("uploader.finishUploadError"); - return super.Message(); - } + public Message(): string { + this.message = i18next.t("uploader.finishUploadError"); + return super.Message(); + } } // S3 类策略分块上传失败 export class S3LikeChunkError extends UploaderError { - constructor(public response: Document) { - super( - UploaderErrorName.S3LikeChunkUploadFailed, - response.getElementsByTagName("Message")[0].innerHTML - ); - } + constructor(public response: Document) { + super( + UploaderErrorName.S3LikeChunkUploadFailed, + response.getElementsByTagName("Message")[0].innerHTML, + ); + } - public Message(): string { - return i18next.t(`uploader.chunkUploadErrorWithMsg`, { - msg: this.message, - }); - } + public Message(): string { + return i18next.t(`uploader.chunkUploadErrorWithMsg`, { + msg: this.message, + }); + } } // OSS 完成传失败 export class S3LikeFinishUploadError extends UploaderError { - constructor(public response: Document) { - super( - UploaderErrorName.S3LikeChunkUploadFailed, - response.getElementsByTagName("Message")[0].innerHTML - ); - } + constructor(public response: Document) { + super( + UploaderErrorName.S3LikeChunkUploadFailed, + response.getElementsByTagName("Message")[0].innerHTML, + ); + } - public Message(): string { - return i18next.t(`uploader.ossFinishUploadError`, { - msg: this.message, - code: this.response.getElementsByTagName("Code")[0].innerHTML, - }); - } + public Message(): string { + return i18next.t(`uploader.ossFinishUploadError`, { + msg: this.message, + code: this.response.getElementsByTagName("Code")[0].innerHTML, + }); + } +} + +export interface ObsJsonError { + message: string; + code: string; +} + +export class ObsFinishUploadError extends UploaderError { + constructor(public response: ObsJsonError) { + super(UploaderErrorName.S3LikeChunkUploadFailed, response.message); + } + + public Message(): string { + return i18next.t(`uploader.ossFinishUploadError`, { + msg: this.message, + code: this.response.code, + }); + } } // qiniu 分块上传失败 export class QiniuChunkError extends UploaderError { - constructor(public response: QiniuError) { - super(UploaderErrorName.QiniuChunkUploadFailed, response.error); - } + constructor(public response: QiniuError) { + super(UploaderErrorName.QiniuChunkUploadFailed, response.error); + } - public Message(): string { - return i18next.t(`uploader.chunkUploadErrorWithMsg`, { - msg: this.message, - }); - } + public Message(): string { + return i18next.t(`uploader.chunkUploadErrorWithMsg`, { + msg: this.message, + }); + } } // qiniu 完成传失败 export class QiniuFinishUploadError extends UploaderError { - constructor(public response: QiniuError) { - super(UploaderErrorName.FailedFinishQiniuUpload, response.error); - } + constructor(public response: QiniuError) { + super(UploaderErrorName.FailedFinishQiniuUpload, response.error); + } - public Message(): string { - return i18next.t(`uploader.finishUploadErrorWithMsg`, { - msg: this.message, - }); - } + public Message(): string { + return i18next.t(`uploader.finishUploadErrorWithMsg`, { + msg: this.message, + }); + } } // COS 上传失败 export class COSUploadError extends UploaderError { - constructor(public response: Document) { - super( - UploaderErrorName.COSPostUploadFailed, - response.getElementsByTagName("Message")[0].innerHTML - ); - } + constructor(public response: Document) { + super( + UploaderErrorName.COSPostUploadFailed, + response.getElementsByTagName("Message")[0].innerHTML, + ); + } - public Message(): string { - return i18next.t(`uploader.cosUploadFailed`, { - msg: this.message, - code: this.response.getElementsByTagName("Code")[0].innerHTML, - }); - } + public Message(): string { + return i18next.t(`uploader.cosUploadFailed`, { + msg: this.message, + code: this.response.getElementsByTagName("Code")[0].innerHTML, + }); + } } // COS 无法完成上传回调 export class COSUploadCallbackError extends APIError { - constructor(response: Response) { - super(UploaderErrorName.COSUploadCallbackFailed, "", response); - } + constructor(response: Response) { + super(UploaderErrorName.COSUploadCallbackFailed, "", response); + } - public Message(): string { - this.message = i18next.t("uploader.finishUploadError"); - return super.Message(); - } + public Message(): string { + this.message = i18next.t("uploader.finishUploadError"); + return super.Message(); + } } // Upyun 上传失败 export class UpyunUploadError extends UploaderError { - constructor(public response: UpyunError) { - super(UploaderErrorName.UpyunPostUploadFailed, response.message); - } + constructor(public response: UpyunError) { + super(UploaderErrorName.UpyunPostUploadFailed, response.message); + } - public Message(): string { - return i18next.t("uploader.upyunUploadFailed", { - msg: this.message, - }); - } + public Message(): string { + return i18next.t("uploader.upyunUploadFailed", { + msg: this.message, + }); + } } // S3 无法完成上传回调 export class S3LikeUploadCallbackError extends APIError { - constructor(response: Response) { - super(UploaderErrorName.S3LikeUploadCallbackFailed, "", response); - } + constructor(response: Response) { + super(UploaderErrorName.S3LikeUploadCallbackFailed, "", response); + } - public Message(): string { - this.message = i18next.t("uploader.finishUploadError"); - return super.Message(); - } + public Message(): string { + this.message = i18next.t("uploader.finishUploadError"); + return super.Message(); + } } // 无法解析响应 export class TransformResponseError extends UploaderError { - constructor(private response: string, parseError: Error) { - super(UploaderErrorName.FailedTransformResponse, parseError.message); - } + constructor( + private response: string, + parseError: Error, + ) { + super(UploaderErrorName.FailedTransformResponse, parseError.message); + } - public Message(): string { - return i18next.t("uploader.parseResponseError", { - msg: this.message, - content: this.response, - }); - } + public Message(): string { + return i18next.t("uploader.parseResponseError", { + msg: this.message, + content: this.response, + }); + } } diff --git a/src/component/Uploader/core/index.ts b/src/component/Uploader/core/index.ts index 11ef391..bb8e4da 100644 --- a/src/component/Uploader/core/index.ts +++ b/src/component/Uploader/core/index.ts @@ -1,236 +1,273 @@ -import { Policy, PolicyType, Task, TaskType } from "./types"; -import Logger, { LogLevel } from "./logger"; +import { PolicyType, StoragePolicy } from "../../../api/explorer.ts"; +import { defaultPath } from "../../../hooks/useNavigation.tsx"; import { UnknownPolicyError, UploaderError, UploaderErrorName } from "./errors"; -import Base from "./uploader/base"; +import Logger, { LogLevel } from "./logger"; +import { Task, TaskType } from "./types"; +import Base, { MessageColor } from "./uploader/base"; +import COS from "./uploader/cos"; import Local from "./uploader/local"; -import { Pool } from "./utils/pool"; -import { - cleanupResumeCtx, - getAllFileEntries, - getDirectoryUploadDst, - getFileInput, - isFileDrop, - listResumeCtx, -} from "./utils"; -import Remote from "./uploader/remote"; +import OBS from "./uploader/obs.ts"; import OneDrive from "./uploader/onedrive"; import OSS from "./uploader/oss"; -import Qiniu from "./uploader/qiniu"; -import COS from "./uploader/cos"; -import Upyun from "./uploader/upyun"; -import S3 from "./uploader/s3"; import ResumeHint from "./uploader/placeholder"; +import Qiniu from "./uploader/qiniu"; +import Remote from "./uploader/remote"; +import S3 from "./uploader/s3"; +import Upyun from "./uploader/upyun"; +import { + cleanupResumeCtx, + getAllFileEntries, + getDirectoryUploadDst, + getFileInput, + isFileDrop, + listResumeCtx, +} from "./utils"; +import { Pool } from "./utils/pool"; export interface Option { - logLevel: LogLevel; - concurrentLimit: number; - dropZone?: HTMLElement; - onDropOver?: (e: DragEvent) => void; - onDropLeave?: (e: DragEvent) => void; - onToast: (type: string, msg: string) => void; - onDropFileAdded?: (uploaders: Base[]) => void; + logLevel: LogLevel; + concurrentLimit: number; + overwrite?: boolean; + dropZone: HTMLElement | null; + onDropOver?: (e: DragEvent) => void; + onDropLeave?: (e: DragEvent) => void; + onToast: (type: MessageColor, msg: string) => void; + onPoolEmpty?: () => void; + onProactiveFileAdded?: (uploaders: Base[]) => void; } export enum SelectType { - File, - Directory, + File, + Directory, } export default class UploadManager { - public logger: Logger; - public pool: Pool; - private static id = 0; - private policy?: Policy; - private fileInput: HTMLInputElement; - private directoryInput: HTMLInputElement; - private id = ++UploadManager.id; - // used for proactive upload (drop, paste) - private currentPath = "/"; + public logger: Logger; + public pool: Pool; + public overwrite?: boolean; + private static id = 0; + private policy?: StoragePolicy; + private fileInput: HTMLInputElement; + private directoryInput: HTMLInputElement; + private id = ++UploadManager.id; + // used for proactive upload (drop, paste) + private currentPath?: string; - constructor(private o: Option) { - this.logger = new Logger(o.logLevel, "MANAGER"); - this.logger.info(`Initialized with log level: ${o.logLevel}`); + constructor(private o: Option) { + this.logger = new Logger(o.logLevel, "MANAGER"); + this.logger.info(`Initialized with log level: ${o.logLevel}`); - this.pool = new Pool(o.concurrentLimit); - this.fileInput = getFileInput(this.id, false); - this.directoryInput = getFileInput(this.id, true); + this.pool = new Pool(o.concurrentLimit, o.onPoolEmpty); + this.fileInput = getFileInput(this.id, false); + this.directoryInput = getFileInput(this.id, true); + this.overwrite = o.overwrite; - if (o.dropZone) { - this.logger.info(`Drag and drop container set to:`, o.dropZone); - o.dropZone.addEventListener("dragenter", (e) => { - if (isFileDrop(e)) { - e.preventDefault(); - if (o.onDropOver) { - o.onDropOver(e); - } - } - }); - - o.dropZone.addEventListener("dragleave", (e) => { - if (isFileDrop(e)) { - e.preventDefault(); - if (o.onDropLeave) { - o.onDropLeave(e); - } - } - }); - - o.dropZone.addEventListener("drop", this.onFileDroppedIn); + if (o.dropZone) { + this.logger.info(`Drag and drop container set to:`, o.dropZone); + o.dropZone.addEventListener("dragenter", (e) => { + if (isFileDrop(e) && this.currentPath) { + e.preventDefault(); + if (o.onDropOver) { + o.onDropOver(e); + } } + }); + + o.dropZone.addEventListener("dragleave", (e) => { + if (isFileDrop(e) && this.currentPath) { + e.preventDefault(); + if (o.onDropLeave) { + o.onDropLeave(e); + } + } + }); + + o.dropZone.addEventListener("drop", this.onFileDroppedIn); } - changeConcurrentLimit = (newLimit: number) => { - this.pool.limit = newLimit; - }; + window.addEventListener("beforeunload", this.beforeLeave); + } - dispatchUploader(task: Task): Base { - if (task.type == TaskType.resumeHint) { - return new ResumeHint(task, this); - } + beforeLeave = (e: BeforeUnloadEvent) => { + if (this.pool.processing.length == 0) { + return; + } + var confirmationMessage = + "It looks like you have been editing something. " + + "If you leave before saving, your changes will be lost."; - switch (task.policy.type) { - case PolicyType.local: - return new Local(task, this); - case PolicyType.remote: - return new Remote(task, this); - case PolicyType.onedrive: - return new OneDrive(task, this); - case PolicyType.oss: - return new OSS(task, this); - case PolicyType.qiniu: - return new Qiniu(task, this); - case PolicyType.cos: - return new COS(task, this); - case PolicyType.upyun: - return new Upyun(task, this); - case PolicyType.s3: - return new S3(task, this); - default: - throw new UnknownPolicyError( - "Unknown policy type.", - task.policy - ); - } + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + }; + + changeConcurrentLimit = (newLimit: number) => { + this.pool.limit = newLimit; + }; + + dispatchUploader(task: Task): Base { + if (task.type == TaskType.resumeHint) { + return new ResumeHint(task, this); } - // 设定当前存储策略 - public setPolicy(p: Policy, path: string) { - this.policy = p; - this.currentPath = path; - if (p == undefined) { - this.logger.info(`Currently no policy selected`); - return; - } - - this.logger.info(`Switching policy to:`, p); - - if (p.allowedSuffix != undefined && p.allowedSuffix.length > 0) { - const acceptVal = p.allowedSuffix - .map((v) => { - return "." + v; - }) - .join(","); - this.logger.info(`Set allowed file suffix to ${acceptVal}`); - this.fileInput.setAttribute("accept", acceptVal); - } else { - this.logger.info(`Set allowed file suffix to *`); - this.fileInput.removeAttribute("accept"); - } + if (task.policy.relay) { + this.logger.info("Upload relay is enabled, cast to local uploader."); + return new Local(task, this); } - // 选择文件 - public select = (dst: string, type = SelectType.File): Promise => { - return new Promise((resolve) => { - if (this.policy == undefined) { - this.logger.warn( - `Calling file selector while no policy is set` - ); - throw new UploaderError( - UploaderErrorName.NoPolicySelected, - "No policy selected." - ); - } + switch (task.policy.type) { + case PolicyType.local: + return new Local(task, this); + case PolicyType.remote: + return new Remote(task, this); + case PolicyType.onedrive: + return new OneDrive(task, this); + case PolicyType.oss: + return new OSS(task, this); + case PolicyType.qiniu: + return new Qiniu(task, this); + case PolicyType.cos: + return new COS(task, this); + case PolicyType.upyun: + return new Upyun(task, this); + case PolicyType.s3: + return new S3(task, this); + case PolicyType.obs: + return new OBS(task, this); + default: + throw new UnknownPolicyError("Unknown policy type.", task.policy); + } + } - this.fileInput.onchange = (ev: Event) => - this.fileSelectCallback(ev, dst, resolve); - this.directoryInput.onchange = (ev: Event) => - this.fileSelectCallback(ev, dst, resolve); - this.fileInput.value = ""; - this.directoryInput.value = ""; - type == SelectType.File - ? this.fileInput.click() - : this.directoryInput.click(); - }); - }; + // 设定当前存储策略 + public setPolicy(p?: StoragePolicy, path?: string) { + this.policy = p; + this.currentPath = path; + this.logger.info(`Switching path to:`, path); + if (p == undefined) { + this.logger.info(`Currently no policy selected`); + return; + } - public resumeTasks = (): Base[] => { - const tasks = listResumeCtx(this.logger); - if (tasks.length > 0) { - this.logger.info( - `Resumed ${tasks.length} unfinished task(s) from local storage:`, - tasks - ); - } - return tasks - .filter( - (t) => - t.chunkProgress.length > 0 && t.chunkProgress[0].loaded > 0 - ) - .map((t) => - this.dispatchUploader({ ...t, type: TaskType.resumeHint }) - ); - }; + this.logger.info(`Switching policy to:`, p); - public cleanupSessions = () => { - cleanupResumeCtx(this.logger); - }; + if (p.allowed_suffix != undefined && p.allowed_suffix.length > 0) { + const acceptVal = p.allowed_suffix + .map((v) => { + return "." + v; + }) + .join(","); + this.logger.info(`Set allowed file suffix to ${acceptVal}`); + this.fileInput.setAttribute("accept", acceptVal); + } else { + this.logger.info(`Set allowed file suffix to *`); + this.fileInput.removeAttribute("accept"); + } + } - private fileSelectCallback = ( - ev: Event | File[], - dst: string, - resolve: (value?: Base[] | PromiseLike | undefined) => void - ) => { - let files: File[] = []; - if (ev instanceof Event) { - const target = ev.target as HTMLInputElement; - if (!ev || !target || !target.files) return; - if (target.files.length > 0) { - files = Array.from(target.files); - } - } else { - files = ev as File[]; - } + // 选择文件 + public select = (dst: string, type = SelectType.File): Promise => { + return new Promise((resolve) => { + if (this.policy == undefined) { + this.logger.warn(`Calling file selector while no policy is set`); + throw new UploaderError( + UploaderErrorName.NoPolicySelected, + "No policy selected.", + ); + } - if (files.length > 0) { - resolve( - files.map( - (file): Base => - this.dispatchUploader({ - type: TaskType.file, - policy: this.policy as Policy, - dst: getDirectoryUploadDst(dst, file), - file: file, - size: file.size, - name: file.name, - chunkProgress: [], - resumed: false, - }) - ) - ); - } - }; + this.fileInput.onchange = (ev: Event) => this.addFiles(ev, dst, resolve); + this.directoryInput.onchange = (ev: Event) => + this.addFiles(ev, dst, resolve); + this.fileInput.value = ""; + this.directoryInput.value = ""; + type == SelectType.File + ? this.fileInput.click() + : this.directoryInput.click(); + }); + }; - private onFileDroppedIn = async (e: DragEvent) => { - const containFile = - e.dataTransfer && e.dataTransfer.types.includes("Files"); - if (containFile) { - this.o.onDropLeave && this.o.onDropLeave(e); - const items = await getAllFileEntries(e.dataTransfer!.items); - console.log(items); - const uploaders = await new Promise((resolve) => - this.fileSelectCallback(items, this.currentPath, resolve) - ); - this.o.onDropFileAdded && this.o.onDropFileAdded(uploaders); - } - }; + public resumeTasks = (): Base[] => { + const tasks = listResumeCtx(this.logger); + if (tasks.length > 0) { + this.logger.info( + `Resumed ${tasks.length} unfinished task(s) from local storage:`, + tasks, + ); + } + return tasks + .filter( + (t) => t.chunkProgress.length > 0 && t.chunkProgress[0].loaded > 0, + ) + .map((t) => this.dispatchUploader({ ...t, type: TaskType.resumeHint })); + }; + + public cleanupSessions = () => { + cleanupResumeCtx(this.logger); + }; + + public addRawFiles = async ( + files: File[], + getName?: (file: File) => string, + ) => { + if (!this.currentPath) { + return; + } + const uploaders = await new Promise((resolve) => + this.addFiles(files, this.currentPath ?? defaultPath, resolve, getName), + ); + this.o.onProactiveFileAdded && this.o.onProactiveFileAdded(uploaders); + }; + + private addFiles = ( + ev: Event | File[], + dst: string, + resolve: (value: Base[] | PromiseLike) => void, + getName?: (file: File) => string, + ) => { + let files: File[] = []; + if (ev instanceof Event) { + const target = ev.target as HTMLInputElement; + if (!ev || !target || !target.files) return; + if (target.files.length > 0) { + files = Array.from(target.files); + } + } else { + files = ev as File[]; + } + + if (files.length > 0) { + resolve( + files.map( + (file): Base => + this.dispatchUploader({ + type: TaskType.file, + policy: this.policy as StoragePolicy, + dst: getDirectoryUploadDst(dst, file), + file: file, + size: file.size, + overwrite: this.overwrite, + name: getName ? getName(file) : file.name, + chunkProgress: [], + resumed: false, + }), + ), + ); + } + }; + + private onFileDroppedIn = async (e: DragEvent) => { + if (!this.currentPath) { + return; + } + const containFile = + e.dataTransfer && e.dataTransfer.types.includes("Files"); + if (containFile) { + this.o.onDropLeave && this.o.onDropLeave(e); + const items = await getAllFileEntries(e.dataTransfer!.items); + const uploaders = await new Promise((resolve) => + this.addFiles(items, this.currentPath, resolve), + ); + this.o.onProactiveFileAdded && this.o.onProactiveFileAdded(uploaders); + } + }; } diff --git a/src/component/Uploader/core/types.ts b/src/component/Uploader/core/types.ts index 7ab6051..9aa4a56 100644 --- a/src/component/Uploader/core/types.ts +++ b/src/component/Uploader/core/types.ts @@ -1,116 +1,68 @@ import { ChunkProgress } from "./uploader/chunk"; - -export enum PolicyType { - local = "local", - remote = "remote", - oss = "oss", - qiniu = "qiniu", - onedrive = "onedrive", - cos = "cos", - upyun = "upyun", - s3 = "s3", -} - -export interface Policy { - id: number; - name: string; - allowedSuffix: Nullable; - maxSize: number; - type: PolicyType; -} +import { StoragePolicy, UploadCredential } from "../../../api/explorer.ts"; export enum TaskType { - file, - resumeHint, + file, + resumeHint, } export interface Task { - type: TaskType; - name: string; - size: number; - policy: Policy; - dst: string; - file: File; - child?: Task[]; - session?: UploadCredential; - chunkProgress: ChunkProgress[]; - resumed: boolean; + type: TaskType; + name: string; + size: number; + policy: StoragePolicy; + dst: string; + file: File; + child?: Task[]; + session?: UploadCredential; + chunkProgress: ChunkProgress[]; + resumed: boolean; + overwrite?: boolean; } type Nullable = T | null; -export interface Response { - code: number; - data: T; - msg: string; - error: string; -} - -export interface UploadSessionRequest { - path: string; - size: number; - name: string; - policy_id: number; - last_modified?: number; - - mime_type?: string; -} - -export interface UploadCredential { - sessionID: string; - expires: number; - chunkSize: number; - uploadURLs: string[]; - credential: string; - uploadID: string; - callback: string; - policy: string; - ak: string; - keyTime: string; - path: string; - completeURL: string; -} - export interface OneDriveError { - error: { - code: string; - message: string; - innererror?: { - code: string; - }; - retryAfterSeconds?: number; + error: { + code: string; + message: string; + innererror?: { + code: string; }; + retryAfterSeconds?: number; + }; } export interface OneDriveChunkResponse { - expirationDateTime: string; - nextExpectedRanges: string[]; + expirationDateTime: string; + nextExpectedRanges: string[]; } export interface QiniuChunkResponse { - etag: string; - md5: string; + etag: string; + md5: string; } export interface QiniuError { - error: string; + error: string; } export interface QiniuPartsInfo { - etag: string; - partNumber: number; + etag: string; + partNumber: number; } export interface QiniuFinishUploadRequest { - parts: QiniuPartsInfo[]; + parts: QiniuPartsInfo[]; + mimeType?: string; } export interface UpyunError { - message: string; - code: number; + message: string; + code: number; } export interface S3Part { - ETag: string; - PartNumber: number; + ETag: string; + PartNumber: number; } diff --git a/src/component/Uploader/core/uploader/base.ts b/src/component/Uploader/core/uploader/base.ts index 275445d..7e914a2 100644 --- a/src/component/Uploader/core/uploader/base.ts +++ b/src/component/Uploader/core/uploader/base.ts @@ -1,235 +1,263 @@ // 所有 Uploader 的基类 -import { PolicyType, Task } from "../types"; +import { Task } from "../types"; import UploadManager from "../index"; import Logger from "../logger"; import { validate } from "../utils/validator"; import { CancelToken } from "../utils/request"; -import axios, { CancelTokenSource } from "axios"; +import axios, { CanceledError, CancelTokenSource } from "axios"; import { createUploadSession, deleteUploadSession } from "../api"; import * as utils from "../utils"; -import { RequestCanceledError, UploaderError } from "../errors"; +import { UploaderError } from "../errors"; +import { PolicyType } from "../../../../api/explorer.ts"; +import CrUri from "../../../../util/uri.ts"; export enum Status { - added, - resumable, - initialized, - queued, - preparing, - processing, - finishing, - finished, - error, - canceled, + added, + resumable, + initialized, + queued, + preparing, + processing, + finishing, + finished, + error, + canceled, } export interface UploadHandlers { - onTransition: (newStatus: Status) => void; - onError: (err: Error) => void; - onProgress: (data: UploadProgress) => void; - onMsg: (msg: string, color: string) => void; + onTransition: (newStatus: Status) => void; + onError: (err: Error) => void; + onProgress: (data: UploadProgress) => void; + onMsg: (msg: string, color: MessageColor) => void; } +export type MessageColor = + | "error" + | "default" + | "success" + | "warning" + | "info" + | "loading" + | undefined; export interface UploadProgress { - total: ProgressCompose; - chunks?: ProgressCompose[]; + total: ProgressCompose; + chunks?: ProgressCompose[]; } export interface ProgressCompose { - size: number; - loaded: number; - percent: number; - fromCache?: boolean; + size: number; + loaded: number; + percent: number; + fromCache?: boolean; } export interface Progress { - total: number; - loaded: number; + total?: number; + loaded: number; +} + +export interface RetryOption { + overwrite?: boolean; + new_prefix?: string; } const resumePolicy = [ - PolicyType.local, - PolicyType.remote, - PolicyType.qiniu, - PolicyType.oss, - PolicyType.onedrive, - PolicyType.s3, + PolicyType.local, + PolicyType.remote, + PolicyType.qiniu, + PolicyType.oss, + PolicyType.onedrive, + PolicyType.s3, ]; const deleteUploadSessionDelay = 500; export default abstract class Base { - public child?: Base[]; - public status: Status = Status.added; - public error?: Error; + public child?: Base[]; + public status: Status = Status.added; + public error?: Error; + public progress?: UploadProgress; - public id = ++Base.id; - private static id = 0; + public id = ++Base.id; + private static id = 0; - protected logger: Logger; - protected subscriber: UploadHandlers; - // 用于取消请求 - protected cancelToken: CancelTokenSource = CancelToken.source(); - protected progress: UploadProgress; + protected logger: Logger; + protected subscriber: UploadHandlers; + // 用于取消请求 + protected cancelToken: CancelTokenSource = CancelToken.source(); - public lastTime = Date.now(); - public startTime = Date.now(); + public lastTime = Date.now(); + public startTime = Date.now(); - constructor(public task: Task, protected manager: UploadManager) { - this.logger = new Logger( - this.manager.logger.level, - "UPLOADER", - this.id - ); - this.logger.info("Initialize new uploader for task: ", task); - this.subscriber = { - /* eslint-disable @typescript-eslint/no-empty-function */ - onTransition: (newStatus: Status) => {}, - onError: (err: Error) => {}, - onProgress: (data: UploadProgress) => {}, - onMsg: (msg, color: string) => {}, - /* eslint-enable @typescript-eslint/no-empty-function */ - }; + constructor( + public task: Task, + protected manager: UploadManager, + ) { + this.logger = new Logger(this.manager.logger.level, "UPLOADER", this.id); + this.logger.info("Initialize new uploader for task: ", task); + this.subscriber = { + /* eslint-disable @typescript-eslint/no-empty-function */ + onTransition: (_newStatus: Status) => {}, + onError: (_err: Error) => {}, + onProgress: (_data: UploadProgress) => {}, + onMsg: (_msg, _color: MessageColor) => {}, + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + } + + public subscribe = (handlers: UploadHandlers) => { + this.subscriber = handlers; + }; + + public start = async () => { + this.logger.info("Activate uploading task"); + this.transit(Status.initialized); + this.lastTime = this.startTime = Date.now(); + + try { + validate(this.task.file, this.task.policy); + } catch (e) { + this.logger.error("File validate failed with error:", e); + this.setError(e as Error); + return; } - public subscribe = (handlers: UploadHandlers) => { - this.subscriber = handlers; - }; + this.logger.info("Enqueued in manager pool"); + this.transit(Status.queued); + this.manager.pool.enqueue(this).catch((e) => { + this.logger.info("Upload task failed with error:", e); + this.setError(e); + }); + }; - public start = async () => { - this.logger.info("Activate uploading task"); - this.transit(Status.initialized); - this.lastTime = this.startTime = Date.now(); - - try { - validate(this.task.file, this.task.policy); - } catch (e) { - this.logger.error("File validate failed with error:", e); - this.setError(e); - return; - } - - this.logger.info("Enqueued in manager pool"); - this.transit(Status.queued); - this.manager.pool.enqueue(this).catch((e) => { - this.logger.info("Upload task failed with error:", e); - this.setError(e); - }); - }; - - public run = async () => { - this.logger.info("Start upload task, create upload session..."); - this.transit(Status.preparing); - const cachedInfo = utils.getResumeCtx(this.task, this.logger); - if (cachedInfo == null) { - this.task.session = await createUploadSession( - { - path: this.task.dst, - size: this.task.file.size, - name: this.task.file.name, - policy_id: this.task.policy.id, - last_modified: this.task.file.lastModified, - mime_type: this.task.file.type, - }, - this.cancelToken.token - ); - this.logger.info("Upload session created:", this.task.session); - } else { - this.task.session = cachedInfo.session; - this.task.resumed = true; - this.task.chunkProgress = cachedInfo.chunkProgress; - this.logger.info("Resume upload from cached ctx:", cachedInfo); - } - - this.transit(Status.processing); - await this.upload(); - await this.afterUpload(); - utils.removeResumeCtx(this.task, this.logger); - this.transit(Status.finished); - this.logger.info("Upload task completed"); - }; - - public abstract async upload(): Promise; - protected async afterUpload(): Promise { - return; + public run = async () => { + this.logger.info("Start upload task, create upload session..."); + this.transit(Status.preparing); + const cachedInfo = utils.getResumeCtx(this.task, this.logger); + if (cachedInfo == null) { + const crUri = new CrUri(this.task.dst); + crUri.join(this.task.name); + this.task.session = await createUploadSession( + { + uri: crUri.toString(), + size: this.task.file.size, + policy_id: this.task.policy.id, + last_modified: this.task.file.lastModified, + mime_type: this.task.file.type, + entity_type: this.task.overwrite ? "version" : undefined, + }, + this.cancelToken.token, + ); + this.logger.info("Upload session created:", this.task.session); + } else { + this.task.session = cachedInfo.session; + this.task.resumed = true; + this.task.chunkProgress = cachedInfo.chunkProgress; + this.logger.info("Resume upload from cached ctx:", cachedInfo); } - public cancel = async () => { - if (this.status === Status.finished) { - return; - } + this.transit(Status.processing); + await this.upload(); + await this.afterUpload(); + utils.removeResumeCtx(this.task, this.logger); + this.transit(Status.finished); + this.logger.info("Upload task completed"); + }; - this.cancelToken.cancel(); - await this.cancelUploadSession(); - this.transit(Status.canceled); + public abstract upload(): Promise; + protected async afterUpload(): Promise { + return; + } + + public cancel = async () => { + if (this.status === Status.finished) { + return; + } + + this.cancelToken.cancel(); + await this.cancelUploadSession(); + this.transit(Status.canceled); + }; + + public retry = (opt?: RetryOption) => { + this.reset(); + if (opt?.overwrite) { + this.task.overwrite = true; + } else if (opt?.new_prefix) { + this.task.name = opt.new_prefix + this.task.name; + } + this.start(); + }; + + protected reset = () => { + this.cancelToken = axios.CancelToken.source(); + this.progress = { + total: { + size: 0, + loaded: 0, + percent: 0, + }, }; + }; - public reset = () => { - this.cancelToken = axios.CancelToken.source(); - this.progress = { - total: { - size: 0, - loaded: 0, - percent: 0, - }, - }; + protected setError(e: Error) { + if (e instanceof CanceledError) { + return; + } + + if ( + !(e instanceof UploaderError && e.Retryable()) || + !resumePolicy.includes(this.task.policy.type) + ) { + this.logger.warn("Non-resume error occurs, clean resume ctx cache"); + this.cancelUploadSession(); + } + + this.status = Status.error; + this.error = e; + this.subscriber.onError(e); + } + + protected cancelUploadSession = (): Promise => { + return new Promise((resolve) => { + utils.removeResumeCtx(this.task, this.logger); + if (this.task.session) { + setTimeout(() => { + deleteUploadSession( + this.task.session!?.session_id, + this.task.session!?.uri, + ) + .catch((e) => { + this.logger.warn("Failed to cancel upload session: ", e); + }) + .finally(() => { + resolve(); + }); + }, deleteUploadSessionDelay); + } else { + resolve(); + } + }); + }; + + protected transit(status: Status) { + this.status = status; + this.subscriber.onTransition(status); + } + + public getProgressInfoItem( + loaded: number, + size: number, + fromCache?: boolean, + ): ProgressCompose { + return { + size, + loaded, + percent: (loaded / size) * 100, + ...(fromCache == null ? {} : { fromCache }), }; + } - protected setError(e: Error) { - if ( - !(e instanceof UploaderError && e.Retryable()) || - !resumePolicy.includes(this.task.policy.type) - ) { - this.logger.warn("Non-resume error occurs, clean resume ctx cache"); - this.cancelUploadSession(); - } - - if (!(e instanceof RequestCanceledError)) { - this.status = Status.error; - this.error = e; - this.subscriber.onError(e); - } - } - - protected cancelUploadSession = (): Promise => { - return new Promise((resolve) => { - utils.removeResumeCtx(this.task, this.logger); - if (this.task.session) { - setTimeout(() => { - deleteUploadSession(this.task.session!?.sessionID) - .catch((e) => { - this.logger.warn( - "Failed to cancel upload session: ", - e - ); - }) - .finally(() => { - resolve(); - }); - }, deleteUploadSessionDelay); - } else { - resolve(); - } - }); - }; - - protected transit(status: Status) { - this.status = status; - this.subscriber.onTransition(status); - } - - public getProgressInfoItem( - loaded: number, - size: number, - fromCache?: boolean - ): ProgressCompose { - return { - size, - loaded, - percent: (loaded / size) * 100, - ...(fromCache == null ? {} : { fromCache }), - }; - } - - public key(): string { - return utils.getResumeCtxKey(this.task); - } + public key(): string { + return utils.getResumeCtxKey(this.task); + } } diff --git a/src/component/Uploader/core/uploader/chunk.ts b/src/component/Uploader/core/uploader/chunk.ts index 02cb302..cc68d1e 100644 --- a/src/component/Uploader/core/uploader/chunk.ts +++ b/src/component/Uploader/core/uploader/chunk.ts @@ -1,84 +1,81 @@ -import Base, { Status, UploadProgress } from "./base"; +import Base from "./base"; import * as utils from "../utils"; -import { Task, TaskType } from "../types"; -import UploadManager from "../index"; -import Logger from "../logger"; export interface ChunkProgress { - loaded: number; - index: number; - etag?: string; + loaded: number; + index: number; + etag?: string; } export interface ChunkInfo { - chunk: Blob; - index: number; + chunk: Blob; + index: number; } export default abstract class Chunk extends Base { - protected chunks: Blob[]; + protected chunks: Blob[]; - public upload = async () => { - this.logger.info("Preparing uploading file chunks."); - this.initBeforeUploadChunks(); + public upload = async () => { + this.logger.info("Preparing uploading file chunks."); + this.initBeforeUploadChunks(); - this.logger.info("Starting uploading file chunks:", this.chunks); + this.logger.info("Starting uploading file chunks:", this.chunks); + this.updateLocalCache(); + for (let i = 0; i < this.chunks.length; i++) { + if ( + this.task.chunkProgress[i].loaded < this.chunks[i].size || + this.chunks[i].size == 0 + ) { + await this.uploadChunk({ chunk: this.chunks[i], index: i }); + this.logger.info(`Chunk [${i}] uploaded.`); this.updateLocalCache(); - for (let i = 0; i < this.chunks.length; i++) { - if ( - this.task.chunkProgress[i].loaded < this.chunks[i].size || - this.chunks[i].size == 0 - ) { - await this.uploadChunk({ chunk: this.chunks[i], index: i }); - this.logger.info(`Chunk [${i}] uploaded.`); - this.updateLocalCache(); - } - } - }; + } + } + }; - private initBeforeUploadChunks() { - this.chunks = utils.getChunks( - this.task.file, - this.task.session?.chunkSize + private initBeforeUploadChunks() { + this.chunks = utils.getChunks( + this.task.file, + this.task.session?.chunk_size, + ); + const cachedInfo = utils.getResumeCtx(this.task, this.logger); + if (cachedInfo == null) { + this.task.chunkProgress = this.chunks.map( + (value, index): ChunkProgress => ({ + loaded: 0, + index, + }), + ); + } + + this.notifyResumeProgress(); + } + + protected abstract async uploadChunk(chunkInfo: ChunkInfo): Promise; + + protected updateChunkProgress(loaded: number, index: number) { + this.task.chunkProgress[index].loaded = loaded; + this.notifyResumeProgress(); + } + + private notifyResumeProgress() { + this.progress = { + total: this.getProgressInfoItem( + utils.sumChunk(this.task.chunkProgress), + this.task.file.size + 1, + ), + chunks: this.chunks.map((chunk, index) => { + return this.getProgressInfoItem( + this.task.chunkProgress[index].loaded, + chunk.size, + false, ); - const cachedInfo = utils.getResumeCtx(this.task, this.logger); - if (cachedInfo == null) { - this.task.chunkProgress = this.chunks.map( - (value, index): ChunkProgress => ({ - loaded: 0, - index, - }) - ); - } + }), + }; + this.subscriber.onProgress(this.progress); + } - this.notifyResumeProgress(); - } - - protected abstract async uploadChunk(chunkInfo: ChunkInfo): Promise; - - protected updateChunkProgress(loaded: number, index: number) { - this.task.chunkProgress[index].loaded = loaded; - this.notifyResumeProgress(); - } - - private notifyResumeProgress() { - this.progress = { - total: this.getProgressInfoItem( - utils.sumChunk(this.task.chunkProgress), - this.task.file.size + 1 - ), - chunks: this.chunks.map((chunk, index) => { - return this.getProgressInfoItem( - this.task.chunkProgress[index].loaded, - chunk.size, - false - ); - }), - }; - this.subscriber.onProgress(this.progress); - } - - private updateLocalCache() { - utils.setResumeCtx(this.task, this.logger); - } + private updateLocalCache() { + utils.setResumeCtx(this.task, this.logger); + } } diff --git a/src/component/Uploader/core/uploader/cos.ts b/src/component/Uploader/core/uploader/cos.ts index 57e0aaa..950fbc0 100644 --- a/src/component/Uploader/core/uploader/cos.ts +++ b/src/component/Uploader/core/uploader/cos.ts @@ -1,38 +1,42 @@ -import Base, { Status } from "./base"; -import { cosFormUploadChunk, cosUploadCallback } from "../api"; +import { Status } from "./base"; +import { + s3LikeFinishUpload, + s3LikeUploadCallback, + s3LikeUploadChunk, +} from "../api"; +import Chunk, { ChunkInfo } from "./chunk.ts"; +import { PolicyType } from "../../../../api/explorer.ts"; -export default class COS extends Base { - public upload = async () => { - this.logger.info("Starting uploading file stream:", this.task.file); - await cosFormUploadChunk( - this.task.session?.uploadURLs[0]!, - this.task.file, - this.task.session?.policy!, - this.task.session?.path!, - this.task.session?.callback!, - this.task.session?.sessionID!, - this.task.session?.keyTime!, - this.task.session?.credential!, - this.task.session?.ak!, - (p) => { - this.subscriber.onProgress({ - total: this.getProgressInfoItem(p.loaded, p.total), - }); - }, - this.cancelToken.token - ); - }; +export default class COS extends Chunk { + protected async uploadChunk(chunkInfo: ChunkInfo) { + const etag = await s3LikeUploadChunk( + this.task.session?.upload_urls[chunkInfo.index]!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); - protected async afterUpload(): Promise { - this.transit(Status.finishing); - this.logger.info(`Sending COS upload callback...`); - try { - await cosUploadCallback( - this.task.session!.sessionID, - this.cancelToken.token - ); - } catch (e) { - this.logger.warn(`Failed to finish COS upload:`, e); - } - } + this.task.chunkProgress[chunkInfo.index].etag = etag; + } + + protected async afterUpload(): Promise { + this.logger.info(`Finishing multipart upload...`); + this.transit(Status.finishing); + await s3LikeFinishUpload( + this.task.session!.completeURL, + false, + this.task.chunkProgress, + this.cancelToken.token, + { "x-cos-forbid-overwrite": "true" }, + ); + + this.logger.info(`Sending S3-like upload callback...`); + return s3LikeUploadCallback( + this.task.session!.session_id, + this.task.session!.callback_secret, + PolicyType.cos, + ); + } } diff --git a/src/component/Uploader/core/uploader/local.ts b/src/component/Uploader/core/uploader/local.ts index 7917d15..5539776 100644 --- a/src/component/Uploader/core/uploader/local.ts +++ b/src/component/Uploader/core/uploader/local.ts @@ -2,14 +2,14 @@ import Chunk, { ChunkInfo } from "./chunk"; import { localUploadChunk } from "../api"; export default class Local extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - return localUploadChunk( - this.task.session?.sessionID!, - chunkInfo, - (p) => { - this.updateChunkProgress(p.loaded, chunkInfo.index); - }, - this.cancelToken.token - ); - } + protected async uploadChunk(chunkInfo: ChunkInfo) { + return localUploadChunk( + this.task.session?.session_id!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); + } } diff --git a/src/component/Uploader/core/uploader/obs.ts b/src/component/Uploader/core/uploader/obs.ts new file mode 100644 index 0000000..d40a8a6 --- /dev/null +++ b/src/component/Uploader/core/uploader/obs.ts @@ -0,0 +1,28 @@ +import Chunk, { ChunkInfo } from "./chunk"; +import { obsFinishUpload, s3LikeUploadChunk } from "../api"; +import { Status } from "./base"; + +export default class OBS extends Chunk { + protected async uploadChunk(chunkInfo: ChunkInfo) { + const etag = await s3LikeUploadChunk( + this.task.session?.upload_urls[chunkInfo.index]!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); + + this.task.chunkProgress[chunkInfo.index].etag = etag; + } + + protected async afterUpload(): Promise { + this.logger.info(`Finishing multipart upload...`); + this.transit(Status.finishing); + return obsFinishUpload( + this.task.session!.completeURL, + this.task.chunkProgress, + this.cancelToken.token, + ); + } +} diff --git a/src/component/Uploader/core/uploader/onedrive.ts b/src/component/Uploader/core/uploader/onedrive.ts index 026fd38..da59101 100644 --- a/src/component/Uploader/core/uploader/onedrive.ts +++ b/src/component/Uploader/core/uploader/onedrive.ts @@ -4,97 +4,91 @@ import { OneDriveChunkError, OneDriveEmptyFileSelected } from "../errors"; import { Status } from "./base"; export default class OneDrive extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - if (chunkInfo.chunk.size === 0) { - throw new OneDriveEmptyFileSelected(); - } - - const rangeEnd = this.progress.total.loaded + chunkInfo.chunk.size - 1; - return this.sendRange( - chunkInfo, - this.progress.total.loaded, - rangeEnd, - 0 - ).catch((e) => { - if ( - e instanceof OneDriveChunkError && - e.response.error.innererror && - e.response.error.innererror.code == "fragmentOverlap" - ) { - return this.alignChunkOffset(chunkInfo); - } - - throw e; - }); + protected async uploadChunk(chunkInfo: ChunkInfo) { + if (chunkInfo.chunk.size === 0) { + throw new OneDriveEmptyFileSelected(); } - private async sendRange( - chunkInfo: ChunkInfo, - start: number, - end: number, - chunkOffset: number - ) { - const range = `bytes ${start}-${end}/${this.task.file.size}`; - return oneDriveUploadChunk( - `${this.task.session?.uploadURLs[0]!}`, - range, - chunkInfo, - (p) => { - this.updateChunkProgress( - chunkOffset + p.loaded, - chunkInfo.index - ); - }, - this.cancelToken.token - ); - } + const rangeEnd = + (this.progress?.total.loaded ?? 0) + chunkInfo.chunk.size - 1; + return this.sendRange( + chunkInfo, + this.progress?.total.loaded ?? 0, + rangeEnd, + 0, + ).catch((e) => { + if ( + e instanceof OneDriveChunkError && + e.response.error.innererror && + e.response.error.innererror.code == "fragmentOverlap" + ) { + return this.alignChunkOffset(chunkInfo); + } - private async alignChunkOffset(chunkInfo: ChunkInfo) { - this.logger.info( - `Chunk [${chunkInfo.index}] overlapped, checking next expected range...` - ); - const rangeStatus = await oneDriveUploadChunk( - `${this.task.session?.uploadURLs[0]!}`, - "", - chunkInfo, - (p) => { - return null; - }, - this.cancelToken.token - ); - const expectedStart = parseInt( - rangeStatus.nextExpectedRanges[0].split("-")[0] - ); - this.logger.info( - `Next expected range start from OneDrive is ${expectedStart}.` - ); + throw e; + }); + } - if (expectedStart >= this.progress.total.loaded) { - this.logger.info(`This whole chunk is overlapped, skipping...`); - this.updateChunkProgress(chunkInfo.chunk.size, chunkInfo.index); - return; - } else { - this.updateChunkProgress(0, chunkInfo.index); - const rangeEnd = - this.progress.total.loaded + chunkInfo.chunk.size - 1; - const newChunkOffset = expectedStart - this.progress.total.loaded; - chunkInfo.chunk = chunkInfo.chunk.slice(newChunkOffset); - this.updateChunkProgress(newChunkOffset, chunkInfo.index); - return this.sendRange( - chunkInfo, - expectedStart, - rangeEnd, - newChunkOffset - ); - } - } + private async sendRange( + chunkInfo: ChunkInfo, + start: number, + end: number, + chunkOffset: number, + ) { + const range = `bytes ${start}-${end}/${this.task.file.size}`; + return oneDriveUploadChunk( + `${this.task.session?.upload_urls[0]!}`, + range, + chunkInfo, + (p) => { + this.updateChunkProgress(chunkOffset + p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); + } - protected async afterUpload(): Promise { - this.logger.info(`Finishing upload...`); - this.transit(Status.finishing); - return finishOneDriveUpload( - this.task.session!.sessionID, - this.cancelToken.token - ); + private async alignChunkOffset(chunkInfo: ChunkInfo) { + this.logger.info( + `Chunk [${chunkInfo.index}] overlapped, checking next expected range...`, + ); + const rangeStatus = await oneDriveUploadChunk( + `${this.task.session?.upload_urls[0]!}`, + "", + chunkInfo, + (_p) => { + return null; + }, + this.cancelToken.token, + ); + const expectedStart = parseInt( + rangeStatus.nextExpectedRanges[0].split("-")[0], + ); + this.logger.info( + `Next expected range start from OneDrive is ${expectedStart}.`, + ); + + const loaded = this.progress?.total.loaded ?? 0; + + if (expectedStart >= loaded) { + this.logger.info(`This whole chunk is overlapped, skipping...`); + this.updateChunkProgress(chunkInfo.chunk.size, chunkInfo.index); + return; + } else { + this.updateChunkProgress(0, chunkInfo.index); + const rangeEnd = loaded + chunkInfo.chunk.size - 1; + const newChunkOffset = expectedStart - loaded; + chunkInfo.chunk = chunkInfo.chunk.slice(newChunkOffset); + this.updateChunkProgress(newChunkOffset, chunkInfo.index); + return this.sendRange(chunkInfo, expectedStart, rangeEnd, newChunkOffset); } + } + + protected async afterUpload(): Promise { + this.logger.info(`Finishing upload...`); + this.transit(Status.finishing); + return finishOneDriveUpload( + this.task.session!.session_id, + this.task.session!.callback_secret, + ); + } } diff --git a/src/component/Uploader/core/uploader/oss.ts b/src/component/Uploader/core/uploader/oss.ts index 305fd36..628fccd 100644 --- a/src/component/Uploader/core/uploader/oss.ts +++ b/src/component/Uploader/core/uploader/oss.ts @@ -3,25 +3,29 @@ import { s3LikeFinishUpload, s3LikeUploadChunk } from "../api"; import { Status } from "./base"; export default class OSS extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - return s3LikeUploadChunk( - this.task.session?.uploadURLs[chunkInfo.index]!, - chunkInfo, - (p) => { - this.updateChunkProgress(p.loaded, chunkInfo.index); - }, - this.cancelToken.token - ); - } + protected async uploadChunk(chunkInfo: ChunkInfo) { + return s3LikeUploadChunk( + this.task.session?.upload_urls[chunkInfo.index]!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); + } - protected async afterUpload(): Promise { - this.logger.info(`Finishing multipart upload...`); - this.transit(Status.finishing); - return s3LikeFinishUpload( - this.task.session!.completeURL, - true, - this.task.chunkProgress, - this.cancelToken.token - ); - } + protected async afterUpload(): Promise { + this.logger.info(`Finishing multipart upload...`); + this.transit(Status.finishing); + return s3LikeFinishUpload( + this.task.session!.completeURL, + true, + this.task.chunkProgress, + this.cancelToken.token, + { + "x-oss-forbid-overwrite": "true", + "x-oss-complete-all": "yes", + }, + ); + } } diff --git a/src/component/Uploader/core/uploader/qiniu.ts b/src/component/Uploader/core/uploader/qiniu.ts index 4b19dc5..61622b7 100644 --- a/src/component/Uploader/core/uploader/qiniu.ts +++ b/src/component/Uploader/core/uploader/qiniu.ts @@ -3,28 +3,29 @@ import { qiniuDriveUploadChunk, qiniuFinishUpload } from "../api"; import { Status } from "./base"; export default class Qiniu extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - const chunkRes = await qiniuDriveUploadChunk( - this.task.session?.uploadURLs[0]!, - this.task.session?.credential!, - chunkInfo, - (p) => { - this.updateChunkProgress(p.loaded, chunkInfo.index); - }, - this.cancelToken.token - ); + protected async uploadChunk(chunkInfo: ChunkInfo) { + const chunkRes = await qiniuDriveUploadChunk( + this.task.session?.upload_urls[0]!, + this.task.session?.credential!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); - this.task.chunkProgress[chunkInfo.index].etag = chunkRes.etag; - } + this.task.chunkProgress[chunkInfo.index].etag = chunkRes.etag; + } - protected async afterUpload(): Promise { - this.logger.info(`Finishing multipart upload...`); - this.transit(Status.finishing); - return qiniuFinishUpload( - this.task.session?.uploadURLs[0]!, - this.task.session?.credential!, - this.task.chunkProgress, - this.cancelToken.token - ); - } + protected async afterUpload(): Promise { + this.logger.info(`Finishing multipart upload...`); + this.transit(Status.finishing); + return qiniuFinishUpload( + this.task.session?.upload_urls[0]!, + this.task.session?.credential!, + this.task.chunkProgress, + this.cancelToken.token, + this.task.session?.mime_type, + ); + } } diff --git a/src/component/Uploader/core/uploader/remote.ts b/src/component/Uploader/core/uploader/remote.ts index 8f13243..16e8486 100644 --- a/src/component/Uploader/core/uploader/remote.ts +++ b/src/component/Uploader/core/uploader/remote.ts @@ -2,15 +2,15 @@ import Chunk, { ChunkInfo } from "./chunk"; import { slaveUploadChunk } from "../api"; export default class Remote extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - return slaveUploadChunk( - `${this.task.session?.uploadURLs[0]!}`, - this.task.session?.credential!, - chunkInfo, - (p) => { - this.updateChunkProgress(p.loaded, chunkInfo.index); - }, - this.cancelToken.token - ); - } + protected async uploadChunk(chunkInfo: ChunkInfo) { + return slaveUploadChunk( + `${this.task.session?.upload_urls[0]!}`, + this.task.session?.credential!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); + } } diff --git a/src/component/Uploader/core/uploader/s3.ts b/src/component/Uploader/core/uploader/s3.ts index 4011628..ab98c28 100644 --- a/src/component/Uploader/core/uploader/s3.ts +++ b/src/component/Uploader/core/uploader/s3.ts @@ -1,39 +1,41 @@ import Chunk, { ChunkInfo } from "./chunk"; import { - s3LikeFinishUpload, - s3LikeUploadCallback, - s3LikeUploadChunk, + s3LikeFinishUpload, + s3LikeUploadCallback, + s3LikeUploadChunk, } from "../api"; import { Status } from "./base"; +import { PolicyType } from "../../../../api/explorer.ts"; export default class OSS extends Chunk { - protected async uploadChunk(chunkInfo: ChunkInfo) { - const etag = await s3LikeUploadChunk( - this.task.session?.uploadURLs[chunkInfo.index]!, - chunkInfo, - (p) => { - this.updateChunkProgress(p.loaded, chunkInfo.index); - }, - this.cancelToken.token - ); + protected async uploadChunk(chunkInfo: ChunkInfo) { + const etag = await s3LikeUploadChunk( + this.task.session?.upload_urls[chunkInfo.index]!, + chunkInfo, + (p) => { + this.updateChunkProgress(p.loaded, chunkInfo.index); + }, + this.cancelToken.token, + ); - this.task.chunkProgress[chunkInfo.index].etag = etag; - } + this.task.chunkProgress[chunkInfo.index].etag = etag; + } - protected async afterUpload(): Promise { - this.logger.info(`Finishing multipart upload...`); - this.transit(Status.finishing); - await s3LikeFinishUpload( - this.task.session!.completeURL, - false, - this.task.chunkProgress, - this.cancelToken.token - ); + protected async afterUpload(): Promise { + this.logger.info(`Finishing multipart upload...`); + this.transit(Status.finishing); + await s3LikeFinishUpload( + this.task.session!.completeURL, + false, + this.task.chunkProgress, + this.cancelToken.token, + ); - this.logger.info(`Sending S3-like upload callback...`); - return s3LikeUploadCallback( - this.task.session!.sessionID, - this.cancelToken.token - ); - } + this.logger.info(`Sending S3-like upload callback...`); + return s3LikeUploadCallback( + this.task.session!.session_id, + this.task.session!.callback_secret, + PolicyType.s3, + ); + } } diff --git a/src/component/Uploader/core/uploader/upyun.ts b/src/component/Uploader/core/uploader/upyun.ts index c72ee45..09192d0 100644 --- a/src/component/Uploader/core/uploader/upyun.ts +++ b/src/component/Uploader/core/uploader/upyun.ts @@ -2,19 +2,20 @@ import Base from "./base"; import { upyunFormUploadChunk } from "../api"; export default class Upyun extends Base { - public upload = async () => { - this.logger.info("Starting uploading file stream:", this.task.file); - await upyunFormUploadChunk( - this.task.session?.uploadURLs[0]!, - this.task.file, - this.task.session?.policy!, - this.task.session?.credential!, - (p) => { - this.subscriber.onProgress({ - total: this.getProgressInfoItem(p.loaded, p.total), - }); - }, - this.cancelToken.token - ); - }; + public upload = async () => { + this.logger.info("Starting uploading file stream:", this.task.file); + await upyunFormUploadChunk( + this.task.session?.upload_urls[0]!, + this.task.file, + this.task.session?.upload_policy!, + this.task.session?.credential!, + (p) => { + this.subscriber.onProgress({ + total: this.getProgressInfoItem(p.loaded, p.total ?? 1), + }); + }, + this.cancelToken.token, + this.task.session?.mime_type, + ); + }; } diff --git a/src/component/Uploader/core/utils/helper.ts b/src/component/Uploader/core/utils/helper.ts index a339043..616cd87 100644 --- a/src/component/Uploader/core/utils/helper.ts +++ b/src/component/Uploader/core/utils/helper.ts @@ -3,318 +3,310 @@ import Logger from "../logger"; import { UploaderError, UploaderErrorName } from "../errors"; import { ChunkProgress } from "../uploader/chunk"; -export const sizeToString = (bytes: number): string => { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i]; -}; - // 文件分块 export function getChunks( - file: File, - chunkByteSize: number | undefined + file: File, + chunkByteSize: number | undefined, ): Blob[] { - // 如果 chunkByteSize 比文件大或为0,则直接取文件的大小 - if (!chunkByteSize || chunkByteSize > file.size || chunkByteSize == 0) { - chunkByteSize = file.size; - } + // 如果 chunkByteSize 比文件大或为0,则直接取文件的大小 + if (!chunkByteSize || chunkByteSize > file.size || chunkByteSize == 0) { + chunkByteSize = file.size; + } - const chunks: Blob[] = []; - const count = Math.ceil(file.size / chunkByteSize); - for (let i = 0; i < count; i++) { - const chunk = file.slice( - chunkByteSize * i, - i === count - 1 ? file.size : chunkByteSize * (i + 1) - ); - chunks.push(chunk); - } + const chunks: Blob[] = []; + const count = Math.ceil(file.size / chunkByteSize); + for (let i = 0; i < count; i++) { + const chunk = file.slice( + chunkByteSize * i, + i === count - 1 ? file.size : chunkByteSize * (i + 1), + ); + chunks.push(chunk); + } - if (chunks.length == 0) { - chunks.push(file.slice(0)); - } - return chunks; + if (chunks.length == 0) { + chunks.push(file.slice(0)); + } + return chunks; } export function sumChunk(list: ChunkProgress[]) { - return list.reduce((data, loaded) => data + loaded.loaded, 0); + return list.reduce((data, loaded) => data + loaded.loaded, 0); } const resumeKeyPrefix = "cd_upload_ctx_"; function isTask(toBeDetermined: Task | string): toBeDetermined is Task { - return !!(toBeDetermined as Task).name; + return !!(toBeDetermined as Task).name; } export function getResumeCtxKey(task: Task | string): string { - if (isTask(task)) { - return `${resumeKeyPrefix}name_${task.name}_dst_${task.dst}_size_${task.size}_policy_${task.policy.id}`; - } + if (isTask(task)) { + return `${resumeKeyPrefix}name_${task.name}_dst_${task.dst}_size_${task.size}_policy_${task.policy.id}`; + } - return task; + return task; } export function setResumeCtx(task: Task, logger: Logger) { - const ctxKey = getResumeCtxKey(task); - try { - localStorage.setItem(ctxKey, JSON.stringify(task)); - } catch (err) { - logger.warn( - new UploaderError( - UploaderErrorName.WriteCtxFailed, - `setResumeCtx failed: ${ctxKey}` - ) - ); - } + const ctxKey = getResumeCtxKey(task); + try { + localStorage.setItem(ctxKey, JSON.stringify(task)); + } catch (err) { + logger.warn( + new UploaderError( + UploaderErrorName.WriteCtxFailed, + `setResumeCtx failed: ${ctxKey}`, + ), + ); + } } export function removeResumeCtx(task: Task | string, logger: Logger) { - const ctxKey = getResumeCtxKey(task); - try { - localStorage.removeItem(ctxKey); - } catch (err) { - logger.warn( - new UploaderError( - UploaderErrorName.RemoveCtxFailed, - `removeResumeCtx failed. key: ${ctxKey}` - ) - ); - } + const ctxKey = getResumeCtxKey(task); + try { + localStorage.removeItem(ctxKey); + } catch (err) { + logger.warn( + new UploaderError( + UploaderErrorName.RemoveCtxFailed, + `removeResumeCtx failed. key: ${ctxKey}`, + ), + ); + } } export function cleanupResumeCtx(logger: Logger) { - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith(resumeKeyPrefix)) { - try { - localStorage.removeItem(key); - } catch (err) { - logger.warn( - new UploaderError( - UploaderErrorName.RemoveCtxFailed, - `removeResumeCtx failed. key: ${key}` - ) - ); - } - } + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(resumeKeyPrefix)) { + try { + localStorage.removeItem(key); + } catch (err) { + logger.warn( + new UploaderError( + UploaderErrorName.RemoveCtxFailed, + `removeResumeCtx failed. key: ${key}`, + ), + ); + } } + } } export function getResumeCtx(task: Task | string, logger: Logger): Task | null { - const ctxKey = getResumeCtxKey(task); - let localInfoString: string | null = null; - try { - localInfoString = localStorage.getItem(ctxKey); - } catch { - logger.warn( - new UploaderError( - UploaderErrorName.ReadCtxFailed, - `getResumeCtx failed. key: ${ctxKey}` - ) - ); - } + const ctxKey = getResumeCtxKey(task); + let localInfoString: string | null = null; + try { + localInfoString = localStorage.getItem(ctxKey); + } catch { + logger.warn( + new UploaderError( + UploaderErrorName.ReadCtxFailed, + `getResumeCtx failed. key: ${ctxKey}`, + ), + ); + } - if (localInfoString == null) { - return null; - } + if (localInfoString == null) { + return null; + } - let localInfo: Task | null = null; - try { - localInfo = JSON.parse(localInfoString); - } catch { - // 本地信息已被破坏,直接删除 - removeResumeCtx(task, logger); - logger.warn( - new UploaderError( - UploaderErrorName.InvalidCtxData, - `getResumeCtx failed to parse. key: ${ctxKey}` - ) - ); - } + let localInfo: Task | null = null; + try { + localInfo = JSON.parse(localInfoString); + } catch { + // 本地信息已被破坏,直接删除 + removeResumeCtx(task, logger); + logger.warn( + new UploaderError( + UploaderErrorName.InvalidCtxData, + `getResumeCtx failed to parse. key: ${ctxKey}`, + ), + ); + } - if ( - localInfo && - localInfo.session && - localInfo.session.expires < Math.floor(Date.now() / 1000) - ) { - removeResumeCtx(task, logger); - logger.warn( - new UploaderError( - UploaderErrorName.CtxExpired, - `upload session already expired at ${localInfo.session.expires}. key: ${ctxKey}` - ) - ); - return null; - } + if ( + localInfo && + localInfo.session && + localInfo.session.expires < Math.floor(Date.now() / 1000) + ) { + removeResumeCtx(task, logger); + logger.warn( + new UploaderError( + UploaderErrorName.CtxExpired, + `upload session already expired at ${localInfo.session.expires}. key: ${ctxKey}`, + ), + ); + return null; + } - return localInfo; + return localInfo; } export function listResumeCtx(logger: Logger): Task[] { - const res: Task[] = []; - for (let i = 0, len = localStorage.length; i < len; i++) { - const key = localStorage.key(i); - if (key && key.startsWith(resumeKeyPrefix)) { - const value = getResumeCtx(key, logger); - if (value) { - res.push(value); - } - } + const res: Task[] = []; + for (let i = 0, len = localStorage.length; i < len; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(resumeKeyPrefix)) { + const value = getResumeCtx(key, logger); + if (value) { + res.push(value); + } } + } - return res; + return res; } export function OBJtoXML(obj: any): string { - let xml = ""; - for (const prop in obj) { - xml += "<" + prop + ">"; - if (Array.isArray(obj[prop])) { - for (const array of obj[prop]) { - // A real botch fix here - xml += ""; - xml += "<" + prop + ">"; - - xml += OBJtoXML(new Object(array)); - } - } else if (typeof obj[prop] == "object") { - xml += OBJtoXML(new Object(obj[prop])); - } else { - xml += obj[prop]; - } + let xml = ""; + for (const prop in obj) { + xml += "<" + prop + ">"; + if (Array.isArray(obj[prop])) { + for (const array of obj[prop]) { + // A real botch fix here xml += ""; + xml += "<" + prop + ">"; + + xml += OBJtoXML(new Object(array)); + } + } else if (typeof obj[prop] == "object") { + xml += OBJtoXML(new Object(obj[prop])); + } else { + xml += obj[prop]; } - return xml.replace(/<\/?[0-9]{1,}>/g, ""); + xml += ""; + } + return xml.replace(/<\/?[0-9]{1,}>/g, ""); } export function getFileInput(id: number, isFolder: boolean): HTMLInputElement { - const input = document.createElement("input"); - input.type = "file"; + const input = document.createElement("input"); + input.type = "file"; + input.id = `upload-file-input-${id}`; + if (isFolder) { + input.id = `upload-folder-input-${id}`; + input.setAttribute("webkitdirectory", "true"); + input.setAttribute("mozdirectory", "true"); + } else { input.id = `upload-file-input-${id}`; - if (isFolder) { - input.id = `upload-folder-input-${id}`; - input.setAttribute("webkitdirectory", "true"); - input.setAttribute("mozdirectory", "true"); - } else { - input.id = `upload-file-input-${id}`; - input.multiple = true; - } - input.hidden = true; - document.body.appendChild(input); - return input; + input.multiple = true; + } + input.hidden = true; + document.body.appendChild(input); + return input; } export function pathJoin(parts: string[], sep = "/"): string { - parts = parts.map((part, index) => { - if (index) { - part = part.replace(new RegExp("^" + sep), ""); - } - if (index !== parts.length - 1) { - part = part.replace(new RegExp(sep + "$"), ""); - } - return part; - }); - return parts.join(sep); + parts = parts.map((part, index) => { + if (index) { + part = part.replace(new RegExp("^" + sep), ""); + } + if (index !== parts.length - 1) { + part = part.replace(new RegExp(sep + "$"), ""); + } + return part; + }); + return parts.join(sep); } function basename(path: string): string { - const pathList = path.split("/"); - pathList.pop(); - return pathList.join("/") === "" ? "/" : pathList.join("/"); + const pathList = path.split("/"); + pathList.pop(); + return pathList.join("/") === "" ? "/" : pathList.join("/"); } export function trimPrefix(src: string, prefix: string): string { - if (src.startsWith(prefix)) { - return src.slice(prefix.length); - } - return src; + if (src.startsWith(prefix)) { + return src.slice(prefix.length); + } + return src; } export function getDirectoryUploadDst(dst: string, file: any): string { - let relPath = file.webkitRelativePath; + let relPath = file.webkitRelativePath; + if (!relPath || relPath == "") { + relPath = file.fsPath; if (!relPath || relPath == "") { - relPath = file.fsPath; - if (!relPath || relPath == "") { - return dst; - } + return dst; } + } - relPath = trimPrefix(relPath, "/"); + relPath = trimPrefix(relPath, "/"); - return basename(pathJoin([dst, relPath])); + return basename(pathJoin([dst, relPath])); } // Wrap readEntries in a promise to make working with readEntries easier async function readEntriesPromise(directoryReader: any): Promise { - try { - return await new Promise((resolve, reject) => { - directoryReader.readEntries(resolve, reject); - }); - } catch (err) { - console.log(err); - } + try { + return await new Promise((resolve, reject) => { + directoryReader.readEntries(resolve, reject); + }); + } catch (err) { + console.log(err); + } } async function readFilePromise(fileReader: any, path: string): Promise { - try { - return await new Promise((resolve, reject) => { - fileReader.file((file: any) => { - file.fsPath = path; - resolve(file); - }); - }); - } catch (err) { - console.log(err); - } + try { + return await new Promise((resolve, _reject) => { + fileReader.file((file: any) => { + file.fsPath = path; + resolve(file); + }); + }); + } catch (err) { + console.log(err); + } } // Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array async function readAllDirectoryEntries(directoryReader: any): Promise { - const entries: any[] = []; - let readEntries = await readEntriesPromise(directoryReader); - while (readEntries.length > 0) { - entries.push(...readEntries); - readEntries = await readEntriesPromise(directoryReader); - } - return entries; + const entries: any[] = []; + let readEntries = await readEntriesPromise(directoryReader); + while (readEntries.length > 0) { + entries.push(...readEntries); + readEntries = await readEntriesPromise(directoryReader); + } + return entries; } // Drop handler function to get all files export async function getAllFileEntries( - dataTransferItemList: DataTransferItemList + dataTransferItemList: DataTransferItemList, ): Promise { - const fileEntries: any[] = []; - // Use BFS to traverse entire directory/file structure - const queue: any[] = []; - // Unfortunately dataTransferItemList is not iterable i.e. no forEach - for (let i = 0; i < dataTransferItemList.length; i++) { - const fileEntry = dataTransferItemList[i].webkitGetAsEntry(); - if (!fileEntry) { - const file = dataTransferItemList[i].getAsFile(); - if (file) { - fileEntries.push(file); - } - } + const fileEntries: any[] = []; + // Use BFS to traverse entire directory/file structure + const queue: any[] = []; + // Unfortunately dataTransferItemList is not iterable i.e. no forEach + for (let i = 0; i < dataTransferItemList.length; i++) { + const fileEntry = dataTransferItemList[i].webkitGetAsEntry(); + if (!fileEntry) { + const file = dataTransferItemList[i].getAsFile(); + if (file) { + fileEntries.push(file); + } + } - queue.push(dataTransferItemList[i].webkitGetAsEntry()); + queue.push(dataTransferItemList[i].webkitGetAsEntry()); + } + while (queue.length > 0) { + const entry = queue.shift(); + if (!entry) { + continue; } - while (queue.length > 0) { - const entry = queue.shift(); - if (!entry) { - continue; - } - if (entry.isFile) { - fileEntries.push(await readFilePromise(entry, entry.fullPath)); - } else if (entry.isDirectory) { - const reader = entry.createReader(); - const entries: any = await readAllDirectoryEntries(reader); - queue.push(...entries); - } + if (entry.isFile) { + fileEntries.push(await readFilePromise(entry, entry.fullPath)); + } else if (entry.isDirectory) { + const reader = entry.createReader(); + const entries: any = await readAllDirectoryEntries(reader); + queue.push(...entries); } - return fileEntries; + } + return fileEntries; } export function isFileDrop(e: DragEvent): boolean { - return !!e.dataTransfer && e.dataTransfer.types.includes("Files"); + return !!e.dataTransfer && e.dataTransfer.types.includes("Files"); } diff --git a/src/component/Uploader/core/utils/pool.ts b/src/component/Uploader/core/utils/pool.ts index 3d000e2..296f21b 100644 --- a/src/component/Uploader/core/utils/pool.ts +++ b/src/component/Uploader/core/utils/pool.ts @@ -1,5 +1,5 @@ -import Base from "../uploader/base"; import { ProcessingTaskDuplicatedError } from "../errors"; +import Base from "../uploader/base"; export interface QueueContent { uploader: Base; @@ -8,10 +8,13 @@ export interface QueueContent { } export class Pool { - queue: Array = []; - processing: Array = []; + queue: Array = []; + processing: Array = []; + onPoolEmpty?: () => void; - constructor(public limit: number) {} + constructor(public limit: number, onPoolEmpty?: () => void) { + this.onPoolEmpty = onPoolEmpty; + } enqueue(uploader: Base) { return new Promise((resolve, reject) => { @@ -26,6 +29,9 @@ export class Pool { release(item: QueueContent) { this.processing = this.processing.filter((v) => v !== item); + if (this.queue.length === 0 && this.processing.length === 0) { + this.onPoolEmpty?.(); + } this.check(); } diff --git a/src/component/Uploader/core/utils/request.ts b/src/component/Uploader/core/utils/request.ts index 90dae7d..075109a 100644 --- a/src/component/Uploader/core/utils/request.ts +++ b/src/component/Uploader/core/utils/request.ts @@ -1,48 +1,41 @@ import axios, { AxiosRequestConfig } from "axios"; import { Response } from "../types"; -import { - HTTPError, - RequestCanceledError, - TransformResponseError, -} from "../errors"; +import { HTTPError, TransformResponseError } from "../errors"; export const { CancelToken } = axios; -export { CancelTokenSource } from "axios"; const baseConfig = { - transformResponse: [ - (response: any) => { - try { - return JSON.parse(response); - } catch (e) { - throw new TransformResponseError(response, e); - } - }, - ], + transformResponse: [ + (response: any) => { + try { + return JSON.parse(response); + } catch (e) { + throw new TransformResponseError(response, e); + } + }, + ], }; const cdBackendConfig = { - ...baseConfig, - baseURL: "/api/v3", - withCredentials: true, + ...baseConfig, + baseURL: "/api/v4", + withCredentials: true, }; export function request(url: string, config?: AxiosRequestConfig) { - return axios - .request({ ...baseConfig, ...config, url }) - .catch((err) => { - if (axios.isCancel(err)) { - throw new RequestCanceledError(); - } + return axios.request({ ...baseConfig, ...config, url }).catch((err) => { + if (axios.isCancel(err)) { + throw err; + } - if (err instanceof TransformResponseError) { - throw err; - } + if (err instanceof TransformResponseError) { + throw err; + } - throw new HTTPError(err, url); - }); + throw new HTTPError(err, url); + }); } export function requestAPI(url: string, config?: AxiosRequestConfig) { - return request>(url, { ...cdBackendConfig, ...config }); + return request>(url, { ...cdBackendConfig, ...config }); } diff --git a/src/component/Uploader/core/utils/validator.ts b/src/component/Uploader/core/utils/validator.ts index 6fb32e7..08d01f4 100644 --- a/src/component/Uploader/core/utils/validator.ts +++ b/src/component/Uploader/core/utils/validator.ts @@ -1,43 +1,40 @@ -import { Policy } from "../types"; +import { StoragePolicy } from "../../../../api/explorer.ts"; import { FileValidateError } from "../errors"; interface Validator { - (file: File, policy: Policy): void; + (file: File, policy: StoragePolicy): void; } // validators const checkers: Array = [ - function checkExt(file: File, policy: Policy) { - if ( - policy.allowedSuffix != undefined && - policy.allowedSuffix.length > 0 - ) { - const ext = file?.name.split(".").pop(); - if (ext === null || !ext || !policy.allowedSuffix.includes(ext)) - throw new FileValidateError( - "File suffix not allowed in policy.", - "suffix", - policy - ); - } - }, + function checkExt(file: File, policy: StoragePolicy) { + if (policy.allowed_suffix != undefined && policy.allowed_suffix.length > 0) { + const ext = file?.name.split(".").pop(); + if (ext === null || !ext || !policy.allowed_suffix.includes(ext)) + throw new FileValidateError( + "File suffix not allowed in policy.", + "suffix", + policy, + ); + } + }, - function checkSize(file: File, policy: Policy) { - if (policy.maxSize > 0) { - if (file.size > policy.maxSize) { - throw new FileValidateError( - "File size exceeds maximum limit.", - "size", - policy - ); - } - } - }, + function checkSize(file: File, policy: StoragePolicy) { + if (policy.max_size > 0) { + if (file.size > policy.max_size) { + throw new FileValidateError( + "File size exceeds maximum limit.", + "size", + policy, + ); + } + } + }, ]; /* 将每个 Validator 执行 失败返回 Error */ -export function validate(file: File, policy: Policy) { - checkers.forEach((c) => c(file, policy)); +export function validate(file: File, policy: StoragePolicy) { + checkers.forEach((c) => c(file, policy)); } diff --git a/src/component/Viewer/Code.js b/src/component/Viewer/Code.js deleted file mode 100644 index 238b147..0000000 --- a/src/component/Viewer/Code.js +++ /dev/null @@ -1,197 +0,0 @@ -import React, { Suspense, useCallback, useEffect, useState } from "react"; -import { Paper, useTheme } from "@material-ui/core"; -import { makeStyles } from "@material-ui/core/styles"; -import { useLocation, useParams, useRouteMatch } from "react-router"; -import API from "../../middleware/Api"; -import { useDispatch } from "react-redux"; -import pathHelper from "../../utils/page"; -import SaveButton from "../Dial/Save"; -import { codePreviewSuffix } from "../../config"; -import TextLoading from "../Placeholder/TextLoading"; -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import Select from "@material-ui/core/Select"; -import Switch from "@material-ui/core/Switch"; -import MenuItem from "@material-ui/core/MenuItem"; -import Divider from "@material-ui/core/Divider"; -import { toggleSnackbar } from "../../redux/explorer"; -import UseFileSubTitle from "../../hooks/fileSubtitle"; -import { useTranslation } from "react-i18next"; - -const MonacoEditor = React.lazy(() => - import(/* webpackChunkName: "codeEditor" */ "react-monaco-editor") -); - -const useStyles = makeStyles((theme) => ({ - layout: { - width: "auto", - marginTop: "30px", - marginLeft: theme.spacing(3), - marginRight: theme.spacing(3), - [theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: { - width: 1100, - marginLeft: "auto", - marginRight: "auto", - }, - marginBottom: 40, - }, - editor: { - borderRadius: theme.shape.borderRadius, - }, - "@global": { - ".overflow-guard": { - borderRadius: "0 0 12px 12px!important", - }, - }, - formControl: { - margin: "8px 16px 8px 16px", - }, - toobar: { - textAlign: "right", - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -export default function CodeViewer() { - const { t } = useTranslation(); - const [content, setContent] = useState(""); - const [status, setStatus] = useState(""); - const [loading, setLoading] = useState(true); - const [suffix, setSuffix] = useState("javascript"); - const [wordWrap, setWordWrap] = useState("off"); - - const math = useRouteMatch(); - const location = useLocation(); - const query = useQuery(); - const { id } = useParams(); - const theme = useTheme(); - const { title } = UseFileSubTitle(query, math, location); - - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - - useEffect(() => { - const extension = title.split("."); - setSuffix(codePreviewSuffix[extension.pop()]); - // eslint-disable-next-line - }, [title]); - - useEffect(() => { - let requestURL = "/file/content/" + query.get("id"); - if (pathHelper.isSharePage(location.pathname)) { - requestURL = "/share/content/" + id; - if (query.get("share_path") !== "") { - requestURL += - "?path=" + encodeURIComponent(query.get("share_path")); - } - } - - setLoading(true); - API.get(requestURL, { responseType: "arraybuffer" }) - .then((response) => { - const buffer = new Buffer(response.rawData, "binary"); - const textdata = buffer.toString(); // for string - setContent(textdata); - }) - .catch((error) => { - ToggleSnackbar( - "top", - "right", - t("fileManager.errorReadFileContent", { - msg: error.message, - }), - "error" - ); - }) - .then(() => { - setLoading(false); - }); - // eslint-disable-next-line - }, [math.params[0]]); - - const save = () => { - setStatus("loading"); - API.put("/file/update/" + query.get("id"), content) - .then(() => { - setStatus("success"); - setTimeout(() => setStatus(""), 2000); - }) - .catch((error) => { - setStatus(""); - ToggleSnackbar("top", "right", error.message, "error"); - }); - }; - - const classes = useStyles(); - const isSharePage = pathHelper.isSharePage(location.pathname); - return ( -
- -
- - - setWordWrap( - e.target.checked ? "on" : "off" - ) - } - /> - } - label={t("fileManager.wordWrap")} - /> - - - - -
- - {loading && } - {!loading && ( - }> - setContent(value)} - /> - - )} -
- {!isSharePage && } -
- ); -} diff --git a/src/component/Viewer/Doc.js b/src/component/Viewer/Doc.js deleted file mode 100644 index 1b9fc19..0000000 --- a/src/component/Viewer/Doc.js +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { makeStyles, useTheme } from "@material-ui/core/styles"; -import { useLocation, useParams, useRouteMatch } from "react-router"; -import API from "../../middleware/Api"; -import { useDispatch, useSelector } from "react-redux"; -import pathHelper from "../../utils/page"; -import { - closeAllModals, - openShareDialog, - setModalsLoading, - setSelectedTarget, - toggleSnackbar, -} from "../../redux/explorer"; -import UseFileSubTitle from "../../hooks/fileSubtitle"; -import i18n from "i18next"; -import CreatShare from "../Modals/CreateShare"; - -const useStyles = makeStyles(() => ({ - layout: { - width: "auto", - }, - "@global": { - iframe: { - border: "none", - width: "100%", - height: "calc(100vh - 64px)", - marginBottom: -10, - }, - }, -})); - -function useQuery() { - return new URLSearchParams(useLocation().search); -} - -export default function DocViewer() { - const [session, setSession] = useState(null); - const [file, setFile] = useState(null); - const math = useRouteMatch(); - const location = useLocation(); - const query = useQuery(); - const { id } = useParams(); - const theme = useTheme(); - const { title } = UseFileSubTitle(query, math, location); - - const shareOpened = useSelector((state) => state.viewUpdate.modals.share); - const modalLoading = useSelector((state) => state.viewUpdate.modalsLoading); - const dispatch = useDispatch(); - const ToggleSnackbar = useCallback( - (vertical, horizontal, msg, color) => - dispatch(toggleSnackbar(vertical, horizontal, msg, color)), - [dispatch] - ); - const CloseAllModals = useCallback( - () => dispatch(closeAllModals()), - [dispatch] - ); - const OpenShareDialog = useCallback( - () => dispatch(openShareDialog()), - [dispatch] - ); - const SetModalsLoading = useCallback( - (status) => dispatch(setModalsLoading(status)), - [dispatch] - ); - - useEffect(() => { - let requestURL = "/file/doc/" + query.get("id"); - if (pathHelper.isSharePage(location.pathname)) { - requestURL = "/share/doc/" + id; - if (query.get("share_path") !== "") { - requestURL += - "?path=" + encodeURIComponent(query.get("share_path")); - } - } - API.get(requestURL) - .then((response) => { - if (response.data.access_token) { - response.data.url = response.data.url.replaceAll( - "lng", - i18n.resolvedLanguage.toLowerCase() - ); - response.data.url = response.data.url.replaceAll( - "darkmode", - theme.palette.type === "dark" ? "2" : "1" - ); - } - - setSession(response.data); - }) - .catch((error) => { - ToggleSnackbar("top", "right", error.message, "error"); - }); - // eslint-disable-next-line - }, [math.params[0], location]); - - const classes = useStyles(); - - const handlePostMessage = (e) => { - console.log("Received PostMessage from " + e.origin, e.data); - let msg; - try { - msg = JSON.parse(e.data); - } catch (e) { - return; - } - - if (msg.MessageId === "UI_Sharing") { - setFile([ - { - name: title, - id: query.get("id"), - type: "file", - }, - ]); - OpenShareDialog(); - } - }; - - useEffect(() => { - const frameholder = document.getElementById("frameholder"); - const office_frame = document.createElement("iframe"); - if (session && session.access_token && frameholder) { - office_frame.name = "office_frame"; - office_frame.id = "office_frame"; - - // The title should be set for accessibility - office_frame.title = "Office Frame"; - - // This attribute allows true fullscreen mode in slideshow view - // when using PowerPoint's 'view' action. - office_frame.setAttribute("allowfullscreen", "true"); - - // The sandbox attribute is needed to allow automatic redirection to the O365 sign-in page in the business user flow - office_frame.setAttribute( - "sandbox", - "allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation allow-popups-to-escape-sandbox" - ); - frameholder.appendChild(office_frame); - document.getElementById("office_form").submit(); - window.addEventListener("message", handlePostMessage, false); - - return () => { - window.removeEventListener("message", handlePostMessage, false); - }; - } - }, [session]); - - return ( -
- CloseAllModals()} - modalsLoading={modalLoading} - setModalsLoading={SetModalsLoading} - selected={file} - /> - {session && !session.access_token && ( -