Frontend: Reformat JS code

This commit is contained in:
Michael Mayer
2020-12-18 14:42:36 +01:00
parent 29145e77b6
commit 003412736e
37 changed files with 3715 additions and 3420 deletions

View File

@@ -1,23 +1,67 @@
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
commonjs: true, commonjs: true,
es6: true, es6: true,
node: true, node: true,
mocha: true, mocha: true,
}, },
extends: 'eslint:recommended', extends: [
parserOptions: { "eslint:recommended",
sourceType: 'module', "plugin:vue/recommended",
}, "plugin:prettier-vue/recommended",
rules: { // Do not add `'prettier/vue'` if you don't want to use prettier for `<template>` blocks
'comma-dangle': ['error', 'always-multiline'], "prettier/vue",
indent: ['error', 4, { "SwitchCase": 1 }], ],
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'double'], // Easier for Go developers! settings: {
semi: ['error', 'always'], "prettier-vue": {
'no-unused-vars': ['warn'], // Settings for how to process Vue SFC Blocks
'no-console': 0, SFCBlocks: {
'no-prototype-builtins': 0, template: false,
script: true,
style: true,
},
// Use prettierrc for prettier options or not (default: `true`)
usePrettierrc: true,
// Set the options for `prettier.getFileInfo`.
// @see https://prettier.io/docs/en/api.html#prettiergetfileinfofilepath-options
fileInfoOptions: {
// Path to ignore file (default: `'.prettierignore'`)
// Notice that the ignore file is only used for this plugin
ignorePath: ".testignore",
// Process the files in `node_modules` or not (default: `false`)
withNodeModules: false,
},
}, },
},
parserOptions: {
sourceType: "module",
},
rules: {
// 'comma-dangle': ['error', 'always-multiline'],
indent: ["error", 2, { SwitchCase: 1 }],
"linebreak-style": ["error", "unix"],
quotes: ["off", "double"], // Easier for Go developers!
semi: ["error", "always"],
"no-unused-vars": ["warn"],
"no-console": 0,
"no-prototype-builtins": 0,
"prettier-vue/prettier": [
"error",
{
// Override all options of `prettier` here
// @see https://prettier.io/docs/en/options.html
printWidth: 100,
singleQuote: false,
semi: true,
trailingComma: "es5",
htmlWhitespaceSensitivity: "strict",
},
],
},
}; };

View File

@@ -34,126 +34,130 @@ const findChrome = require("chrome-finder");
process.env.CHROME_BIN = findChrome(); process.env.CHROME_BIN = findChrome();
module.exports = (config) => { module.exports = (config) => {
config.set({ config.set({
logLevel: config.LOG_ERROR, logLevel: config.LOG_ERROR,
webpackMiddleware: { webpackMiddleware: {
stats: "errors-only", stats: "errors-only",
}, },
frameworks: ["mocha"], frameworks: ["mocha"],
browsers: ["LocalChrome"], browsers: ["LocalChrome"],
customLaunchers: { customLaunchers: {
LocalChrome: { LocalChrome: {
base: "ChromeHeadless", base: "ChromeHeadless",
flags: ["--disable-translate", "--disable-extensions", "--no-sandbox", "--disable-web-security", "--disable-dev-shm-usage"], flags: [
}, "--disable-translate",
}, "--disable-extensions",
"--no-sandbox",
files: [ "--disable-web-security",
"node_modules/@babel/polyfill/dist/polyfill.js", "--disable-dev-shm-usage",
"node_modules/regenerator-runtime/runtime/runtime.js",
{pattern: "tests/unit/**/*_test.js", watched: false},
], ],
},
},
// Preprocess through webpack files: [
preprocessors: { "node_modules/@babel/polyfill/dist/polyfill.js",
"tests/unit/**/*_test.js": ["webpack"], "node_modules/regenerator-runtime/runtime/runtime.js",
{ pattern: "tests/unit/**/*_test.js", watched: false },
],
// Preprocess through webpack
preprocessors: {
"tests/unit/**/*_test.js": ["webpack"],
},
reporters: ["progress", "html", "coverage-istanbul"],
htmlReporter: {
outputFile: "tests/unit.html",
},
coverageIstanbulReporter: {
// reports can be any that are listed here: https://github.com/istanbuljs/istanbuljs/tree/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib
reports: ["html", "lcovonly", "text-summary"],
// base output directory. If you include %browser% in the path it will be replaced with the karma browser name
dir: path.join(__dirname, "coverage"),
// Combines coverage information from multiple browsers into one report rather than outputting a report
// for each browser.
combineBrowserReports: true,
// if using webpack and pre-loaders, work around webpack breaking the source path
fixWebpackSourcePaths: true,
// Omit files with no statements, no functions and no branches from the report
skipFilesWithNoCoverage: true,
// Most reporters accept additional config options. You can pass these through the `report-config` option
"report-config": {
// all options available at: https://github.com/istanbuljs/istanbuljs/blob/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib/html/index.js#L135-L137
html: {
// outputs the report in ./coverage/html
subdir: "html",
}, },
},
reporters: ["progress", "html", "coverage-istanbul"], // enforce percentage thresholds
// anything under these percentages will cause karma to fail with an exit code of 1 if not running in watch mode
htmlReporter: { thresholds: {
outputFile: "tests/unit.html", emitWarning: true, // set to `true` to not fail the test command when thresholds are not met
// thresholds for all files
global: {
//statements: 90,
lines: 90,
//branches: 90,
//functions: 90,
}, },
// thresholds per file
coverageIstanbulReporter: { each: {
// reports can be any that are listed here: https://github.com/istanbuljs/istanbuljs/tree/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib //statements: 90,
reports: ["html", "lcovonly", "text-summary"], lines: 90,
//branches: 90,
// base output directory. If you include %browser% in the path it will be replaced with the karma browser name //functions: 90,
dir: path.join(__dirname, "coverage"), overrides: {
"src/common/viewer.js": {
// Combines coverage information from multiple browsers into one report rather than outputting a report lines: 0,
// for each browser. functions: 0,
combineBrowserReports: true,
// if using webpack and pre-loaders, work around webpack breaking the source path
fixWebpackSourcePaths: true,
// Omit files with no statements, no functions and no branches from the report
skipFilesWithNoCoverage: true,
// Most reporters accept additional config options. You can pass these through the `report-config` option
"report-config": {
// all options available at: https://github.com/istanbuljs/istanbuljs/blob/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib/html/index.js#L135-L137
html: {
// outputs the report in ./coverage/html
subdir: "html",
},
}, },
},
// enforce percentage thresholds
// anything under these percentages will cause karma to fail with an exit code of 1 if not running in watch mode
thresholds: {
emitWarning: true, // set to `true` to not fail the test command when thresholds are not met
// thresholds for all files
global: {
//statements: 90,
lines: 90,
//branches: 90,
//functions: 90,
},
// thresholds per file
each: {
//statements: 90,
lines: 90,
//branches: 90,
//functions: 90,
overrides: {
"src/common/viewer.js": {
lines: 0,
functions: 0,
},
},
},
},
verbose: false, // output config used by istanbul for debugging
}, },
},
webpack: { verbose: false, // output config used by istanbul for debugging
mode: "development", },
resolve: { webpack: {
modules: [ mode: "development",
path.join(__dirname, "src"),
path.join(__dirname, "node_modules"), resolve: {
path.join(__dirname, "tests/unit"), modules: [
], path.join(__dirname, "src"),
alias: { path.join(__dirname, "node_modules"),
vue: "vue/dist/vue.min.js", path.join(__dirname, "tests/unit"),
}, ],
}, alias: {
module: { vue: "vue/dist/vue.min.js",
rules: [
{
test: /\.js$/,
loader: "babel-loader",
exclude: file => (
/node_modules/.test(file)
),
query: {
presets: ["@babel/preset-env"],
compact: false,
},
},
],
},
}, },
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
exclude: (file) => /node_modules/.test(file),
query: {
presets: ["@babel/preset-env"],
compact: false,
},
},
],
},
},
singleRun: true, singleRun: true,
}); });
}; };

View File

@@ -1926,6 +1926,62 @@
"@vue/shared": "3.0.2" "@vue/shared": "3.0.2"
} }
}, },
"@vue/component-compiler-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz",
"integrity": "sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw==",
"requires": {
"consolidate": "^0.15.1",
"hash-sum": "^1.0.2",
"lru-cache": "^4.1.2",
"merge-source-map": "^1.1.0",
"postcss": "^7.0.14",
"postcss-selector-parser": "^6.0.2",
"prettier": "^1.18.2",
"source-map": "~0.6.1",
"vue-template-es2015-compiler": "^1.9.0"
},
"dependencies": {
"consolidate": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz",
"integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==",
"requires": {
"bluebird": "^3.1.1"
}
},
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ="
},
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"optional": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
}
}
},
"@vue/shared": { "@vue/shared": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.2.tgz",
@@ -5040,6 +5096,11 @@
} }
} }
}, },
"eslint-config-prettier": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz",
"integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ=="
},
"eslint-config-standard": { "eslint-config-standard": {
"version": "14.1.1", "version": "14.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz",
@@ -5381,6 +5442,18 @@
} }
} }
}, },
"eslint-plugin-prettier-vue": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier-vue/-/eslint-plugin-prettier-vue-2.1.1.tgz",
"integrity": "sha512-B9nYJCwf6508tc36fBU6a7QRwmp688Z8q6BPDvHLftR5KccqVNRkCUwPYjHXouw1m6NzdcRZraJ0A0jXwtNCTQ==",
"requires": {
"@vue/component-compiler-utils": "^3.1.2",
"chalk": "^4.0.0",
"prettier": "^1.18.2 || ^2.0.0",
"prettier-linter-helpers": "^1.0.0",
"vue-template-compiler": "^2.0.0"
}
},
"eslint-plugin-promise": { "eslint-plugin-promise": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz",
@@ -5391,6 +5464,27 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz",
"integrity": "sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==" "integrity": "sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ=="
}, },
"eslint-plugin-vue": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.3.0.tgz",
"integrity": "sha512-4rc9xrZgwT4aLz3XE6lrHu+FZtDLWennYvtzVvvS81kW9c65U4DUzQQWAFjDCgCFvN6HYWxi7ueEtxZVSB+f0g==",
"requires": {
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0",
"semver": "^7.3.2",
"vue-eslint-parser": "^7.3.0"
},
"dependencies": {
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"eslint-rule-docs": { "eslint-rule-docs": {
"version": "1.1.213", "version": "1.1.213",
"resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.213.tgz", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.213.tgz",
@@ -5708,6 +5802,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w=="
},
"fast-glob": { "fast-glob": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
@@ -8026,7 +8125,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
"integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
"optional": true,
"requires": { "requires": {
"source-map": "^0.6.1" "source-map": "^0.6.1"
}, },
@@ -8034,8 +8132,7 @@
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"optional": true
} }
} }
}, },
@@ -10597,9 +10694,17 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
}, },
"prettier": { "prettier": {
"version": "1.19.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==" "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q=="
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"requires": {
"fast-diff": "^1.1.2"
}
}, },
"pretty-error": { "pretty-error": {
"version": "2.1.2", "version": "2.1.2",
@@ -13473,6 +13578,36 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
}, },
"vue-eslint-parser": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.3.0.tgz",
"integrity": "sha512-n5PJKZbyspD0+8LnaZgpEvNCrjQx1DyDHw8JdWwoxhhC+yRip4TAvSDpXGf9SWX6b0umeB5aR61gwUo6NVvFxw==",
"requires": {
"debug": "^4.1.1",
"eslint-scope": "^5.0.0",
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.2.1",
"esquery": "^1.0.1",
"lodash": "^4.17.15"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
},
"espree": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
"integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
"requires": {
"acorn": "^7.1.1",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.1.0"
}
}
}
},
"vue-fullscreen": { "vue-fullscreen": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/vue-fullscreen/-/vue-fullscreen-2.1.6.tgz", "resolved": "https://registry.npmjs.org/vue-fullscreen/-/vue-fullscreen-2.1.6.tgz",
@@ -13637,6 +13772,11 @@
"uniq": "^1.0.1" "uniq": "^1.0.1"
} }
}, },
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="
},
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@@ -9,7 +9,7 @@
"watch": "webpack --watch", "watch": "webpack --watch",
"build": "webpack --optimize-minimize", "build": "webpack --optimize-minimize",
"lint": "eslint --cache src/ *.js", "lint": "eslint --cache src/ *.js",
"fmt": "eslint --cache --fix src/ *.js", "fmt": "eslint --cache --fix src/ *.js .eslintrc.js",
"test": "karma start", "test": "karma start",
"upgrade": "npm --depth 10 update && npm audit fix", "upgrade": "npm --depth 10 update && npm audit fix",
"acceptance": "testcafe \"chromium:headless --disable-dev-shm-usage\" --skip-js-errors --selector-timeout 5000 -S -s tests/screenshots tests/acceptance", "acceptance": "testcafe \"chromium:headless --disable-dev-shm-usage\" --skip-js-errors --selector-timeout 5000 -S -s tests/screenshots tests/acceptance",
@@ -50,6 +50,7 @@
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"easygettext": "^2.16.1", "easygettext": "^2.16.1",
"eslint": "^7.15.0", "eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-formatter-pretty": "^4.0.0", "eslint-formatter-pretty": "^4.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
@@ -57,8 +58,10 @@
"eslint-plugin-html": "^6.1.1", "eslint-plugin-html": "^6.1.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier-vue": "^2.1.1",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0", "eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.3.0",
"eventsource-polyfill": "^0.9.6", "eventsource-polyfill": "^0.9.6",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"friendly-errors-webpack-plugin": "^1.7.0", "friendly-errors-webpack-plugin": "^1.7.0",
@@ -94,6 +97,7 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"postcss-reporter": "^6.0.1", "postcss-reporter": "^6.0.1",
"postcss-url": "^8.0.0", "postcss-url": "^8.0.0",
"prettier": "^2.2.1",
"pubsub-js": "^1.9.2", "pubsub-js": "^1.9.2",
"puppeteer-core": "^5.5.0", "puppeteer-core": "^5.5.0",
"regenerator-runtime": "^0.13.7", "regenerator-runtime": "^0.13.7",

View File

@@ -1,7 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
"postcss-import": {}, "postcss-import": {},
"postcss-preset-env": {}, "postcss-preset-env": {},
"cssnano": {}, cssnano: {},
}, },
}; };

View File

@@ -41,8 +41,8 @@ import Log from "common/log";
import PhotoPrism from "app.vue"; import PhotoPrism from "app.vue";
import Router from "vue-router"; import Router from "vue-router";
import Routes from "routes"; import Routes from "routes";
import {config, session} from "session"; import { config, session } from "session";
import {Settings} from "luxon"; import { Settings } from "luxon";
import Socket from "common/websocket"; import Socket from "common/websocket";
import Viewer from "common/viewer"; import Viewer from "common/viewer";
import Vue from "vue"; import Vue from "vue";
@@ -53,13 +53,15 @@ import VueFullscreen from "vue-fullscreen";
import VueInfiniteScroll from "vue-infinite-scroll"; import VueInfiniteScroll from "vue-infinite-scroll";
import VueModal from "vue-js-modal"; import VueModal from "vue-js-modal";
import Hls from "hls.js"; import Hls from "hls.js";
import {$gettext, Mount} from "common/vm"; import { $gettext, Mount } from "common/vm";
// Initialize helpers // Initialize helpers
const viewer = new Viewer(); const viewer = new Viewer();
const clipboard = new Clipboard(window.localStorage, "photo_clipboard"); const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
const isPublic = config.get("public"); const isPublic = config.get("public");
const isMobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
// HTTP Live Streaming (video support) // HTTP Live Streaming (video support)
window.Hls = Hls; window.Hls = Hls;
@@ -77,23 +79,23 @@ Vue.prototype.$clipboard = clipboard;
Vue.prototype.$isMobile = isMobile; Vue.prototype.$isMobile = isMobile;
// Register Vuetify // Register Vuetify
Vue.use(Vuetify, {"theme": config.theme}); Vue.use(Vuetify, { theme: config.theme });
Vue.config.language = config.values.settings.ui.language; Vue.config.language = config.values.settings.ui.language;
Settings.defaultLocale = Vue.config.language.substring(0, 2); Settings.defaultLocale = Vue.config.language.substring(0, 2);
// Register other VueJS plugins // Register other VueJS plugins
Vue.use(GetTextPlugin, { Vue.use(GetTextPlugin, {
translations: config.translations, translations: config.translations,
silent: true, // !config.values.debug, silent: true, // !config.values.debug,
defaultLanguage: Vue.config.language, defaultLanguage: Vue.config.language,
autoAddKeyAttributes: true, autoAddKeyAttributes: true,
}); });
Vue.use(VueLuxon); Vue.use(VueLuxon);
Vue.use(VueInfiniteScroll); Vue.use(VueInfiniteScroll);
Vue.use(VueFullscreen); Vue.use(VueFullscreen);
Vue.use(VueModal, {dynamic: true, dynamicDefaults: {clickToClose: true}}); Vue.use(VueModal, { dynamic: true, dynamicDefaults: { clickToClose: true } });
Vue.use(VueFilters); Vue.use(VueFilters);
Vue.use(Components); Vue.use(Components);
Vue.use(Dialogs); Vue.use(Dialogs);
@@ -101,52 +103,52 @@ Vue.use(Router);
// Configure client-side routing // Configure client-side routing
const router = new Router({ const router = new Router({
routes: Routes, routes: Routes,
mode: "history", mode: "history",
saveScrollPosition: true, saveScrollPosition: true,
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.settings) && config.values.disable.settings) { if (to.matched.some((record) => record.meta.settings) && config.values.disable.settings) {
next({name: "home"}); next({ name: "home" });
} else if (to.matched.some(record => record.meta.admin)) { } else if (to.matched.some((record) => record.meta.admin)) {
if (isPublic || session.isAdmin()) { if (isPublic || session.isAdmin()) {
next(); next();
} else {
next({
name: "login",
params: {nextUrl: to.fullPath},
});
}
} else if (to.matched.some(record => record.meta.auth)) {
if (isPublic || session.isUser()) {
next();
} else {
next({
name: "login",
params: {nextUrl: to.fullPath},
});
}
} else { } else {
next(); next({
name: "login",
params: { nextUrl: to.fullPath },
});
} }
} else if (to.matched.some((record) => record.meta.auth)) {
if (isPublic || session.isUser()) {
next();
} else {
next({
name: "login",
params: { nextUrl: to.fullPath },
});
}
} else {
next();
}
}); });
router.afterEach((to) => { router.afterEach((to) => {
if (to.meta.title && config.values.siteTitle !== to.meta.title) { if (to.meta.title && config.values.siteTitle !== to.meta.title) {
config.page.title = $gettext(to.meta.title); config.page.title = $gettext(to.meta.title);
window.document.title = config.values.siteTitle + ": " + config.page.title; window.document.title = config.values.siteTitle + ": " + config.page.title;
} else { } else {
config.page.title = config.values.siteTitle; config.page.title = config.values.siteTitle;
window.document.title = config.values.siteTitle + ": " + config.values.siteCaption; window.document.title = config.values.siteTitle + ": " + config.values.siteCaption;
} }
}); });
// Pull client config every 10 minutes in case push fails (except on mobile to save battery). // Pull client config every 10 minutes in case push fails (except on mobile to save battery).
if (isMobile) { if (isMobile) {
document.body.classList.add("mobile"); document.body.classList.add("mobile");
} else { } else {
setInterval(() => config.update(), 600000); setInterval(() => config.update(), 600000);
} }
// Start application. // Start application.

View File

@@ -30,65 +30,73 @@ https://docs.photoprism.org/developer-guide/
import Axios from "axios"; import Axios from "axios";
import Notify from "common/notify"; import Notify from "common/notify";
import {$gettext} from "./vm"; import { $gettext } from "./vm";
const testConfig = {"jsHash":"48019917", "cssHash":"2b327230", "version": "test"}; const testConfig = { jsHash: "48019917", cssHash: "2b327230", version: "test" };
const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig; const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
const Api = Axios.create({ const Api = Axios.create({
baseURL: "/api/v1", baseURL: "/api/v1",
headers: {common: { headers: {
"X-Session-ID": window.localStorage.getItem("session_id"), common: {
"X-Client-Hash": config.jsHash, "X-Session-ID": window.localStorage.getItem("session_id"),
"X-Client-Version": config.version, "X-Client-Hash": config.jsHash,
}}, "X-Client-Version": config.version,
},
},
}); });
Api.interceptors.request.use(function (config) { Api.interceptors.request.use(
function (config) {
// Do something before request is sent // Do something before request is sent
Notify.ajaxStart(); Notify.ajaxStart();
return config; return config;
}, function (error) { },
function (error) {
// Do something with request error // Do something with request error
return Promise.reject(error); return Promise.reject(error);
}); }
);
Api.interceptors.response.use(function (response) { Api.interceptors.response.use(
function (response) {
Notify.ajaxEnd(); Notify.ajaxEnd();
if(typeof response.data == "string") { if (typeof response.data == "string") {
Notify.error($gettext("Request failed - invalid response")); Notify.error($gettext("Request failed - invalid response"));
console.warn("WARNING: Server returned HTML instead of JSON - API not implemented?"); console.warn("WARNING: Server returned HTML instead of JSON - API not implemented?");
} }
return response; return response;
}, function (error) { },
function (error) {
Notify.ajaxEnd(); Notify.ajaxEnd();
if (Axios.isCancel(error)) { if (Axios.isCancel(error)) {
return Promise.reject(error); return Promise.reject(error);
} }
if(console && console.log) { if (console && console.log) {
console.log(error); console.log(error);
} }
let errorMessage = $gettext("An error occurred - are you offline?"); let errorMessage = $gettext("An error occurred - are you offline?");
let code = error.code; let code = error.code;
if(error.response && error.response.data) { if (error.response && error.response.data) {
let data = error.response.data; let data = error.response.data;
code = data.code; code = data.code;
errorMessage = data.message ? data.message : data.error; errorMessage = data.message ? data.message : data.error;
} }
if (code === 401) { if (code === 401) {
Notify.logout(errorMessage); Notify.logout(errorMessage);
} else { } else {
Notify.error(errorMessage); Notify.error(errorMessage);
} }
return Promise.reject(error); return Promise.reject(error);
}); }
);
export default Api; export default Api;

View File

@@ -30,188 +30,188 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Notify from "common/notify"; import Notify from "common/notify";
import {$gettext} from "./vm"; import { $gettext } from "./vm";
export const MaxItems = 999; export const MaxItems = 999;
export default class Clipboard { export default class Clipboard {
/** /**
* @param {Storage} storage * @param {Storage} storage
* @param {string} key * @param {string} key
*/ */
constructor(storage, key) { constructor(storage, key) {
this.storageKey = key ? key : "clipboard"; this.storageKey = key ? key : "clipboard";
this.storage = storage; this.storage = storage;
this.selectionMap = {}; this.selectionMap = {};
this.selection = []; this.selection = [];
this.lastId = ""; this.lastId = "";
this.maxItems = MaxItems; this.maxItems = MaxItems;
this.loadFromStorage(); this.loadFromStorage();
}
isModel(model) {
if (!model) {
console.warn("Clipboard::isModel() - empty model", model);
return false;
} }
isModel(model) { if (typeof model.getId !== "function") {
if (!model) { console.warn("Clipboard::isModel() - model.getId() is not a function", model);
console.warn("Clipboard::isModel() - empty model", model); return false;
return false;
}
if (typeof model.getId !== "function") {
console.warn("Clipboard::isModel() - model.getId() is not a function", model);
return false;
}
return true;
} }
loadFromStorage() { return true;
const photosJson = this.storage.getItem(this.storageKey); }
if (photosJson !== null && typeof photosJson !== "undefined") { loadFromStorage() {
this.setIds(JSON.parse(photosJson)); const photosJson = this.storage.getItem(this.storageKey);
}
if (photosJson !== null && typeof photosJson !== "undefined") {
this.setIds(JSON.parse(photosJson));
}
}
saveToStorage() {
this.storage.setItem(this.storageKey, JSON.stringify(this.selection));
}
toggle(model) {
if (!this.isModel(model)) {
return;
} }
saveToStorage() { const id = model.getId();
this.storage.setItem(this.storageKey, JSON.stringify(this.selection)); this.toggleId(id);
}
toggleId(id) {
const index = this.selection.indexOf(id);
if (index === -1) {
if (this.selection.length >= this.maxItems) {
Notify.warn($gettext("Can't select more items"));
return;
}
this.selection.push(id);
this.selectionMap["id:" + id] = true;
this.lastId = id;
} else {
this.selection.splice(index, 1);
delete this.selectionMap["id:" + id];
this.lastId = "";
} }
toggle(model) { this.saveToStorage();
if (!this.isModel(model)) { }
return;
}
const id = model.getId(); add(model) {
this.toggleId(id); if (!this.isModel(model)) {
return;
} }
toggleId(id) { const id = model.getId();
const index = this.selection.indexOf(id);
if (index === -1) { this.addId(id);
if (this.selection.length >= this.maxItems) { }
Notify.warn($gettext("Can't select more items"));
return;
}
this.selection.push(id); addId(id) {
this.selectionMap["id:" + id] = true; if (this.hasId(id)) {
this.lastId = id; return;
} else {
this.selection.splice(index, 1);
delete this.selectionMap["id:" + id];
this.lastId = "";
}
this.saveToStorage();
} }
add(model) { if (this.selection.length >= this.maxItems) {
if (!this.isModel(model)) { Notify.warn($gettext("Can't select more items"));
return; return;
}
const id = model.getId();
this.addId(id);
} }
addId(id) { this.selection.push(id);
if (this.hasId(id)) { this.selectionMap["id:" + id] = true;
return; this.lastId = id;
}
if (this.selection.length >= this.maxItems) { this.saveToStorage();
Notify.warn($gettext("Can't select more items")); }
return;
}
this.selection.push(id); addRange(rangeEnd, models) {
this.selectionMap["id:" + id] = true; if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
this.lastId = id; console.warn("Clipboard::addRange() - invalid arguments:", rangeEnd, models);
return;
this.saveToStorage();
} }
addRange(rangeEnd, models) { let rangeStart = models.findIndex((photo) => photo.UID === this.lastId);
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("Clipboard::addRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((photo) => photo.UID === this.lastId); if (rangeStart === -1) {
this.toggle(models[rangeEnd]);
if (rangeStart === -1) { return 1;
this.toggle(models[rangeEnd]);
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.add(models[i]);
}
return (rangeEnd - rangeStart) + 1;
} }
has(model) { if (rangeStart > rangeEnd) {
if (!this.isModel(model)) { const newEnd = rangeStart;
return; rangeStart = rangeEnd;
} rangeEnd = newEnd;
return this.hasId(model.getId());
} }
hasId(id) { for (let i = rangeStart; i <= rangeEnd; i++) {
return typeof this.selectionMap["id:" + id] !== "undefined"; this.add(models[i]);
} }
remove(model) { return rangeEnd - rangeStart + 1;
if (!this.isModel(model)) { }
return;
}
this.removeId(model.getId()); has(model) {
if (!this.isModel(model)) {
return;
} }
removeId(id) { return this.hasId(model.getId());
if (!this.hasId(id)) return; }
const index = this.selection.indexOf(id); hasId(id) {
return typeof this.selectionMap["id:" + id] !== "undefined";
}
this.selection.splice(index, 1); remove(model) {
this.lastId = ""; if (!this.isModel(model)) {
delete this.selectionMap["id:" + id]; return;
this.saveToStorage();
} }
getIds() { this.removeId(model.getId());
return this.selection; }
removeId(id) {
if (!this.hasId(id)) return;
const index = this.selection.indexOf(id);
this.selection.splice(index, 1);
this.lastId = "";
delete this.selectionMap["id:" + id];
this.saveToStorage();
}
getIds() {
return this.selection;
}
setIds(ids) {
if (!Array.isArray(ids)) return;
this.selection = ids;
this.selectionMap = {};
this.lastId = "";
for (let i = 0; i < this.selection.length; i++) {
this.selectionMap["id:" + this.selection[i]] = true;
} }
}
setIds(ids) { clear() {
if (!Array.isArray(ids)) return; this.lastId = "";
this.selectionMap = {};
this.selection = ids; this.selection.splice(0, this.selection.length);
this.selectionMap = {}; this.storage.removeItem(this.storageKey);
this.lastId = ""; }
for (let i = 0; i < this.selection.length; i++) {
this.selectionMap["id:" + this.selection[i]] = true;
}
}
clear() {
this.lastId = "";
this.selectionMap = {};
this.selection.splice(0, this.selection.length);
this.storage.removeItem(this.storageKey);
}
} }

View File

@@ -34,198 +34,198 @@ import translations from "locales/translations.json";
import Api from "./api"; import Api from "./api";
export default class Config { export default class Config {
/** /**
* @param {Storage} storage * @param {Storage} storage
* @param {object} values * @param {object} values
*/ */
constructor(storage, values) { constructor(storage, values) {
this.disconnected = false; this.disconnected = false;
this.storage = storage; this.storage = storage;
this.storage_key = "config"; this.storage_key = "config";
this.$vuetify = null; this.$vuetify = null;
this.translations = translations; this.translations = translations;
if (!values || !values.siteTitle) { if (!values || !values.siteTitle) {
console.warn("config: values are empty"); console.warn("config: values are empty");
this.debug = true; this.debug = true;
this.demo = false; this.demo = false;
this.values = {}; this.values = {};
this.page = { this.page = {
title: "PhotoPrism", title: "PhotoPrism",
caption: "Browse Your Life", caption: "Browse Your Life",
}; };
return; return;
}
this.page = {
title: values.siteTitle,
caption: values.siteCaption,
};
this.values = values;
this.debug = !!values.debug;
this.demo = !!values.demo;
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
if (this.has("settings")) {
this.setTheme(this.get("settings").ui.theme);
} else {
this.setTheme("default");
}
} }
update() { this.page = {
Api.get("config").then( title: values.siteTitle,
(response) => this.setValues(response.data), caption: values.siteCaption,
() => console.warn("failed pulling updated client config") };
);
this.values = values;
this.debug = !!values.debug;
this.demo = !!values.demo;
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
if (this.has("settings")) {
this.setTheme(this.get("settings").ui.theme);
} else {
this.setTheme("default");
}
}
update() {
Api.get("config").then(
(response) => this.setValues(response.data),
() => console.warn("failed pulling updated client config")
);
}
setValues(values) {
if (!values) return;
if (this.debug) {
console.log("config: new values", values);
} }
setValues(values) { if (values.jsHash && this.values.jsHash !== values.jsHash) {
if (!values) return; Event.publish("dialog.reload", { values });
if (this.debug) {
console.log("config: new values", values);
}
if (values.jsHash && this.values.jsHash !== values.jsHash) {
Event.publish("dialog.reload", {values});
}
for (let key in values) {
if (values.hasOwnProperty(key)) {
this.set(key, values[key]);
}
}
if (values.settings) {
this.setTheme(values.settings.ui.theme);
}
return this;
} }
onCount(ev, data) { for (let key in values) {
const type = ev.split(".")[1]; if (values.hasOwnProperty(key)) {
this.set(key, values[key]);
switch (type) { }
case "cameras":
this.values.count.cameras += data.count;
this.update();
break;
case "lenses":
this.values.count.lenses += data.count;
break;
case "countries":
this.values.count.countries += data.count;
this.update();
break;
case "states":
this.values.count.states += data.count;
break;
case "places":
this.values.count.places += data.count;
break;
case "labels":
this.values.count.labels += data.count;
break;
case "videos":
this.values.count.videos += data.count;
break;
case "albums":
this.values.count.albums += data.count;
break;
case "moments":
this.values.count.moments += data.count;
break;
case "months":
this.values.count.months += data.count;
break;
case "folders":
this.values.count.folders += data.count;
break;
case "files":
this.values.count.files += data.count;
break;
case "favorites":
this.values.count.favorites += data.count;
break;
case "review":
this.values.count.review += data.count;
break;
case "private":
this.values.count.private += data.count;
break;
case "photos":
this.values.count.photos += data.count;
break;
default:
console.warn("unknown count type", ev, data);
}
this.values.count;
} }
setVuetify(instance) { if (values.settings) {
this.$vuetify = instance; this.setTheme(values.settings.ui.theme);
} }
setTheme(name) { return this;
this.theme = themes[name] ? themes[name] : themes["default"]; }
if (this.$vuetify) { onCount(ev, data) {
this.$vuetify.theme = this.theme; const type = ev.split(".")[1];
}
return this; switch (type) {
case "cameras":
this.values.count.cameras += data.count;
this.update();
break;
case "lenses":
this.values.count.lenses += data.count;
break;
case "countries":
this.values.count.countries += data.count;
this.update();
break;
case "states":
this.values.count.states += data.count;
break;
case "places":
this.values.count.places += data.count;
break;
case "labels":
this.values.count.labels += data.count;
break;
case "videos":
this.values.count.videos += data.count;
break;
case "albums":
this.values.count.albums += data.count;
break;
case "moments":
this.values.count.moments += data.count;
break;
case "months":
this.values.count.months += data.count;
break;
case "folders":
this.values.count.folders += data.count;
break;
case "files":
this.values.count.files += data.count;
break;
case "favorites":
this.values.count.favorites += data.count;
break;
case "review":
this.values.count.review += data.count;
break;
case "private":
this.values.count.private += data.count;
break;
case "photos":
this.values.count.photos += data.count;
break;
default:
console.warn("unknown count type", ev, data);
} }
getValues() { this.values.count;
return this.values; }
setVuetify(instance) {
this.$vuetify = instance;
}
setTheme(name) {
this.theme = themes[name] ? themes[name] : themes["default"];
if (this.$vuetify) {
this.$vuetify.theme = this.theme;
} }
storeValues() { return this;
this.storage.setItem(this.storage_key, JSON.stringify(this.getValues())); }
return this;
getValues() {
return this.values;
}
storeValues() {
this.storage.setItem(this.storage_key, JSON.stringify(this.getValues()));
return this;
}
set(key, value) {
this.values[key] = value;
return this;
}
has(key) {
return !!this.values[key];
}
get(key) {
return this.values[key];
}
feature(name) {
return this.values.settings.features[name];
}
settings() {
return this.values.settings;
}
downloadToken() {
return this.values["downloadToken"];
}
previewToken() {
return this.values["previewToken"];
}
albumCategories() {
if (this.values["albumCategories"]) {
return this.values["albumCategories"];
} }
set(key, value) { return [];
this.values[key] = value; }
return this;
}
has(key) {
return !!this.values[key];
}
get(key) {
return this.values[key];
}
feature(name) {
return this.values.settings.features[name];
}
settings() {
return this.values.settings;
}
downloadToken() {
return this.values["downloadToken"];
}
previewToken() {
return this.values["previewToken"];
}
albumCategories() {
if (this.values["albumCategories"]) {
return this.values["albumCategories"];
}
return [];
}
} }

View File

@@ -29,80 +29,80 @@ https://docs.photoprism.org/developer-guide/
*/ */
export const FormPropertyType = Object.freeze({ export const FormPropertyType = Object.freeze({
String: "string", String: "string",
Number: "number", Number: "number",
Object: "object", Object: "object",
}); });
export default class Form { export default class Form {
constructor(definition) { constructor(definition) {
this.definition = definition; this.definition = definition;
}
setValues(values) {
const def = this.getDefinition();
for (let prop in def) {
if (values.hasOwnProperty(prop)) {
this.setValue(prop, values[prop]);
}
} }
setValues(values) { return this;
const def = this.getDefinition(); }
for (let prop in def) { getValues() {
if (values.hasOwnProperty(prop)) { const result = {};
this.setValue(prop, values[prop]); const def = this.getDefinition();
}
}
return this; for (let prop in def) {
result[prop] = this.getValue(prop);
} }
getValues() { return result;
const result = {}; }
const def = this.getDefinition();
for (let prop in def) { setValue(name, value) {
result[prop] = this.getValue(prop); const def = this.getDefinition();
}
return result; if (!def.hasOwnProperty(name)) {
throw `Property ${name} not found`;
} else if (typeof value != def[name].type) {
throw `Property ${name} must be ${def[name].type}`;
} else {
def[name].value = value;
} }
setValue(name, value) { return this;
const def = this.getDefinition(); }
if (!def.hasOwnProperty(name)) { getValue(name) {
throw `Property ${name} not found`; const def = this.getDefinition();
} else if (typeof value != def[name].type) {
throw `Property ${name} must be ${def[name].type}`;
} else {
def[name].value = value;
}
return this; if (def.hasOwnProperty(name)) {
return def[name].value;
} else {
throw `Property ${name} not found`;
}
}
setDefinition(definition) {
this.definition = definition;
}
getDefinition() {
return this.definition ? this.definition : {};
}
getOptions(fieldName) {
if (
this.definition &&
this.definition.hasOwnProperty(fieldName) &&
this.definition[fieldName].hasOwnProperty("options")
) {
return this.definition[fieldName].options;
} }
getValue(name) { return [{ option: "", label: "" }];
const def = this.getDefinition(); }
if (def.hasOwnProperty(name)) {
return def[name].value;
} else {
throw `Property ${name} not found`;
}
}
setDefinition(definition) {
this.definition = definition;
}
getDefinition() {
return this.definition ? this.definition : {};
}
getOptions(fieldName) {
if (
this.definition &&
this.definition.hasOwnProperty(fieldName) &&
this.definition[fieldName].hasOwnProperty("options")
) {
return this.definition[fieldName].options;
}
return [{ option: "", label: "" }];
}
} }

View File

@@ -31,35 +31,35 @@ https://docs.photoprism.org/developer-guide/
import Event from "pubsub-js"; import Event from "pubsub-js";
class Log { class Log {
constructor() { constructor() {
this.cap = 150; this.cap = 150;
this.created = new Date; this.created = new Date();
this.logs = [ this.logs = [
/* EXAMPLE LOG MESSAGE /* EXAMPLE LOG MESSAGE
{ {
"message": "waiting for events", "message": "waiting for events",
"level": "debug", "level": "debug",
"time": this.created.toISOString(), "time": this.created.toISOString(),
}, },
*/ */
]; ];
this.logId = 0; this.logId = 0;
Event.subscribe("log", this.onLog.bind(this)); Event.subscribe("log", this.onLog.bind(this));
} }
onLog(ev, data) { onLog(ev, data) {
data.id = this.logId++; data.id = this.logId++;
this.logs.unshift(data); this.logs.unshift(data);
if(this.logs.length > this.cap) { if (this.logs.length > this.cap) {
this.logs.splice(this.cap); this.logs.splice(this.cap);
}
} }
}
} }
const log = new Log; const log = new Log();
export default log; export default log;

View File

@@ -29,48 +29,48 @@ https://docs.photoprism.org/developer-guide/
*/ */
import Event from "pubsub-js"; import Event from "pubsub-js";
import {$gettext} from "./vm"; import { $gettext } from "./vm";
const Notify = { const Notify = {
info: function (message) { info: function (message) {
Event.publish("notify.info", {message}); Event.publish("notify.info", { message });
}, },
warn: function (message) { warn: function (message) {
Event.publish("notify.warning", {message}); Event.publish("notify.warning", { message });
}, },
error: function (message) { error: function (message) {
Event.publish("notify.error", {message}); Event.publish("notify.error", { message });
}, },
success: function (message) { success: function (message) {
Event.publish("notify.success", {message}); Event.publish("notify.success", { message });
}, },
logout: function (message) { logout: function (message) {
Event.publish("notify.error", {message}); Event.publish("notify.error", { message });
Event.publish("session.logout", {message}); Event.publish("session.logout", { message });
}, },
ajaxStart: function() { ajaxStart: function () {
Event.publish("ajax.start"); Event.publish("ajax.start");
}, },
ajaxEnd: function() { ajaxEnd: function () {
Event.publish("ajax.end"); Event.publish("ajax.end");
}, },
blockUI: function() { blockUI: function () {
const el = document.getElementById("busy-overlay"); const el = document.getElementById("busy-overlay");
if(el) { if (el) {
el.style.display = "block"; el.style.display = "block";
} }
}, },
unblockUI: function() { unblockUI: function () {
const el = document.getElementById("busy-overlay"); const el = document.getElementById("busy-overlay");
if(el) { if (el) {
el.style.display = "none"; el.style.display = "none";
} }
}, },
wait: function () { wait: function () {
this.warn($gettext("Busy, please wait…")); this.warn($gettext("Busy, please wait…"));
}, },
}; };
export default Notify; export default Notify;

View File

@@ -34,217 +34,213 @@ import User from "model/user";
import Socket from "./websocket"; import Socket from "./websocket";
export default class Session { export default class Session {
/** /**
* @param {Storage} storage * @param {Storage} storage
* @param {Config} config * @param {Config} config
*/ */
constructor(storage, config) { constructor(storage, config) {
this.auth = false; this.auth = false;
this.config = config; this.config = config;
if (storage.getItem("session_storage") === "true") { if (storage.getItem("session_storage") === "true") {
this.storage = window.sessionStorage; this.storage = window.sessionStorage;
} else { } else {
this.storage = storage; this.storage = storage;
} }
if (this.applyId(this.storage.getItem("session_id"))) { if (this.applyId(this.storage.getItem("session_id"))) {
const dataJson = this.storage.getItem("data"); const dataJson = this.storage.getItem("data");
this.data = dataJson !== "undefined" ? JSON.parse(dataJson) : null; this.data = dataJson !== "undefined" ? JSON.parse(dataJson) : null;
} }
if (this.data && this.data.user) { if (this.data && this.data.user) {
this.user = new User(this.data.user); this.user = new User(this.data.user);
} }
if (this.isUser()) { if (this.isUser()) {
this.auth = true; this.auth = true;
} }
Event.subscribe("session.logout", () => { Event.subscribe("session.logout", () => {
return this.onLogout(); return this.onLogout();
});
Event.subscribe("websocket.connected", () => {
this.sendClientInfo();
});
this.sendClientInfo();
}
useSessionStorage() {
this.deleteId();
this.storage.setItem("session_storage", "true");
this.storage = window.sessionStorage;
}
useLocalStorage() {
this.storage.setItem("session_storage", "false");
this.storage = window.localStorage;
}
applyId(id) {
if (!id) {
this.deleteId();
return false;
}
this.session_id = id;
Api.defaults.headers.common["X-Session-ID"] = id;
return true;
}
setId(id) {
this.storage.setItem("session_id", id);
return this.applyId(id);
}
setConfig(values) {
this.config.setValues(values);
}
getId() {
return this.session_id;
}
hasId() {
return !!this.session_id;
}
deleteId() {
this.session_id = null;
this.storage.removeItem("session_id");
delete Api.defaults.headers.common["X-Session-ID"];
this.deleteData();
}
setData(data) {
if (!data) {
return;
}
this.data = data;
this.user = new User(this.data.user);
this.storage.setItem("data", JSON.stringify(data));
this.auth = true;
}
getUser() {
return this.user;
}
getEmail() {
if (this.isUser()) {
return this.user.PrimaryEmail;
}
return "";
}
getNickName() {
if (this.isUser()) {
return this.user.NickName;
}
return "";
}
getFullName() {
if (this.isUser()) {
return this.user.FullName;
}
return "";
}
isUser() {
return this.user && this.user.hasId();
}
isAdmin() {
return this.user && this.user.hasId() && this.user.RoleAdmin;
}
isAnonymous() {
return !this.user || !this.user.hasId();
}
hasToken(token) {
if (!this.data || !this.data.tokens) {
return false;
}
return this.data.tokens.indexOf(token) >= 0;
}
deleteData() {
this.auth = false;
this.user = new User();
this.data = null;
this.storage.removeItem("data");
}
sendClientInfo() {
const clientInfo = {
session: this.getId(),
js: window.__CONFIG__.jsHash,
css: window.__CONFIG__.cssHash,
version: window.__CONFIG__.version,
};
try {
Socket.send(JSON.stringify(clientInfo));
} catch (e) {
if (this.config.debug) {
console.log("session: can't use websocket, not connected (yet)");
}
}
}
login(username, password, token) {
this.deleteId();
return Api.post("session", { username, password, token }).then((resp) => {
this.setConfig(resp.data.config);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
});
}
redeemToken(token) {
return Api.post("session", { token }).then((resp) => {
this.setConfig(resp.data.config);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
});
}
onLogout(noRedirect) {
this.deleteId();
if (noRedirect !== true) {
window.location = "/";
}
return Promise.resolve();
}
logout(noRedirect) {
if (this.hasId()) {
return Api.delete("session/" + this.getId())
.then(() => {
return this.onLogout(noRedirect);
})
.catch(() => {
return this.onLogout(noRedirect);
}); });
} else {
Event.subscribe("websocket.connected", () => { return this.onLogout(noRedirect);
this.sendClientInfo();
});
this.sendClientInfo();
}
useSessionStorage() {
this.deleteId();
this.storage.setItem("session_storage", "true");
this.storage = window.sessionStorage;
}
useLocalStorage() {
this.storage.setItem("session_storage", "false");
this.storage = window.localStorage;
}
applyId(id) {
if (!id) {
this.deleteId();
return false;
}
this.session_id = id;
Api.defaults.headers.common["X-Session-ID"] = id;
return true;
}
setId(id) {
this.storage.setItem("session_id", id);
return this.applyId(id);
}
setConfig(values) {
this.config.setValues(values);
}
getId() {
return this.session_id;
}
hasId() {
return !!this.session_id;
}
deleteId() {
this.session_id = null;
this.storage.removeItem("session_id");
delete Api.defaults.headers.common["X-Session-ID"];
this.deleteData();
}
setData(data) {
if (!data) {
return;
}
this.data = data;
this.user = new User(this.data.user);
this.storage.setItem("data", JSON.stringify(data));
this.auth = true;
}
getUser() {
return this.user;
}
getEmail() {
if (this.isUser()) {
return this.user.PrimaryEmail;
}
return "";
}
getNickName() {
if (this.isUser()) {
return this.user.NickName;
}
return "";
}
getFullName() {
if (this.isUser()) {
return this.user.FullName;
}
return "";
}
isUser() {
return this.user && this.user.hasId();
}
isAdmin() {
return this.user && this.user.hasId() && this.user.RoleAdmin;
}
isAnonymous() {
return !this.user || !this.user.hasId();
}
hasToken(token) {
if (!this.data || !this.data.tokens) {
return false;
}
return this.data.tokens.indexOf(token) >= 0;
}
deleteData() {
this.auth = false;
this.user = new User;
this.data = null;
this.storage.removeItem("data");
}
sendClientInfo() {
const clientInfo = {
"session": this.getId(),
"js": window.__CONFIG__.jsHash,
"css": window.__CONFIG__.cssHash,
"version": window.__CONFIG__.version,
};
try {
Socket.send(JSON.stringify(clientInfo));
} catch (e) {
if (this.config.debug) {
console.log("session: can't use websocket, not connected (yet)");
}
}
}
login(username, password, token) {
this.deleteId();
return Api.post("session", {username, password, token}).then(
(resp) => {
this.setConfig(resp.data.config);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
}
);
}
redeemToken(token) {
return Api.post("session", {token}).then(
(resp) => {
this.setConfig(resp.data.config);
this.setId(resp.data.id);
this.setData(resp.data.data);
this.sendClientInfo();
}
);
}
onLogout(noRedirect) {
this.deleteId();
if (noRedirect !== true) {
window.location = "/";
}
return Promise.resolve();
}
logout(noRedirect) {
if (this.hasId()) {
return Api.delete("session/" + this.getId())
.then(() => {
return this.onLogout(noRedirect);
})
.catch(() => {
return this.onLogout(noRedirect);
});
} else {
return this.onLogout(noRedirect);
}
} }
}
} }

View File

@@ -36,94 +36,93 @@ const Minute = 60 * Second;
const Hour = 60 * Minute; const Hour = 60 * Minute;
export default class Util { export default class Util {
static duration(d) { static duration(d) {
let u = d; let u = d;
let neg = d < 0; let neg = d < 0;
if (neg) { if (neg) {
u = -u; u = -u;
}
if (u < Second) {
// Special case: if duration is smaller than a second,
// use smaller units, like 1.2ms
if (!u) {
return "0s";
}
if (u < Microsecond) {
return u + "ns";
}
if (u < Millisecond) {
return Math.round(u / Microsecond) + "µs";
}
return Math.round(u / Millisecond) + "ms";
}
let result = [];
let h = Math.floor(u / Hour);
let min = Math.floor(u / Minute)%60;
let sec = Math.ceil(u / Second)%60;
result.push(h.toString().padStart(2, "0"));
result.push(min.toString().padStart(2, "0"));
result.push(sec.toString().padStart(2, "0"));
// return `${h}h${min}m${sec}s`
return result.join(":");
} }
static arabicToRoman(number) { if (u < Second) {
let roman = ""; // Special case: if duration is smaller than a second,
const romanNumList = { // use smaller units, like 1.2ms
M: 1000, if (!u) {
CM: 900, return "0s";
D: 500, }
CD: 400,
C: 100,
XC: 90,
L: 50,
XV: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
let a;
if (number < 1 || number > 3999)
return "";
else {
for (let key in romanNumList) {
a = Math.floor(number / romanNumList[key]);
if (a >= 0) {
for (let i = 0; i < a; i++) {
roman += key;
}
}
number = number % romanNumList[key];
}
}
return roman; if (u < Microsecond) {
return u + "ns";
}
if (u < Millisecond) {
return Math.round(u / Microsecond) + "µs";
}
return Math.round(u / Millisecond) + "ms";
} }
static truncate(str, length, ending) { let result = [];
if (length == null) {
length = 100; let h = Math.floor(u / Hour);
} let min = Math.floor(u / Minute) % 60;
if (ending == null) { let sec = Math.ceil(u / Second) % 60;
ending = "…";
} result.push(h.toString().padStart(2, "0"));
if (str.length > length) { result.push(min.toString().padStart(2, "0"));
return str.substring(0, length - ending.length) + ending; result.push(sec.toString().padStart(2, "0"));
} else {
return str; // return `${h}h${min}m${sec}s`
return result.join(":");
}
static arabicToRoman(number) {
let roman = "";
const romanNumList = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XV: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
let a;
if (number < 1 || number > 3999) return "";
else {
for (let key in romanNumList) {
a = Math.floor(number / romanNumList[key]);
if (a >= 0) {
for (let i = 0; i < a; i++) {
roman += key;
}
} }
number = number % romanNumList[key];
}
} }
return roman;
}
static truncate(str, length, ending) {
if (length == null) {
length = 100;
}
if (ending == null) {
ending = "…";
}
if (str.length > length) {
return str.substring(0, length - ending.length) + ending;
} else {
return str;
}
}
} }

View File

@@ -36,151 +36,186 @@ import stripHtml from "string-strip-html";
const thumbs = window.__CONFIG__.thumbs; const thumbs = window.__CONFIG__.thumbs;
class Viewer { class Viewer {
constructor() { constructor() {
this.el = null; this.el = null;
this.gallery = null; this.gallery = null;
}
getEl() {
if (!this.el) {
this.el = document.getElementById("p-photo-viewer");
if (this.el === null) {
let err = "no photo viewer element found";
console.warn(err);
throw err;
}
} }
getEl() { return this.el;
if (!this.el) { }
this.el = document.getElementById("p-photo-viewer");
if (this.el === null) { show(items, index = 0) {
let err = "no photo viewer element found"; if (!Array.isArray(items) || items.length === 0 || index >= items.length) {
console.warn(err); console.log("photo list passed to gallery was empty:", items);
throw err; return;
} }
const shareButtons = [
{
id: "fit_720",
template: "Tiny (size)",
label: "Tiny",
url: "{{raw_image_url}}",
download: true,
},
{
id: "fit_1280",
template: "Small (size)",
label: "Small",
url: "{{raw_image_url}}",
download: true,
},
{
id: "fit_2048",
template: "Medium (size)",
label: "Medium",
url: "{{raw_image_url}}",
download: true,
},
{
id: "fit_2560",
template: "Large (size)",
label: "Large",
url: "{{raw_image_url}}",
download: true,
},
{
id: "original",
template: "Original (size)",
label: "Original",
url: "{{raw_image_url}}",
download: true,
},
];
const options = {
index: index,
history: true,
preload: [1, 1],
focus: true,
modal: true,
closeEl: true,
captionEl: true,
fullscreenEl: true,
zoomEl: true,
shareEl: true,
shareButtons: shareButtons,
counterEl: false,
arrowEl: true,
preloaderEl: true,
addCaptionHTMLFn: function (item, captionEl /*, isFake */) {
// item - slide object
// captionEl - caption DOM element
// isFake - true when content is added to fake caption container
// (used to get size of next or previous caption)
if (!item.title) {
captionEl.children[0].innerHTML = "";
return false;
} }
return this.el; captionEl.children[0].innerHTML = stripHtml(item.title);
}
show(items, index = 0) { if (item.playable) {
if (!Array.isArray(items) || items.length === 0 || index >= items.length) { captionEl.children[0].innerHTML +=
console.log("photo list passed to gallery was empty:", items); ' <i aria-hidden="true" class="v-icon material-icons theme--dark">movie_creation</i>';
return;
} }
const shareButtons = [ if (item.description) {
{id: "fit_720", template: "Tiny (size)", label: "Tiny", url: "{{raw_image_url}}", download: true}, captionEl.children[0].innerHTML +=
{id: "fit_1280", template: "Small (size)", label: "Small", url: "{{raw_image_url}}", download: true}, '<br><span class="description">' + stripHtml(item.description) + "</span>";
{id: "fit_2048", template: "Medium (size)", label: "Medium", url: "{{raw_image_url}}", download: true},
{id: "fit_2560", template: "Large (size)", label: "Large", url: "{{raw_image_url}}", download: true},
{id: "original", template: "Original (size)", label: "Original", url: "{{raw_image_url}}", download: true},
];
const options = {
index: index,
history: true,
preload: [1, 1],
focus: true,
modal: true,
closeEl: true,
captionEl: true,
fullscreenEl: true,
zoomEl: true,
shareEl: true,
shareButtons: shareButtons,
counterEl: false,
arrowEl: true,
preloaderEl: true,
addCaptionHTMLFn: function(item, captionEl /*, isFake */) {
// item - slide object
// captionEl - caption DOM element
// isFake - true when content is added to fake caption container
// (used to get size of next or previous caption)
if(!item.title) {
captionEl.children[0].innerHTML = "";
return false;
}
captionEl.children[0].innerHTML = stripHtml(item.title);
if(item.playable) {
captionEl.children[0].innerHTML += " <i aria-hidden=\"true\" class=\"v-icon material-icons theme--dark\">movie_creation</i>";
}
if(item.description) {
captionEl.children[0].innerHTML += "<br><span class=\"description\">" + stripHtml(item.description) + "</span>";
}
if(item.playable) {
captionEl.children[0].innerHTML = "<button>" + captionEl.children[0].innerHTML + "</button>";
}
return true;
},
};
let gallery = new PhotoSwipe(this.getEl(), PhotoSwipeUI_Default, items, options);
let realViewportWidth;
let realViewportHeight;
let previousSize;
let nextSize;
let firstResize = true;
let photoSrcWillChange;
this.gallery = gallery;
Event.publish("viewer.show");
gallery.listen("close", () => {
Event.publish("viewer.pause");
Event.publish("viewer.hide");
});
gallery.listen("shareLinkClick", () => Event.publish("viewer.pause"));
gallery.listen("initialZoomIn", () => Event.publish("viewer.pause"));
gallery.listen("initialZoomOut", () => Event.publish("viewer.pause"));
gallery.listen("beforeChange", () => Event.publish("viewer.change", {gallery: gallery, item: gallery.currItem}));
gallery.listen("beforeResize", () => {
realViewportWidth = gallery.viewportSize.x * window.devicePixelRatio;
realViewportHeight = gallery.viewportSize.y * window.devicePixelRatio;
if (!previousSize) {
previousSize = "tile_720";
}
nextSize = this.constructor.mapViewportToImageSize(realViewportWidth, realViewportHeight);
if (nextSize !== previousSize) {
photoSrcWillChange = true;
}
if (photoSrcWillChange && !firstResize) {
gallery.invalidateCurrItems();
}
if (firstResize) {
firstResize = false;
}
photoSrcWillChange = false;
});
gallery.listen("gettingData", function (index, item) {
item.src = item[nextSize].src;
item.w = item[nextSize].w;
item.h = item[nextSize].h;
previousSize = nextSize;
});
gallery.init();
}
static mapViewportToImageSize(viewportWidth, viewportHeight) {
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
if (t.w >= viewportWidth || t.h >= viewportHeight) {
return t.size;
}
} }
return "fit_7680"; if (item.playable) {
captionEl.children[0].innerHTML =
"<button>" + captionEl.children[0].innerHTML + "</button>";
}
return true;
},
};
let gallery = new PhotoSwipe(this.getEl(), PhotoSwipeUI_Default, items, options);
let realViewportWidth;
let realViewportHeight;
let previousSize;
let nextSize;
let firstResize = true;
let photoSrcWillChange;
this.gallery = gallery;
Event.publish("viewer.show");
gallery.listen("close", () => {
Event.publish("viewer.pause");
Event.publish("viewer.hide");
});
gallery.listen("shareLinkClick", () => Event.publish("viewer.pause"));
gallery.listen("initialZoomIn", () => Event.publish("viewer.pause"));
gallery.listen("initialZoomOut", () => Event.publish("viewer.pause"));
gallery.listen("beforeChange", () =>
Event.publish("viewer.change", { gallery: gallery, item: gallery.currItem })
);
gallery.listen("beforeResize", () => {
realViewportWidth = gallery.viewportSize.x * window.devicePixelRatio;
realViewportHeight = gallery.viewportSize.y * window.devicePixelRatio;
if (!previousSize) {
previousSize = "tile_720";
}
nextSize = this.constructor.mapViewportToImageSize(realViewportWidth, realViewportHeight);
if (nextSize !== previousSize) {
photoSrcWillChange = true;
}
if (photoSrcWillChange && !firstResize) {
gallery.invalidateCurrItems();
}
if (firstResize) {
firstResize = false;
}
photoSrcWillChange = false;
});
gallery.listen("gettingData", function (index, item) {
item.src = item[nextSize].src;
item.w = item[nextSize].w;
item.h = item[nextSize].h;
previousSize = nextSize;
});
gallery.init();
}
static mapViewportToImageSize(viewportWidth, viewportHeight) {
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
if (t.w >= viewportWidth || t.h >= viewportHeight) {
return t.size;
}
} }
return "fit_7680";
}
} }
export default Viewer; export default Viewer;

View File

@@ -1,21 +1,23 @@
export let vm = { export let vm = {
$gettext: (msgid) => msgid, $gettext: (msgid) => msgid,
$ngettext: (msgid, plural, n) => { return n > 1 ? plural : msgid; }, $ngettext: (msgid, plural, n) => {
$pgettext:(context, msgid) => msgid, return n > 1 ? plural : msgid;
$npgettext: (context, msgid) => msgid, },
$pgettext: (context, msgid) => msgid,
$npgettext: (context, msgid) => msgid,
}; };
export function $gettext (msgid) { export function $gettext(msgid) {
return vm.$gettext(msgid); return vm.$gettext(msgid);
} }
export function $ngettext (msgid, plural, n) { export function $ngettext(msgid, plural, n) {
return vm.$ngettext(msgid, plural, n); return vm.$ngettext(msgid, plural, n);
} }
export function Mount (Vue, app, router) { export function Mount(Vue, app, router) {
vm = new Vue({ vm = new Vue({
router, router,
render: h => h(app), render: (h) => h(app),
}).$mount("#photoprism"); }).$mount("#photoprism");
} }

View File

@@ -30,31 +30,31 @@ https://docs.photoprism.org/developer-guide/
import Sockette from "sockette"; import Sockette from "sockette";
import Event from "pubsub-js"; import Event from "pubsub-js";
import {config} from "session"; import { config } from "session";
const host = window.location.host; const host = window.location.host;
const prot = ("https:" === document.location.protocol ? "wss://" : "ws://"); const prot = "https:" === document.location.protocol ? "wss://" : "ws://";
const url = prot + host + "/api/v1/ws"; const url = prot + host + "/api/v1/ws";
const Socket = new Sockette(url, { const Socket = new Sockette(url, {
timeout: 5e3, timeout: 5e3,
onopen: e => { onopen: (e) => {
console.log("websocket: connected"); console.log("websocket: connected");
config.disconnected = false; config.disconnected = false;
document.body.classList.remove("disconnected"); document.body.classList.remove("disconnected");
Event.publish("websocket.connected", e); Event.publish("websocket.connected", e);
}, },
onmessage: e => { onmessage: (e) => {
const m = JSON.parse(e.data); const m = JSON.parse(e.data);
Event.publish(m.event, m.data); Event.publish(m.event, m.data);
}, },
onreconnect: () => console.log("websocket: reconnecting"), onreconnect: () => console.log("websocket: reconnecting"),
onmaximum: () => console.warn("websocket: hit max reconnect limit"), onmaximum: () => console.warn("websocket: hit max reconnect limit"),
onclose: () => { onclose: () => {
console.warn("websocket: disconnected"); console.warn("websocket: disconnected");
config.disconnected = true; config.disconnected = true;
document.body.classList.add("disconnected"); document.body.classList.add("disconnected");
}, },
}); });
export default Socket; export default Socket;

View File

@@ -48,22 +48,22 @@ import PAboutFooter from "./footer.vue";
const components = {}; const components = {};
components.install = (Vue) => { components.install = (Vue) => {
Vue.component("p-notify", PNotify); Vue.component("PNotify", PNotify);
Vue.component("p-navigation", PNavigation); Vue.component("PNavigation", PNavigation);
Vue.component("p-scroll-top", PScrollTop); Vue.component("PScrollTop", PScrollTop);
Vue.component("p-loading-bar", PLoadingBar); Vue.component("PLoadingBar", PLoadingBar);
Vue.component("p-video-player", PVideoPlayer); Vue.component("PVideoPlayer", PVideoPlayer);
Vue.component("p-photo-viewer", PPhotoViewer); Vue.component("PPhotoViewer", PPhotoViewer);
Vue.component("p-photo-toolbar", PPhotoToolbar); Vue.component("PPhotoToolbar", PPhotoToolbar);
Vue.component("p-photo-cards", PPhotoCards); Vue.component("PPhotoCards", PPhotoCards);
Vue.component("p-photo-mosaic", PPhotoMosaic); Vue.component("PPhotoMosaic", PPhotoMosaic);
Vue.component("p-photo-list", PPhotoList); Vue.component("PPhotoList", PPhotoList);
Vue.component("p-photo-clipboard", PPhotoClipboard); Vue.component("PPhotoClipboard", PPhotoClipboard);
Vue.component("p-album-clipboard", PAlbumClipboard); Vue.component("PAlbumClipboard", PAlbumClipboard);
Vue.component("p-album-toolbar", PAlbumToolbar); Vue.component("PAlbumToolbar", PAlbumToolbar);
Vue.component("p-label-clipboard", PLabelClipboard); Vue.component("PLabelClipboard", PLabelClipboard);
Vue.component("p-file-clipboard", PFileClipboard); Vue.component("PFileClipboard", PFileClipboard);
Vue.component("p-about-footer", PAboutFooter); Vue.component("PAboutFooter", PAboutFooter);
}; };
export default components; export default components;

View File

@@ -48,22 +48,22 @@ import PReloadDialog from "./reload.vue";
const dialogs = {}; const dialogs = {};
dialogs.install = (Vue) => { dialogs.install = (Vue) => {
Vue.component("p-account-add-dialog", PAccountAddDialog); Vue.component("PAccountAddDialog", PAccountAddDialog);
Vue.component("p-account-remove-dialog", PAccountRemoveDialog); Vue.component("PAccountRemoveDialog", PAccountRemoveDialog);
Vue.component("p-account-edit-dialog", PAccountEditDialog); Vue.component("PAccountEditDialog", PAccountEditDialog);
Vue.component("p-photo-archive-dialog", PPhotoArchiveDialog); Vue.component("PPhotoArchiveDialog", PPhotoArchiveDialog);
Vue.component("p-photo-album-dialog", PPhotoAlbumDialog); Vue.component("PPhotoAlbumDialog", PPhotoAlbumDialog);
Vue.component("p-photo-edit-dialog", PPhotoEditDialog); Vue.component("PPhotoEditDialog", PPhotoEditDialog);
Vue.component("p-file-delete-dialog", PFileDeleteDialog); Vue.component("PFileDeleteDialog", PFileDeleteDialog);
Vue.component("p-album-edit-dialog", PAlbumEditDialog); Vue.component("PAlbumEditDialog", PAlbumEditDialog);
Vue.component("p-album-delete-dialog", PAlbumDeleteDialog); Vue.component("PAlbumDeleteDialog", PAlbumDeleteDialog);
Vue.component("p-label-delete-dialog", PLabelDeleteDialog); Vue.component("PLabelDeleteDialog", PLabelDeleteDialog);
Vue.component("p-upload-dialog", PUploadDialog); Vue.component("PUploadDialog", PUploadDialog);
Vue.component("p-video-dialog", PVideoDialog); Vue.component("PVideoDialog", PVideoDialog);
Vue.component("p-share-dialog", PShareDialog); Vue.component("PShareDialog", PShareDialog);
Vue.component("p-share-upload-dialog", PShareUploadDialog); Vue.component("PShareUploadDialog", PShareUploadDialog);
Vue.component("p-webdav-dialog", PWebdavDialog); Vue.component("PWebdavDialog", PWebdavDialog);
Vue.component("p-reload-dialog", PReloadDialog); Vue.component("PReloadDialog", PReloadDialog);
}; };
export default dialogs; export default dialogs;

View File

@@ -30,67 +30,71 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Api from "common/api"; import Api from "common/api";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
import {config} from "../session"; import { config } from "../session";
export class Account extends RestModel { export class Account extends RestModel {
getDefaults() { getDefaults() {
return { return {
ID: 0, ID: 0,
AccName: "", AccName: "",
AccOwner: "", AccOwner: "",
AccURL: "", AccURL: "",
AccType: "", AccType: "",
AccKey: "", AccKey: "",
AccUser: "", AccUser: "",
AccPass: "", AccPass: "",
AccError: "", AccError: "",
AccErrors: 0, AccErrors: 0,
AccShare: true, AccShare: true,
AccSync: false, AccSync: false,
RetryLimit: 3, RetryLimit: 3,
SharePath: "/", SharePath: "/",
ShareSize: "", ShareSize: "",
ShareExpires: 0, ShareExpires: 0,
SyncPath: "/", SyncPath: "/",
SyncStatus: "", SyncStatus: "",
SyncInterval: 86400, SyncInterval: 86400,
SyncDate: null, SyncDate: null,
SyncFilenames: true, SyncFilenames: true,
SyncUpload: false, SyncUpload: false,
SyncDownload: !config.get("readonly"), SyncDownload: !config.get("readonly"),
SyncRaw: true, SyncRaw: true,
CreatedAt: "", CreatedAt: "",
UpdatedAt: "", UpdatedAt: "",
DeletedAt: null, DeletedAt: null,
}; };
} }
getEntityName() { getEntityName() {
return this.AccName; return this.AccName;
} }
getId() { getId() {
return this.ID; return this.ID;
} }
Folders() { Folders() {
return Api.get(this.getEntityResource() + "/folders").then((response) => Promise.resolve(response.data)); return Api.get(this.getEntityResource() + "/folders").then((response) =>
} Promise.resolve(response.data)
);
}
Share(photos, dest) { Share(photos, dest) {
const values = {Photos: photos, Destination: dest}; const values = { Photos: photos, Destination: dest };
return Api.post(this.getEntityResource() + "/share", values).then((response) => Promise.resolve(response.data)); return Api.post(this.getEntityResource() + "/share", values).then((response) =>
} Promise.resolve(response.data)
);
}
static getCollectionResource() { static getCollectionResource() {
return "accounts"; return "accounts";
} }
static getModelName() { static getModelName() {
return $gettext("Account"); return $gettext("Account");
} }
} }
export default Account; export default Account;

View File

@@ -30,136 +30,136 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Api from "common/api"; import Api from "common/api";
import {DateTime} from "luxon"; import { DateTime } from "luxon";
import {config} from "../session"; import { config } from "../session";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export class Album extends RestModel { export class Album extends RestModel {
getDefaults() { getDefaults() {
return { return {
UID: "", UID: "",
Cover: "", Cover: "",
Parent: "", Parent: "",
Folder: "", Folder: "",
Slug: "", Slug: "",
Type: "", Type: "",
Title: "", Title: "",
Location: "", Location: "",
Caption: "", Caption: "",
Category: "", Category: "",
Description: "", Description: "",
Notes: "", Notes: "",
Filter: "", Filter: "",
Order: "", Order: "",
Template: "", Template: "",
Country: "", Country: "",
Day: -1, Day: -1,
Year: -1, Year: -1,
Month: -1, Month: -1,
Favorite: true, Favorite: true,
Private: false, Private: false,
PhotoCount: 0, PhotoCount: 0,
LinkCount: 0, LinkCount: 0,
CreatedAt: "", CreatedAt: "",
UpdatedAt: "", UpdatedAt: "",
}; };
}
getEntityName() {
return this.Slug;
}
getTitle() {
return this.Title;
}
thumbnailUrl(size) {
return `/api/v1/albums/${this.getId()}/t/${config.previewToken()}/${size}`;
}
dayString() {
if (!this.Day || this.Day <= 0) {
return "01";
} }
getEntityName() { return this.Day.toString().padStart(2, "0");
return this.Slug; }
monthString() {
if (!this.Month || this.Month <= 0) {
return "01";
} }
getTitle() { return this.Month.toString().padStart(2, "0");
return this.Title; }
yearString() {
if (!this.Year || this.Year <= 1000) {
return new Date().getFullYear().toString().padStart(4, "0");
} }
thumbnailUrl(size) { return this.Year.toString();
return `/api/v1/albums/${this.getId()}/t/${config.previewToken()}/${size}`; }
getDate() {
let date = this.yearString() + "-" + this.monthString() + "-" + this.dayString();
return DateTime.fromISO(`${date}T12:00:00Z`).toUTC();
}
localDate(time) {
if (!this.TakenAtLocal) {
return this.utcDate();
} }
dayString() { let zone = this.getTimeZone();
if (!this.Day || this.Day <= 0) {
return "01";
}
return this.Day.toString().padStart(2, "0"); return DateTime.fromISO(this.localDateString(time), { zone });
}
getDateString() {
if (!this.Year || this.Year <= 1000) {
return $gettext("Unknown");
} else if (!this.Month || this.Month <= 0) {
return this.localYearString();
} else if (!this.Day || this.Day <= 0) {
return this.getDate().toLocaleString({ month: "long", year: "numeric" });
} }
monthString() { return this.localDate().toLocaleString(DateTime.DATE_HUGE);
if (!this.Month || this.Month <= 0) { }
return "01";
}
return this.Month.toString().padStart(2, "0"); getCreatedString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
} }
}
yearString() { like() {
if (!this.Year || this.Year <= 1000) { this.Favorite = true;
return new Date().getFullYear().toString().padStart(4, "0"); return Api.post(this.getEntityResource() + "/like");
} }
return this.Year.toString(); unlike() {
} this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
getDate() { static getCollectionResource() {
let date = this.yearString() + "-" + this.monthString() + "-" + this.dayString(); return "albums";
}
return DateTime.fromISO(`${date}T12:00:00Z`).toUTC(); static getModelName() {
} return $gettext("Album");
}
localDate(time) {
if(!this.TakenAtLocal) {
return this.utcDate();
}
let zone = this.getTimeZone();
return DateTime.fromISO(this.localDateString(time), {zone});
}
getDateString() {
if (!this.Year || this.Year <= 1000) {
return $gettext("Unknown");
} else if (!this.Month || this.Month <= 0) {
return this.localYearString();
} else if (!this.Day || this.Day <= 0) {
return this.getDate().toLocaleString({month: "long", year: "numeric"});
}
return this.localDate().toLocaleString(DateTime.DATE_HUGE);
}
getCreatedString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
}
}
like() {
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
static getCollectionResource() {
return "albums";
}
static getModelName() {
return $gettext("Album");
}
} }
export default Album; export default Album;

View File

@@ -30,208 +30,208 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Api from "common/api"; import Api from "common/api";
import {DateTime} from "luxon"; import { DateTime } from "luxon";
import Util from "common/util"; import Util from "common/util";
import {config} from "../session"; import { config } from "../session";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export class File extends RestModel { export class File extends RestModel {
getDefaults() { getDefaults() {
return { return {
UID: "", UID: "",
PhotoUID: "", PhotoUID: "",
InstanceID: "", InstanceID: "",
Root: "/", Root: "/",
Name: "", Name: "",
OriginalName: "", OriginalName: "",
Hash: "", Hash: "",
Size: 0, Size: 0,
ModTime: 0, ModTime: 0,
Codec: "", Codec: "",
Type: "", Type: "",
Mime: "", Mime: "",
Primary: false, Primary: false,
Sidecar: false, Sidecar: false,
Missing: false, Missing: false,
Portrait: false, Portrait: false,
Video: false, Video: false,
Duration: 0, Duration: 0,
Width: 0, Width: 0,
Height: 0, Height: 0,
Orientation: 0, Orientation: 0,
Projection: "", Projection: "",
AspectRatio: 1.0, AspectRatio: 1.0,
MainColor: "", MainColor: "",
Colors: "", Colors: "",
Luminance: "", Luminance: "",
Diff: 0, Diff: 0,
Chroma: 0, Chroma: 0,
Notes: "", Notes: "",
Error: "", Error: "",
CreatedAt: "", CreatedAt: "",
CreatedIn: 0, CreatedIn: 0,
UpdatedAt: "", UpdatedAt: "",
UpdatedIn: 0, UpdatedIn: 0,
DeletedAt: "", DeletedAt: "",
}; };
}
baseName(truncate) {
let result = this.Name;
const slash = result.lastIndexOf("/");
if (slash >= 0) {
result = this.Name.substring(slash + 1);
} }
baseName(truncate) { if (truncate) {
let result = this.Name; result = Util.truncate(result, truncate, "…");
const slash = result.lastIndexOf("/");
if (slash >= 0) {
result = this.Name.substring(slash + 1);
}
if (truncate) {
result = Util.truncate(result, truncate, "…");
}
return result;
} }
isFile() { return result;
return true; }
isFile() {
return true;
}
getEntityName() {
return this.Root + "/" + this.Name;
}
thumbnailUrl(size) {
if (this.Error) {
return "/api/v1/svg/broken";
} else if (this.Type === "raw") {
return "/api/v1/svg/raw";
} }
getEntityName() { return `/api/v1/t/${this.Hash}/${config.previewToken()}/${size}`;
return this.Root + "/" + this.Name; }
getDownloadUrl() {
return "/api/v1/dl/" + this.Hash + "?t=" + config.downloadToken();
}
download() {
if (!this.Hash) {
console.warn("no file hash found for download", this);
return;
} }
thumbnailUrl(size) { let link = document.createElement("a");
if (this.Error) { link.href = this.getDownloadUrl();
return "/api/v1/svg/broken"; link.download = this.baseName(this.Name);
} else if (this.Type === "raw") { link.click();
return "/api/v1/svg/raw"; }
}
return `/api/v1/t/${this.Hash}/${config.previewToken()}/${size}`; calculateSize(width, height) {
if (width >= this.Width && height >= this.Height) {
// Smaller
return { width: this.Width, height: this.Height };
} }
getDownloadUrl() { const srcAspectRatio = this.Width / this.Height;
return "/api/v1/dl/" + this.Hash + "?t=" + config.downloadToken(); const maxAspectRatio = width / height;
let newW, newH;
if (srcAspectRatio > maxAspectRatio) {
newW = width;
newH = Math.round(newW / srcAspectRatio);
} else {
newH = height;
newW = Math.round(newH * srcAspectRatio);
} }
download() { return { width: newW, height: newH };
if (!this.Hash) { }
console.warn("no file hash found for download", this);
return;
}
let link = document.createElement("a"); getDateString() {
link.href = this.getDownloadUrl(); return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
link.download = this.baseName(this.Name); }
link.click();
getInfo() {
let info = [];
if (this.Type) {
info.push(this.Type.toUpperCase());
} }
calculateSize(width, height) { if (this.Duration > 0) {
if (width >= this.Width && height >= this.Height) { // Smaller info.push(Util.duration(this.Duration));
return {width: this.Width, height: this.Height};
}
const srcAspectRatio = this.Width / this.Height;
const maxAspectRatio = width / height;
let newW, newH;
if (srcAspectRatio > maxAspectRatio) {
newW = width;
newH = Math.round(newW / srcAspectRatio);
} else {
newH = height;
newW = Math.round(newH * srcAspectRatio);
}
return {width: newW, height: newH};
} }
getDateString() { this.addSizeInfo(info);
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
return info.join(", ");
}
typeInfo() {
if (this.Video) {
return $gettext("Video");
} else if (this.Sidecar) {
return $gettext("Sidecar");
} }
getInfo() { return this.Type.toUpperCase();
let info = []; }
if (this.Type) { sizeInfo() {
info.push(this.Type.toUpperCase()); let info = [];
}
if (this.Duration > 0) { this.addSizeInfo(info);
info.push(Util.duration(this.Duration));
}
this.addSizeInfo(info); return info.join(", ");
}
return info.join(", "); addSizeInfo(info) {
if (this.Width && this.Height) {
info.push(this.Width + " × " + this.Height);
} }
typeInfo() { if (this.Size > 102400) {
if (this.Video) { const size = Number.parseFloat(this.Size) / 1048576;
return $gettext("Video");
} else if (this.Sidecar) {
return $gettext("Sidecar");
}
return this.Type.toUpperCase(); info.push(size.toFixed(1) + " MB");
} else if (this.Size) {
const size = Number.parseFloat(this.Size) / 1024;
info.push(size.toFixed(1) + " KB");
} }
}
sizeInfo() { toggleLike() {
let info = []; this.Favorite = !this.Favorite;
this.addSizeInfo(info); if (this.Favorite) {
return Api.post(this.getPhotoResource() + "/like");
return info.join(", "); } else {
return Api.delete(this.getPhotoResource() + "/like");
} }
}
addSizeInfo(info) { getPhotoResource() {
if (this.Width && this.Height) { return "photos/" + this.PhotoUID;
info.push(this.Width + " × " + this.Height); }
}
if (this.Size > 102400) { like() {
const size = Number.parseFloat(this.Size) / 1048576; this.Favorite = true;
return Api.post(this.getPhotoResource() + "/like");
}
info.push(size.toFixed(1) + " MB"); unlike() {
} else if (this.Size) { this.Favorite = false;
const size = Number.parseFloat(this.Size) / 1024; return Api.delete(this.getPhotoResource() + "/like");
}
info.push(size.toFixed(1) + " KB"); static getCollectionResource() {
} return "files";
} }
toggleLike() { static getModelName() {
this.Favorite = !this.Favorite; return $gettext("File");
}
if (this.Favorite) {
return Api.post(this.getPhotoResource() + "/like");
} else {
return Api.delete(this.getPhotoResource() + "/like");
}
}
getPhotoResource() {
return "photos/" + this.PhotoUID;
}
like() {
this.Favorite = true;
return Api.post(this.getPhotoResource() + "/like");
}
unlike() {
this.Favorite = false;
return Api.delete(this.getPhotoResource() + "/like");
}
static getCollectionResource() {
return "files";
}
static getModelName() {
return $gettext("File");
}
} }
export default File; export default File;

View File

@@ -30,162 +30,162 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Api from "common/api"; import Api from "common/api";
import {DateTime} from "luxon"; import { DateTime } from "luxon";
import File from "model/file"; import File from "model/file";
import Util from "common/util"; import Util from "common/util";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export const RootImport = "import"; export const RootImport = "import";
export const RootOriginals = "originals"; export const RootOriginals = "originals";
export class Folder extends RestModel { export class Folder extends RestModel {
getDefaults() { getDefaults() {
return { return {
Folder: true, Folder: true,
Path: "", Path: "",
Root: "", Root: "",
UID: "", UID: "",
Type: "", Type: "",
Title: "", Title: "",
Category: "", Category: "",
Description: "", Description: "",
Order: "", Order: "",
Country: "", Country: "",
Year: "", Year: "",
Month: "", Month: "",
Favorite: false, Favorite: false,
Private: false, Private: false,
Ignore: false, Ignore: false,
Watch: false, Watch: false,
FileCount: 0, FileCount: 0,
CreatedAt: "", CreatedAt: "",
UpdatedAt: "", UpdatedAt: "",
}; };
}
baseName(truncate) {
let result = this.Path;
const slash = result.lastIndexOf("/");
if (slash >= 0) {
result = this.Path.substring(slash + 1);
} }
baseName(truncate) { if (truncate) {
let result = this.Path; result = Util.truncate(result, truncate, "…");
const slash = result.lastIndexOf("/"); }
if (slash >= 0) { return result;
result = this.Path.substring(slash + 1); }
isFile() {
return false;
}
getEntityName() {
return this.Root + "/" + this.Path;
}
thumbnailUrl() {
return "/api/v1/svg/folder";
}
getDateString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
}
}
like() {
this.Favorite = true;
return Api.post(this.getEntityResource() + "/like");
}
unlike() {
this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like");
}
static findAll(path) {
return this.search(path, { recursive: true });
}
static findAllUncached(path) {
return this.search(path, { recursive: true, uncached: true });
}
static originals(path, params) {
if (!path || path[0] !== "/") {
path = "/" + path;
}
return this.search(RootOriginals + path, params);
}
static search(path, params) {
const options = {
params: params,
};
if (!path || path[0] !== "/") {
path = "/" + path;
}
return Api.get(this.getCollectionResource() + path, options).then((response) => {
let folders = response.data.folders;
let files = response.data.files ? response.data.files : [];
let count = folders.length + files.length;
let limit = 0;
let offset = 0;
if (response.headers) {
if (response.headers["x-count"]) {
count = parseInt(response.headers["x-count"]);
} }
if(truncate) { if (response.headers["x-limit"]) {
result = Util.truncate(result, truncate, "…"); limit = parseInt(response.headers["x-limit"]);
} }
return result; if (response.headers["x-offset"]) {
} offset = parseInt(response.headers["x-offset"]);
isFile() {
return false;
}
getEntityName() {
return this.Root + "/" + this.Path;
}
thumbnailUrl() {
return "/api/v1/svg/folder";
}
getDateString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
}
toggleLike() {
this.Favorite = !this.Favorite;
if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like");
} else {
return Api.delete(this.getEntityResource() + "/like");
} }
} }
like() { response.models = [];
this.Favorite = true; response.files = files.length;
return Api.post(this.getEntityResource() + "/like"); response.folders = folders.length;
} response.count = count;
response.limit = limit;
response.offset = offset;
unlike() { for (let i = 0; i < folders.length; i++) {
this.Favorite = false; response.models.push(new this(folders[i]));
return Api.delete(this.getEntityResource() + "/like"); }
}
static findAll(path) { for (let i = 0; i < files.length; i++) {
return this.search(path, {recursive: true}); response.models.push(new File(files[i]));
} }
static findAllUncached(path) { return Promise.resolve(response);
return this.search(path, {recursive: true, uncached: true}); });
} }
static originals(path, params) { static getCollectionResource() {
if(!path || path[0] !== "/") { return "folders";
path = "/" + path; }
}
return this.search(RootOriginals + path, params);
}
static search(path, params) { static getModelName() {
const options = { return $gettext("Folder");
params: params, }
};
if (!path || path[0] !== "/") {
path = "/" + path;
}
return Api.get(this.getCollectionResource() + path, options).then((response) => {
let folders = response.data.folders;
let files = response.data.files ? response.data.files : [];
let count = folders.length + files.length;
let limit = 0;
let offset = 0;
if (response.headers) {
if (response.headers["x-count"]) {
count = parseInt(response.headers["x-count"]);
}
if (response.headers["x-limit"]) {
limit = parseInt(response.headers["x-limit"]);
}
if (response.headers["x-offset"]) {
offset = parseInt(response.headers["x-offset"]);
}
}
response.models = [];
response.files = files.length;
response.folders = folders.length;
response.count = count;
response.limit = limit;
response.offset = offset;
for (let i = 0; i < folders.length; i++) {
response.models.push(new this(folders[i]));
}
for (let i = 0; i < files.length; i++) {
response.models.push(new File(files[i]));
}
return Promise.resolve(response);
});
}
static getCollectionResource() {
return "folders";
}
static getModelName() {
return $gettext("Folder");
}
} }
export default Folder; export default Folder;

View File

@@ -30,72 +30,72 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Api from "common/api"; import Api from "common/api";
import {DateTime} from "luxon"; import { DateTime } from "luxon";
import {config} from "../session"; import { config } from "../session";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export class Label extends RestModel { export class Label extends RestModel {
getDefaults() { getDefaults() {
return { return {
ID: 0, ID: 0,
UID: "", UID: "",
Slug: "", Slug: "",
CustomSlug: "", CustomSlug: "",
Name: "", Name: "",
Priority: 0, Priority: 0,
Favorite: false, Favorite: false,
Description: "", Description: "",
Notes: "", Notes: "",
PhotoCount: 0, PhotoCount: 0,
CreatedAt: "", CreatedAt: "",
UpdatedAt: "", UpdatedAt: "",
DeletedAt: "", DeletedAt: "",
}; };
} }
getEntityName() { getEntityName() {
return this.Slug; return this.Slug;
} }
getTitle() { getTitle() {
return this.Name; return this.Name;
} }
thumbnailUrl(size) { thumbnailUrl(size) {
return `/api/v1/labels/${this.getId()}/t/${config.previewToken()}/${size}`; return `/api/v1/labels/${this.getId()}/t/${config.previewToken()}/${size}`;
} }
getDateString() { getDateString() {
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED); return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
} }
toggleLike() { toggleLike() {
this.Favorite = !this.Favorite; this.Favorite = !this.Favorite;
if (this.Favorite) { if (this.Favorite) {
return Api.post(this.getEntityResource() + "/like"); return Api.post(this.getEntityResource() + "/like");
} else { } else {
return Api.delete(this.getEntityResource() + "/like"); return Api.delete(this.getEntityResource() + "/like");
}
} }
}
like() { like() {
this.Favorite = true; this.Favorite = true;
return Api.post(this.getEntityResource() + "/like"); return Api.post(this.getEntityResource() + "/like");
} }
unlike() { unlike() {
this.Favorite = false; this.Favorite = false;
return Api.delete(this.getEntityResource() + "/like"); return Api.delete(this.getEntityResource() + "/like");
} }
static getCollectionResource() { static getCollectionResource() {
return "labels"; return "labels";
} }
static getModelName() { static getModelName() {
return $gettext("Label"); return $gettext("Label");
} }
} }
export default Label; export default Label;

View File

@@ -29,79 +29,81 @@ https://docs.photoprism.org/developer-guide/
*/ */
import Model from "./model"; import Model from "./model";
import {DateTime} from "luxon"; import { DateTime } from "luxon";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export default class Link extends Model { export default class Link extends Model {
getDefaults() { getDefaults() {
return { return {
UID: "", UID: "",
Share: "", Share: "",
Slug: "", Slug: "",
Token: "", Token: "",
Expires: 0, Expires: 0,
Views: 0, Views: 0,
MaxViews: 0, MaxViews: 0,
Password: "", Password: "",
HasPassword: false, HasPassword: false,
CanComment: false, CanComment: false,
CanEdit: false, CanEdit: false,
CreatedAt: "", CreatedAt: "",
ModifiedAt: "", ModifiedAt: "",
}; };
}
getToken() {
return this.Token.toLowerCase().trim();
}
url() {
let token = this.getToken();
if (!token) {
token = "…";
} }
getToken() { if (this.hasSlug()) {
return this.Token.toLowerCase().trim(); return `${window.location.origin}/s/${token}/${this.Slug}`;
} }
url() { return `${window.location.origin}/s/${token}/${this.Share}`;
let token = this.getToken(); }
if(!token) { caption() {
token = "…"; return `/s/${this.getToken()}`;
} }
if(this.hasSlug()) { getId() {
return `${window.location.origin}/s/${token}/${this.Slug}`; return this.UID;
} }
return `${window.location.origin}/s/${token}/${this.Share}`; hasId() {
} return !!this.getId();
}
caption() { getSlug() {
return `/s/${this.getToken()}`; return this.Slug ? this.Slug : "";
} }
getId() { hasSlug() {
return this.UID; return !!this.getSlug();
} }
hasId() { clone() {
return !!this.getId(); return new this.constructor(this.getValues());
} }
getSlug() { expires() {
return this.Slug ? this.Slug : ""; return DateTime.fromISO(this.UpdatedAt)
} .plus({ seconds: this.Expires })
.toLocaleString(DateTime.DATE_SHORT);
}
hasSlug() { static getCollectionResource() {
return !!this.getSlug(); return "links";
} }
clone() { static getModelName() {
return new this.constructor(this.getValues()); return $gettext("Link");
} }
expires() {
return DateTime.fromISO(this.UpdatedAt).plus({ seconds: this.Expires }).toLocaleString(DateTime.DATE_SHORT);
}
static getCollectionResource() {
return "links";
}
static getModelName() {
return $gettext("Link");
}
} }

View File

@@ -29,87 +29,86 @@ https://docs.photoprism.org/developer-guide/
*/ */
export class Model { export class Model {
constructor(values) { constructor(values) {
this.__originalValues = {}; this.__originalValues = {};
if (values) { if (values) {
this.setValues(values); this.setValues(values);
} else {
this.setValues(this.getDefaults());
}
}
setValues(values, scalarOnly) {
if (!values) return;
for (let key in values) {
if (values.hasOwnProperty(key) && key !== "__originalValues") {
this[key] = values[key];
if (typeof values[key] !== "object") {
this.__originalValues[key] = values[key];
} else if (!scalarOnly) {
this.__originalValues[key] = JSON.parse(JSON.stringify(values[key]));
}
}
}
return this;
}
getValues(changed) {
const result = {};
const defaults = this.getDefaults();
for (let key in this.__originalValues) {
if (this.__originalValues.hasOwnProperty(key) && key !== "__originalValues") {
let val;
if (defaults.hasOwnProperty(key)) {
switch (typeof defaults[key]) {
case "string":
if (this[key] === null || this[key] === undefined) {
val = "";
} else {
val = this[key];
}
break;
case "bigint":
case "number":
val = parseFloat(this[key]);
break;
case "boolean":
val = !!this[key];
break;
default:
val = this[key];
}
} else { } else {
this.setValues(this.getDefaults()); val = this[key];
}
}
setValues(values, scalarOnly) {
if (!values) return;
for (let key in values) {
if (values.hasOwnProperty(key) && key !== "__originalValues") {
this[key] = values[key];
if (typeof values[key] !== "object") {
this.__originalValues[key] = values[key];
} else if (!scalarOnly) {
this.__originalValues[key] = JSON.parse(JSON.stringify(values[key]));
}
}
} }
return this; if (!changed || JSON.stringify(val) !== JSON.stringify(this.__originalValues[key])) {
} result[key] = val;
getValues(changed) {
const result = {};
const defaults = this.getDefaults();
for (let key in this.__originalValues) {
if (this.__originalValues.hasOwnProperty(key) && key !== "__originalValues") {
let val;
if (defaults.hasOwnProperty(key)) {
switch (typeof defaults[key]) {
case "string":
if(this[key] === null || this[key] === undefined) {
val = "";
} else {
val = this[key];
}
break;
case "bigint":
case "number":
val = parseFloat(this[key]);
break;
case "boolean":
val = !!this[key];
break;
default:
val = this[key];
}
} else {
val = this[key];
}
if (!changed || JSON.stringify(val) !== JSON.stringify(this.__originalValues[key])) {
result[key] = val;
}
}
} }
}
return result;
} }
wasChanged() { return result;
const changed = this.getValues(true); }
if(!changed) { wasChanged() {
return false; const changed = this.getValues(true);
}
return !(changed.constructor === Object && Object.keys(changed).length === 0); if (!changed) {
return false;
} }
getDefaults() { return !(changed.constructor === Object && Object.keys(changed).length === 0);
return {}; }
}
getDefaults() {
return {};
}
} }
export default Model; export default Model;

File diff suppressed because it is too large Load Diff

View File

@@ -32,173 +32,183 @@ import Api from "common/api";
import Form from "common/form"; import Form from "common/form";
import Model from "./model"; import Model from "./model";
import Link from "./link"; import Link from "./link";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export class Rest extends Model { export class Rest extends Model {
getId() { getId() {
return this.UID ? this.UID : this.ID; return this.UID ? this.UID : this.ID;
}
hasId() {
return !!this.getId();
}
getSlug() {
return this.Slug ? this.Slug : "";
}
clone() {
return new this.constructor(this.getValues());
}
find(id, params) {
return Api.get(this.getEntityResource(id), params).then((resp) =>
Promise.resolve(new this.constructor(resp.data))
);
}
save() {
if (this.hasId()) {
return this.update();
} }
hasId() { return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((resp) =>
return !!this.getId(); Promise.resolve(this.setValues(resp.data))
);
}
update() {
return Api.put(this.getEntityResource(), this.getValues(true)).then((resp) =>
Promise.resolve(this.setValues(resp.data))
);
}
remove() {
return Api.delete(this.getEntityResource()).then(() => Promise.resolve(this));
}
getEditForm() {
return Api.options(this.getEntityResource()).then((resp) =>
Promise.resolve(new Form(resp.data))
);
}
getEntityResource(id) {
if (!id) {
id = this.getId();
} }
getSlug() { return this.constructor.getCollectionResource() + "/" + id;
return this.Slug ? this.Slug : ""; }
getEntityName() {
return this.constructor.getModelName() + " " + this.getId();
}
createLink(password, expires) {
return Api.post(this.getEntityResource() + "/links", {
Password: password ? password : "",
Expires: expires ? expires : 0,
Slug: this.getSlug(),
CanEdit: false,
CanComment: false,
}).then((resp) => Promise.resolve(new Link(resp.data)));
}
updateLink(link) {
let values = link.getValues(false);
if (link.Token) {
values["Token"] = link.getToken();
} }
clone() { if (link.Password) {
return new this.constructor(this.getValues()); values["Password"] = link.Password;
} }
find(id, params) { return Api.put(this.getEntityResource() + "/links/" + link.getId(), values).then((resp) =>
return Api.get(this.getEntityResource(id), params).then((resp) => Promise.resolve(new this.constructor(resp.data))); Promise.resolve(link.setValues(resp.data))
} );
}
save() { removeLink(link) {
if (this.hasId()) { return Api.delete(this.getEntityResource() + "/links/" + link.getId()).then((resp) =>
return this.update(); Promise.resolve(link.setValues(resp.data))
);
}
links() {
return Api.get(this.getEntityResource() + "/links").then((resp) => {
resp.models = [];
resp.count = resp.data.length;
for (let i = 0; i < resp.data.length; i++) {
resp.models.push(new Link(resp.data[i]));
}
return Promise.resolve(resp);
});
}
modelName() {
return this.constructor.getModelName();
}
static getCollectionResource() {
// Needs to be implemented!
return "";
}
static getCreateResource() {
return this.getCollectionResource();
}
static getCreateForm() {
return Api.options(this.getCreateResource()).then((resp) =>
Promise.resolve(new Form(resp.data))
);
}
static getModelName() {
return $gettext("Item");
}
static getSearchForm() {
return Api.options(this.getCollectionResource()).then((resp) =>
Promise.resolve(new Form(resp.data))
);
}
static limit() {
return 3333;
}
static search(params) {
const options = {
params: params,
};
return Api.get(this.getCollectionResource(), options).then((resp) => {
let count = resp.data.length;
let limit = 0;
let offset = 0;
if (resp.headers) {
if (resp.headers["x-count"]) {
count = parseInt(resp.headers["x-count"]);
} }
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((resp) => Promise.resolve(this.setValues(resp.data))); if (resp.headers["x-limit"]) {
} limit = parseInt(resp.headers["x-limit"]);
update() {
return Api.put(this.getEntityResource(), this.getValues(true)).then((resp) => Promise.resolve(this.setValues(resp.data)));
}
remove() {
return Api.delete(this.getEntityResource()).then(() => Promise.resolve(this));
}
getEditForm() {
return Api.options(this.getEntityResource()).then(resp => Promise.resolve(new Form(resp.data)));
}
getEntityResource(id) {
if (!id) {
id = this.getId();
} }
return this.constructor.getCollectionResource() + "/" + id; if (resp.headers["x-offset"]) {
} offset = parseInt(resp.headers["x-offset"]);
getEntityName() {
return this.constructor.getModelName() + " " + this.getId();
}
createLink(password, expires) {
return Api
.post(this.getEntityResource() + "/links", {
"Password": password ? password : "",
"Expires": expires ? expires : 0,
"Slug": this.getSlug(),
"CanEdit": false,
"CanComment": false,
})
.then((resp) => Promise.resolve(new Link(resp.data)));
}
updateLink(link) {
let values = link.getValues(false);
if(link.Token) {
values["Token"] = link.getToken();
} }
}
if(link.Password) { resp.models = [];
values["Password"] = link.Password; resp.count = count;
} resp.limit = limit;
resp.offset = offset;
return Api for (let i = 0; i < resp.data.length; i++) {
.put(this.getEntityResource() + "/links/" + link.getId(), values) resp.models.push(new this(resp.data[i]));
.then((resp) => Promise.resolve(link.setValues(resp.data))); }
}
removeLink(link) { return Promise.resolve(resp);
return Api });
.delete(this.getEntityResource() + "/links/" + link.getId()) }
.then((resp) => Promise.resolve(link.setValues(resp.data)));
}
links() {
return Api.get(this.getEntityResource() + "/links").then((resp) => {
resp.models = [];
resp.count = resp.data.length;
for (let i = 0; i < resp.data.length; i++) {
resp.models.push(new Link(resp.data[i]));
}
return Promise.resolve(resp);
});
}
modelName() {
return this.constructor.getModelName();
}
static getCollectionResource() {
// Needs to be implemented!
return "";
}
static getCreateResource() {
return this.getCollectionResource();
}
static getCreateForm() {
return Api.options(this.getCreateResource()).then(resp => Promise.resolve(new Form(resp.data)));
}
static getModelName() {
return $gettext("Item");
}
static getSearchForm() {
return Api.options(this.getCollectionResource()).then(resp => Promise.resolve(new Form(resp.data)));
}
static limit() {
return 3333;
}
static search(params) {
const options = {
params: params,
};
return Api.get(this.getCollectionResource(), options).then((resp) => {
let count = resp.data.length;
let limit = 0;
let offset = 0;
if (resp.headers) {
if (resp.headers["x-count"]) {
count = parseInt(resp.headers["x-count"]);
}
if (resp.headers["x-limit"]) {
limit = parseInt(resp.headers["x-limit"]);
}
if (resp.headers["x-offset"]) {
offset = parseInt(resp.headers["x-offset"]);
}
}
resp.models = [];
resp.count = count;
resp.limit = limit;
resp.offset = offset;
for (let i = 0; i < resp.data.length; i++) {
resp.models.push(new this(resp.data[i]));
}
return Promise.resolve(resp);
});
}
} }
export default Rest; export default Rest;

View File

@@ -32,23 +32,25 @@ import Api from "common/api";
import Model from "./model"; import Model from "./model";
export class Settings extends Model { export class Settings extends Model {
changed(area, key) { changed(area, key) {
if (typeof this.__originalValues[area] === "undefined") { if (typeof this.__originalValues[area] === "undefined") {
return false; return false;
}
return (this[area][key] !== this.__originalValues[area][key]);
} }
load() { return this[area][key] !== this.__originalValues[area][key];
return Api.get("settings").then((response) => { }
return Promise.resolve(this.setValues(response.data));
});
}
save() { load() {
return Api.post("settings", this.getValues(true)).then((response) => Promise.resolve(this.setValues(response.data))); return Api.get("settings").then((response) => {
} return Promise.resolve(this.setValues(response.data));
});
}
save() {
return Api.post("settings", this.getValues(true)).then((response) =>
Promise.resolve(this.setValues(response.data))
);
}
} }
export default Settings; export default Settings;

View File

@@ -30,219 +30,218 @@ https://docs.photoprism.org/developer-guide/
import Model from "./model"; import Model from "./model";
import Api from "common/api"; import Api from "common/api";
import {config} from "../session"; import { config } from "../session";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
const thumbs = window.__CONFIG__.thumbs; const thumbs = window.__CONFIG__.thumbs;
export class Thumb extends Model { export class Thumb extends Model {
getDefaults() { getDefaults() {
return { return {
uid: "", uid: "",
title: "", title: "",
taken: "", taken: "",
description: "", description: "",
favorite: false, favorite: false,
playable: false, playable: false,
original_w: 0, original_w: 0,
original_h: 0, original_h: 0,
download_url: "", download_url: "",
}; };
}
getId() {
return this.uid;
}
hasId() {
return !!this.getId();
}
toggleLike() {
this.favorite = !this.favorite;
if (this.favorite) {
return Api.post("photos/" + this.uid + "/like");
} else {
return Api.delete("photos/" + this.uid + "/like");
}
}
static thumbNotFound() {
const result = {
uid: "",
title: $gettext("Not Found"),
taken: "",
description: "",
favorite: false,
playable: false,
original_w: 0,
original_h: 0,
download_url: "",
};
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
result[t.size] = {
src: "/api/v1/svg/photo",
w: t.w,
h: t.h,
};
} }
getId() { return result;
return this.uid; }
static fromPhotos(photos) {
let result = [];
const n = photos.length;
for (let i = 0; i < n; i++) {
result.push(this.fromPhoto(photos[i]));
} }
hasId() { return result;
return !!this.getId(); }
static fromPhoto(photo) {
if (photo.Files) {
return this.fromFile(photo, photo.mainFile());
} }
toggleLike() { if (!photo || !photo.Hash) {
this.favorite = !this.favorite; return this.thumbNotFound();
if (this.favorite) {
return Api.post("photos/" + this.uid + "/like");
} else {
return Api.delete("photos/" + this.uid + "/like");
}
} }
static thumbNotFound() { const result = {
const result = { uid: photo.UID,
uid: "", title: photo.Title,
title: $gettext("Not Found"), taken: photo.getDateString(),
taken: "", description: photo.Description,
description: "", favorite: photo.Favorite,
favorite: false, playable: photo.isPlayable(),
playable: false, download_url: this.downloadUrl(photo),
original_w: 0, original_w: photo.Width,
original_h: 0, original_h: photo.Height,
download_url: "", };
};
for (let i = 0; i < thumbs.length; i++) { for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i]; let t = thumbs[i];
let size = photo.calculateSize(t.w, t.h);
result[t.size] = { result[t.size] = {
src: "/api/v1/svg/photo", src: photo.thumbnailUrl(t.size),
w: t.w, w: size.width,
h: t.h, h: size.height,
}; };
}
return result;
} }
static fromPhotos(photos) { return new this(result);
let result = []; }
const n = photos.length;
for (let i = 0; i < n; i++) { static fromFile(photo, file) {
result.push(this.fromPhoto(photos[i])); if (!photo || !file || !file.Hash) {
} return this.thumbNotFound();
return result;
} }
static fromPhoto(photo) { const result = {
if (photo.Files) { uid: photo.UID,
return this.fromFile(photo, photo.mainFile()); title: photo.Title,
} taken: photo.getDateString(),
description: photo.Description,
favorite: photo.Favorite,
playable: photo.isPlayable(),
download_url: this.downloadUrl(file),
original_w: file.Width,
original_h: file.Height,
};
if (!photo || !photo.Hash) { for (let i = 0; i < thumbs.length; i++) {
return this.thumbNotFound(); let t = thumbs[i];
} let size = this.calculateSize(file, t.w, t.h);
const result = { result[t.size] = {
uid: photo.UID, src: this.thumbnailUrl(file, t.size),
title: photo.Title, w: size.width,
taken: photo.getDateString(), h: size.height,
description: photo.Description, };
favorite: photo.Favorite,
playable: photo.isPlayable(),
download_url: this.downloadUrl(photo),
original_w: photo.Width,
original_h: photo.Height,
};
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
let size = photo.calculateSize(t.w, t.h);
result[t.size] = {
src: photo.thumbnailUrl(t.size),
w: size.width,
h: size.height,
};
}
return new this(result);
} }
static fromFile(photo, file) { return new this(result);
if (!photo || !file || !file.Hash) { }
return this.thumbNotFound();
}
const result = { static fromFiles(photos) {
uid: photo.UID, let result = [];
title: photo.Title,
taken: photo.getDateString(),
description: photo.Description,
favorite: photo.Favorite,
playable: photo.isPlayable(),
download_url: this.downloadUrl(file),
original_w: file.Width,
original_h: file.Height,
};
for (let i = 0; i < thumbs.length; i++) { if (!photos || !photos.length) {
let t = thumbs[i]; return result;
let size = this.calculateSize(file, t.w, t.h);
result[t.size] = {
src: this.thumbnailUrl(file, t.size),
w: size.width,
h: size.height,
};
}
return new this(result);
} }
static fromFiles(photos) { const n = photos.length;
let result = [];
if (!photos || !photos.length) { for (let i = 0; i < n; i++) {
return result; let p = photos[i];
if (!p.Files || !p.Files.length) {
continue;
}
for (let j = 0; j < p.Files.length; j++) {
let f = p.Files[j];
if (!f || f.Type !== "jpg") {
continue;
} }
const n = photos.length; let thumb = this.fromFile(p, f);
for (let i = 0; i < n; i++) { if (thumb) {
let p = photos[i]; result.push(thumb);
if (!p.Files || !p.Files.length) {
continue;
}
for (let j = 0; j < p.Files.length; j++) {
let f = p.Files[j];
if (!f || f.Type !== "jpg") {
continue;
}
let thumb = this.fromFile(p, f);
if (thumb) {
result.push(thumb);
}
}
} }
}
return result;
} }
static calculateSize(file, width, height) { return result;
if (width >= file.Width && height >= file.Height) { // Smaller }
return {width: file.Width, height: file.Height};
}
const srcAspectRatio = file.Width / file.Height; static calculateSize(file, width, height) {
const maxAspectRatio = width / height; if (width >= file.Width && height >= file.Height) {
// Smaller
let newW, newH; return { width: file.Width, height: file.Height };
if (srcAspectRatio > maxAspectRatio) {
newW = width;
newH = Math.round(newW / srcAspectRatio);
} else {
newH = height;
newW = Math.round(newH * srcAspectRatio);
}
return {width: newW, height: newH};
} }
static thumbnailUrl(file, size) { const srcAspectRatio = file.Width / file.Height;
if (!file.Hash) { const maxAspectRatio = width / height;
return "/api/v1/svg/photo";
} let newW, newH;
return `/api/v1/t/${file.Hash}/${config.previewToken()}/${size}`; if (srcAspectRatio > maxAspectRatio) {
newW = width;
newH = Math.round(newW / srcAspectRatio);
} else {
newH = height;
newW = Math.round(newH * srcAspectRatio);
} }
static downloadUrl(file) { return { width: newW, height: newH };
if (!file || !file.Hash) { }
return "";
}
return `/api/v1/dl/${file.Hash}?t=${config.downloadToken()}`; static thumbnailUrl(file, size) {
if (!file.Hash) {
return "/api/v1/svg/photo";
} }
return `/api/v1/t/${file.Hash}/${config.previewToken()}/${size}`;
}
static downloadUrl(file) {
if (!file || !file.Hash) {
return "";
}
return `/api/v1/dl/${file.Hash}?t=${config.downloadToken()}`;
}
} }
export default Thumb; export default Thumb;

View File

@@ -31,90 +31,96 @@ https://docs.photoprism.org/developer-guide/
import RestModel from "model/rest"; import RestModel from "model/rest";
import Form from "common/form"; import Form from "common/form";
import Api from "common/api"; import Api from "common/api";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
export class User extends RestModel { export class User extends RestModel {
getDefaults() { getDefaults() {
return { return {
UID: "", UID: "",
Address: {}, Address: {},
MotherUID: "", MotherUID: "",
FatherUID: "", FatherUID: "",
GlobalUID: "", GlobalUID: "",
FullName: "", FullName: "",
NickName: "", NickName: "",
MaidenName: "", MaidenName: "",
ArtistName: "", ArtistName: "",
UserName: "", UserName: "",
UserStatus: "", UserStatus: "",
UserDisabled: false, UserDisabled: false,
UserSettings: "", UserSettings: "",
PrimaryEmail: "", PrimaryEmail: "",
EmailConfirmed: false, EmailConfirmed: false,
BackupEmail: "", BackupEmail: "",
PersonURL: "", PersonURL: "",
PersonPhone: "", PersonPhone: "",
PersonStatus: "", PersonStatus: "",
PersonAvatar: "", PersonAvatar: "",
PersonLocation: "", PersonLocation: "",
PersonBio: "", PersonBio: "",
BusinessURL: "", BusinessURL: "",
BusinessPhone: "", BusinessPhone: "",
BusinessEmail: "", BusinessEmail: "",
CompanyName: "", CompanyName: "",
DepartmentName: "", DepartmentName: "",
JobTitle: "", JobTitle: "",
BirthYear: -1, BirthYear: -1,
BirthMonth: -1, BirthMonth: -1,
BirthDay: -1, BirthDay: -1,
TermsAccepted: false, TermsAccepted: false,
IsArtist: false, IsArtist: false,
IsSubject: false, IsSubject: false,
RoleAdmin: false, RoleAdmin: false,
RoleGuest: false, RoleGuest: false,
RoleChild: false, RoleChild: false,
RoleFamily: false, RoleFamily: false,
RoleFriend: false, RoleFriend: false,
WebDAV: false, WebDAV: false,
StoragePath: "", StoragePath: "",
CanInvite: false, CanInvite: false,
InviteToken: "", InviteToken: "",
InvitedBy: "", InvitedBy: "",
CreatedAt: "", CreatedAt: "",
UpdatedAt: "", UpdatedAt: "",
}; };
} }
getEntityName() { getEntityName() {
return this.FullName ? this.FullName : this.UserName; return this.FullName ? this.FullName : this.UserName;
} }
getRegisterForm() { getRegisterForm() {
return Api.options(this.getEntityResource() + "/register").then(response => Promise.resolve(new Form(response.data))); return Api.options(this.getEntityResource() + "/register").then((response) =>
} Promise.resolve(new Form(response.data))
);
}
getProfileForm() { getProfileForm() {
return Api.options(this.getEntityResource() + "/profile").then(response => Promise.resolve(new Form(response.data))); return Api.options(this.getEntityResource() + "/profile").then((response) =>
} Promise.resolve(new Form(response.data))
);
}
changePassword(oldPassword, newPassword) { changePassword(oldPassword, newPassword) {
return Api.put(this.getEntityResource() + "/password", { return Api.put(this.getEntityResource() + "/password", {
old: oldPassword, old: oldPassword,
new: newPassword, new: newPassword,
}).then((response) => Promise.resolve(response.data)); }).then((response) => Promise.resolve(response.data));
} }
saveProfile() { saveProfile() {
return Api.post(this.getEntityResource() + "/profile", this.getValues()).then((response) => Promise.resolve(this.setValues(response.data))); return Api.post(this.getEntityResource() + "/profile", this.getValues()).then((response) =>
} Promise.resolve(this.setValues(response.data))
);
}
static getCollectionResource() { static getCollectionResource() {
return "users"; return "users";
} }
static getModelName() { static getModelName() {
return $gettext("User"); return $gettext("User");
} }
} }
export default User; export default User;

View File

@@ -1,276 +1,279 @@
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
import moment from "moment-timezone"; import moment from "moment-timezone";
import {Info} from "luxon"; import { Info } from "luxon";
import {config} from "../session"; import { config } from "../session";
import {TypeVideo,TypeImage,TypeLive,TypeRaw} from "../model/photo"; import { TypeVideo, TypeImage, TypeLive, TypeRaw } from "../model/photo";
export const TimeZones = () => moment.tz.names(); export const TimeZones = () => moment.tz.names();
export const Days = () => { export const Days = () => {
let result = []; let result = [];
for (let i = 1; i <= 31; i++) { for (let i = 1; i <= 31; i++) {
result.push({"value": i, "text": i.toString().padStart(2, "0")}); result.push({ value: i, text: i.toString().padStart(2, "0") });
} }
result.push({"value": -1, "text": $gettext("Unknown")}); result.push({ value: -1, text: $gettext("Unknown") });
return result; return result;
}; };
export const Years = () => { export const Years = () => {
let result = []; let result = [];
const currentYear = new Date().getUTCFullYear(); const currentYear = new Date().getUTCFullYear();
for (let i = currentYear; i >= 1750; i--) { for (let i = currentYear; i >= 1750; i--) {
result.push({"value": i, "text": i.toString().padStart(4, "0")}); result.push({ value: i, text: i.toString().padStart(4, "0") });
} }
result.push({"value": -1, "text": $gettext("Unknown")}); result.push({ value: -1, text: $gettext("Unknown") });
return result; return result;
}; };
export const IndexedYears = () => { export const IndexedYears = () => {
let result = []; let result = [];
if (config.values.years) { if (config.values.years) {
for (let i = 0; i < config.values.years.length; i++) { for (let i = 0; i < config.values.years.length; i++) {
result.push({"value": parseInt(config.values.years[i]), "text": config.values.years[i].toString()}); result.push({
} value: parseInt(config.values.years[i]),
text: config.values.years[i].toString(),
});
} }
}
result.push({"value": -1, "text": $gettext("Unknown")}); result.push({ value: -1, text: $gettext("Unknown") });
return result; return result;
}; };
export const Months = () => { export const Months = () => {
let result = []; let result = [];
const months = Info.months("long"); const months = Info.months("long");
for (let i = 0; i < months.length; i++) { for (let i = 0; i < months.length; i++) {
result.push({"value": i + 1, "text": months[i]}); result.push({ value: i + 1, text: months[i] });
} }
result.push({"value": -1, "text": $gettext("Unknown")}); result.push({ value: -1, text: $gettext("Unknown") });
return result; return result;
}; };
export const MonthsShort = () => { export const MonthsShort = () => {
let result = []; let result = [];
for (let i = 1; i <= 12; i++) { for (let i = 1; i <= 12; i++) {
result.push({"value": i, "text": i.toString().padStart(2, "0")}); result.push({ value: i, text: i.toString().padStart(2, "0") });
} }
result.push({"value": -1, "text": $gettext("Unknown")}); result.push({ value: -1, text: $gettext("Unknown") });
return result; return result;
}; };
export const Languages = () => [ export const Languages = () => [
{ {
"text": "English", text: "English",
"translated": $gettext("English"), translated: $gettext("English"),
"value": "en", value: "en",
}, },
{ {
"text": "Deutsch", text: "Deutsch",
"translated": $gettext("German"), translated: $gettext("German"),
"value": "de", value: "de",
}, },
{ {
"text": "Español", text: "Español",
"translated": $gettext("Spanish"), translated: $gettext("Spanish"),
"value": "es", value: "es",
}, },
{ {
"text": "Français", text: "Français",
"translated": $gettext("French"), translated: $gettext("French"),
"value": "fr", value: "fr",
}, },
{ {
"text": "हिन्दी", text: "हिन्दी",
"translated": $gettext("Hindi"), translated: $gettext("Hindi"),
"value": "hi", value: "hi",
}, },
{ {
"text": "Nederlands", text: "Nederlands",
"translated": $gettext("Dutch"), translated: $gettext("Dutch"),
"value": "nl", value: "nl",
}, },
{ {
"text": "Polski", text: "Polski",
"translated": $gettext("Polish"), translated: $gettext("Polish"),
"value": "pl", value: "pl",
}, },
{ {
"text": "Português do Brasil", text: "Português do Brasil",
"translated": $gettext("Brazilian Portuguese"), translated: $gettext("Brazilian Portuguese"),
"value": "pt_BR", value: "pt_BR",
}, },
{ {
"text": "Русский", text: "Русский",
"translated": $gettext("Russian"), translated: $gettext("Russian"),
"value": "ru", value: "ru",
}, },
{ {
"text": "Slovenčina", text: "Slovenčina",
"translated": $gettext("Slovak"), translated: $gettext("Slovak"),
"value": "sk", value: "sk",
}, },
{ {
"text": "简体中文", text: "简体中文",
"translated": $gettext("Chinese Simplified"), translated: $gettext("Chinese Simplified"),
"value": "zh", value: "zh",
}, },
{ {
"text": "繁体中文", text: "繁体中文",
"translated": $gettext("Chinese Traditional"), translated: $gettext("Chinese Traditional"),
"value": "zh_TW", value: "zh_TW",
}, },
]; ];
export const Themes = () => [ export const Themes = () => [
{ {
"text": $gettext("Default"), text: $gettext("Default"),
"value": "default", value: "default",
}, },
{ {
"text": $gettext("Cyano"), text: $gettext("Cyano"),
"value": "cyano", value: "cyano",
}, },
{ {
"text": $gettext("Lavender"), text: $gettext("Lavender"),
"value": "lavender", value: "lavender",
}, },
{ {
"text": $gettext("Moonlight"), text: $gettext("Moonlight"),
"value": "moonlight", value: "moonlight",
}, },
{ {
"text": $gettext("Onyx"), text: $gettext("Onyx"),
"value": "onyx", value: "onyx",
}, },
{ {
"text": $gettext("Raspberry"), text: $gettext("Raspberry"),
"value": "raspberry", value: "raspberry",
}, },
{ {
"text": $gettext("Seaweed"), text: $gettext("Seaweed"),
"value": "seaweed", value: "seaweed",
}, },
]; ];
export const MapsAnimate = () => [ export const MapsAnimate = () => [
{ {
"text": $gettext("None"), text: $gettext("None"),
"value": 0, value: 0,
}, },
{ {
"text": $gettext("Fast"), text: $gettext("Fast"),
"value": 2500, value: 2500,
}, },
{ {
"text": $gettext("Medium"), text: $gettext("Medium"),
"value": 6250, value: 6250,
}, },
{ {
"text": $gettext("Slow"), text: $gettext("Slow"),
"value": 10000, value: 10000,
}, },
]; ];
export const MapsStyle = () => [ export const MapsStyle = () => [
{ {
"text": $gettext("Offline"), text: $gettext("Offline"),
"value": "offline", value: "offline",
}, },
{ {
"text": $gettext("Streets"), text: $gettext("Streets"),
"value": "streets", value: "streets",
}, },
{ {
"text": $gettext("Hybrid"), text: $gettext("Hybrid"),
"value": "hybrid", value: "hybrid",
}, },
{ {
"text": $gettext("Topographic"), text: $gettext("Topographic"),
"value": "topographique", value: "topographique",
}, },
{ {
"text": $gettext("Outdoor"), text: $gettext("Outdoor"),
"value": "outdoor", value: "outdoor",
}, },
]; ];
export const PhotoTypes = () => [ export const PhotoTypes = () => [
{ {
"text": $gettext("Image"), text: $gettext("Image"),
"value": TypeImage, value: TypeImage,
}, },
{ {
"text": $gettext("Raw"), text: $gettext("Raw"),
"value": TypeRaw, value: TypeRaw,
}, },
{ {
"text": $gettext("Live"), text: $gettext("Live"),
"value": TypeLive, value: TypeLive,
}, },
{ {
"text": $gettext("Video"), text: $gettext("Video"),
"value": TypeVideo, value: TypeVideo,
}, },
]; ];
export const Intervals = () => [ export const Intervals = () => [
{"value": 0, "text": $gettext("Never")}, { value: 0, text: $gettext("Never") },
{"value": 3600, "text": $gettext("1 hour")}, { value: 3600, text: $gettext("1 hour") },
{"value": 3600 * 4, "text": $gettext("4 hours")}, { value: 3600 * 4, text: $gettext("4 hours") },
{"value": 3600 * 12, "text": $gettext("12 hours")}, { value: 3600 * 12, text: $gettext("12 hours") },
{"value": 86400, "text": $gettext("Daily")}, { value: 86400, text: $gettext("Daily") },
{"value": 86400 * 2, "text": $gettext("Every two days")}, { value: 86400 * 2, text: $gettext("Every two days") },
{"value": 86400 * 7, "text": $gettext("Once a week")}, { value: 86400 * 7, text: $gettext("Once a week") },
]; ];
export const Expires = () => [ export const Expires = () => [
{"value": 0, "text": $gettext("Never")}, { value: 0, text: $gettext("Never") },
{"value": 86400, "text": $gettext("After 1 day")}, { value: 86400, text: $gettext("After 1 day") },
{"value": 86400 * 3, "text": $gettext("After 3 days")}, { value: 86400 * 3, text: $gettext("After 3 days") },
{"value": 86400 * 7, "text": $gettext("After 7 days")}, { value: 86400 * 7, text: $gettext("After 7 days") },
{"value": 86400 * 14, "text": $gettext("After two weeks")}, { value: 86400 * 14, text: $gettext("After two weeks") },
{"value": 86400 * 31, "text": $gettext("After one month")}, { value: 86400 * 31, text: $gettext("After one month") },
{"value": 86400 * 60, "text": $gettext("After two months")}, { value: 86400 * 60, text: $gettext("After two months") },
{"value": 86400 * 365, "text": $gettext("After one year")}, { value: 86400 * 365, text: $gettext("After one year") },
]; ];
export const Colors = () => [ export const Colors = () => [
{"Example": "#AB47BC", "Name": $gettext("Purple"), "Slug": "purple"}, { Example: "#AB47BC", Name: $gettext("Purple"), Slug: "purple" },
{"Example": "#FF00FF", "Name": $gettext("Magenta"), "Slug": "magenta"}, { Example: "#FF00FF", Name: $gettext("Magenta"), Slug: "magenta" },
{"Example": "#EC407A", "Name": $gettext("Pink"), "Slug": "pink"}, { Example: "#EC407A", Name: $gettext("Pink"), Slug: "pink" },
{"Example": "#EF5350", "Name": $gettext("Red"), "Slug": "red"}, { Example: "#EF5350", Name: $gettext("Red"), Slug: "red" },
{"Example": "#FFA726", "Name": $gettext("Orange"), "Slug": "orange"}, { Example: "#FFA726", Name: $gettext("Orange"), Slug: "orange" },
{"Example": "#D4AF37", "Name": $gettext("Gold"), "Slug": "gold"}, { Example: "#D4AF37", Name: $gettext("Gold"), Slug: "gold" },
{"Example": "#FDD835", "Name": $gettext("Yellow"), "Slug": "yellow"}, { Example: "#FDD835", Name: $gettext("Yellow"), Slug: "yellow" },
{"Example": "#CDDC39", "Name": $gettext("Lime"), "Slug": "lime"}, { Example: "#CDDC39", Name: $gettext("Lime"), Slug: "lime" },
{"Example": "#66BB6A", "Name": $gettext("Green"), "Slug": "green"}, { Example: "#66BB6A", Name: $gettext("Green"), Slug: "green" },
{"Example": "#009688", "Name": $gettext("Teal"), "Slug": "teal"}, { Example: "#009688", Name: $gettext("Teal"), Slug: "teal" },
{"Example": "#00BCD4", "Name": $gettext("Cyan"), "Slug": "cyan"}, { Example: "#00BCD4", Name: $gettext("Cyan"), Slug: "cyan" },
{"Example": "#2196F3", "Name": $gettext("Blue"), "Slug": "blue"}, { Example: "#2196F3", Name: $gettext("Blue"), Slug: "blue" },
{"Example": "#A1887F", "Name": $gettext("Brown"), "Slug": "brown"}, { Example: "#A1887F", Name: $gettext("Brown"), Slug: "brown" },
{"Example": "#F5F5F5", "Name": $gettext("White"), "Slug": "white"}, { Example: "#F5F5F5", Name: $gettext("White"), Slug: "white" },
{"Example": "#9E9E9E", "Name": $gettext("Grey"), "Slug": "grey"}, { Example: "#9E9E9E", Name: $gettext("Grey"), Slug: "grey" },
{"Example": "#212121", "Name": $gettext("Black"), "Slug": "black"}, { Example: "#212121", Name: $gettext("Black"), Slug: "black" },
]; ];
export const FeedbackCategories = () => [ export const FeedbackCategories = () => [
{"value": "help", "text": $gettext("Customer Support")}, { value: "help", text: $gettext("Customer Support") },
{"value": "feedback", "text": $gettext("Product Feedback")}, { value: "feedback", text: $gettext("Product Feedback") },
{"value": "feature", "text": $gettext("Feature Request")}, { value: "feature", text: $gettext("Feature Request") },
{"value": "bug", "text": $gettext("Bug Report")}, { value: "bug", text: $gettext("Bug Report") },
{"value": "donations", "text": $gettext("Donations")}, { value: "donations", text: $gettext("Donations") },
{"value": "other", "text": $gettext("Other")}, { value: "other", text: $gettext("Other") },
]; ];

View File

@@ -44,288 +44,309 @@ import About from "pages/about/about.vue";
import Feedback from "pages/about/feedback.vue"; import Feedback from "pages/about/feedback.vue";
import License from "pages/about/license.vue"; import License from "pages/about/license.vue";
import Help from "pages/help.vue"; import Help from "pages/help.vue";
import {$gettext} from "common/vm"; import { $gettext } from "common/vm";
const c = window.__CONFIG__; const c = window.__CONFIG__;
export default [ export default [
{ {
name: "home", name: "home",
path: "/", path: "/",
redirect: "/photos", redirect: "/photos",
},
{
name: "about",
path: "/about",
component: About,
meta: { title: c.name, auth: false },
},
{
name: "feedback",
path: "/feedback",
component: Feedback,
meta: { title: c.name, auth: true },
},
{
name: "license",
path: "/about/license",
component: License,
meta: { title: c.name, auth: false },
},
{
name: "help",
path: "/help*",
component: Help,
meta: { title: c.name, auth: false },
},
{
name: "login",
path: "/login",
component: Login,
meta: { auth: false },
},
{
name: "photos",
path: "/photos",
component: Photos,
meta: { title: c.name, auth: true },
props: { staticFilter: { photo: "true" } },
},
{
name: "moments",
path: "/moments",
component: Albums,
meta: { title: $gettext("Moments"), auth: true },
props: { view: "moment", staticFilter: { type: "moment" } },
},
{
name: "moment",
path: "/moments/:uid/:slug",
component: AlbumPhotos,
meta: { title: $gettext("Moments"), auth: true },
},
{
name: "albums",
path: "/albums",
component: Albums,
meta: { title: $gettext("Albums"), auth: true },
props: { view: "album", staticFilter: { type: "album" } },
},
{
name: "album",
path: "/albums/:uid/:slug",
component: AlbumPhotos,
meta: { auth: true },
},
{
name: "calendar",
path: "/calendar",
component: Albums,
meta: { title: $gettext("Calendar"), auth: true },
props: { view: "month", staticFilter: { type: "month" } },
},
{
name: "month",
path: "/calendar/:uid/:slug",
component: AlbumPhotos,
meta: { title: $gettext("Calendar"), auth: true },
},
{
name: "folders",
path: "/folders",
component: Albums,
meta: { title: $gettext("Folders"), auth: true },
props: { view: "folder", staticFilter: { type: "folder", order: "default" } },
},
{
name: "folder",
path: "/folders/:uid/:slug",
component: AlbumPhotos,
meta: { title: $gettext("Folders"), auth: true },
},
{
name: "unsorted",
path: "/unsorted",
component: Photos,
meta: { title: $gettext("Unsorted"), auth: true },
props: { staticFilter: { unsorted: true } },
},
{
name: "favorites",
path: "/favorites",
component: Photos,
meta: { title: $gettext("Favorites"), auth: true },
props: { staticFilter: { favorite: true } },
},
{
name: "videos",
path: "/videos",
component: Photos,
meta: { title: $gettext("Videos"), auth: true },
props: { staticFilter: { video: "true" } },
},
{
name: "review",
path: "/review",
component: Photos,
meta: { title: $gettext("Review"), auth: true },
props: { staticFilter: { review: true } },
},
{
name: "private",
path: "/private",
component: Photos,
meta: { title: $gettext("Private"), auth: true },
props: { staticFilter: { private: true } },
},
{
name: "archive",
path: "/archive",
component: Photos,
meta: { title: $gettext("Archive"), auth: true },
props: { staticFilter: { archived: true } },
},
{
name: "places",
path: "/places",
component: Places,
meta: { title: $gettext("Places"), auth: true },
},
{
name: "place",
path: "/places/:q",
component: Places,
meta: { title: $gettext("Places"), auth: true },
},
{
name: "states",
path: "/states",
component: Albums,
meta: { title: $gettext("Places"), auth: true },
props: { view: "state", staticFilter: { type: "state" } },
},
{
name: "state",
path: "/states/:uid/:slug",
component: AlbumPhotos,
meta: { title: $gettext("Places"), auth: true },
},
{
name: "files",
path: "/library/files*",
component: Files,
meta: { title: $gettext("File Browser"), auth: true },
},
{
name: "hidden",
path: "/library/hidden",
component: Photos,
meta: { title: $gettext("Hidden Files"), auth: true },
props: { staticFilter: { hidden: true } },
},
{
name: "errors",
path: "/library/errors",
component: Errors,
meta: { title: c.name, auth: true },
},
{
name: "labels",
path: "/labels",
component: Labels,
meta: { title: $gettext("Labels"), auth: true },
},
{
name: "browse",
path: "/browse",
component: Photos,
meta: { title: $gettext("Search"), auth: true },
props: { staticFilter: { quality: 0 } },
},
{
name: "people",
path: "/people",
component: People,
meta: { title: $gettext("People"), auth: true },
},
{
name: "library",
path: "/library",
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-index" },
},
{
name: "library_import",
path: "/library/import",
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-import" },
},
{
name: "library_logs",
path: "/library/logs",
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-logs" },
},
{
name: "settings",
path: "/settings",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
}, },
{ props: { tab: "settings-general" },
name: "about", },
path: "/about", {
component: About, name: "settings_library",
meta: {title: c.name, auth: false}, path: "/settings/library",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
}, },
{ props: { tab: "settings-library" },
name: "feedback", },
path: "/feedback", {
component: Feedback, name: "settings_sync",
meta: {title: c.name, auth: true}, path: "/settings/sync",
component: Settings,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
}, },
{ props: { tab: "settings-sync" },
name: "license", },
path: "/about/license", {
component: License, name: "settings_account",
meta: {title: c.name, auth: false}, path: "/settings/account",
}, component: Settings,
{ meta: {
name: "help", title: $gettext("Settings"),
path: "/help*", auth: true,
component: Help, settings: true,
meta: {title: c.name, auth: false}, background: "application-light",
},
{
name: "login",
path: "/login",
component: Login,
meta: {auth: false},
},
{
name: "photos",
path: "/photos",
component: Photos,
meta: {title: c.name, auth: true},
props: {staticFilter: {photo: "true"}},
},
{
name: "moments",
path: "/moments",
component: Albums,
meta: {title: $gettext("Moments"), auth: true},
props: {view: "moment", staticFilter: {type: "moment"}},
},
{
name: "moment",
path: "/moments/:uid/:slug",
component: AlbumPhotos,
meta: {title: $gettext("Moments"), auth: true},
},
{
name: "albums",
path: "/albums",
component: Albums,
meta: {title: $gettext("Albums"), auth: true},
props: {view: "album", staticFilter: {type: "album"}},
},
{
name: "album",
path: "/albums/:uid/:slug",
component: AlbumPhotos,
meta: {auth: true},
},
{
name: "calendar",
path: "/calendar",
component: Albums,
meta: {title: $gettext("Calendar"), auth: true},
props: {view: "month", staticFilter: {type: "month"}},
},
{
name: "month",
path: "/calendar/:uid/:slug",
component: AlbumPhotos,
meta: {title: $gettext("Calendar"), auth: true},
},
{
name: "folders",
path: "/folders",
component: Albums,
meta: {title: $gettext("Folders"), auth: true},
props: {view: "folder", staticFilter: {type: "folder", order: "default"}},
},
{
name: "folder",
path: "/folders/:uid/:slug",
component: AlbumPhotos,
meta: {title: $gettext("Folders"), auth: true},
},
{
name: "unsorted",
path: "/unsorted",
component: Photos,
meta: {title: $gettext("Unsorted"), auth: true},
props: {staticFilter: {unsorted: true}},
},
{
name: "favorites",
path: "/favorites",
component: Photos,
meta: {title: $gettext("Favorites"), auth: true},
props: {staticFilter: {favorite: true}},
},
{
name: "videos",
path: "/videos",
component: Photos,
meta: {title: $gettext("Videos"), auth: true},
props: {staticFilter: {video: "true"}},
},
{
name: "review",
path: "/review",
component: Photos,
meta: {title: $gettext("Review"), auth: true},
props: {staticFilter: {review: true}},
},
{
name: "private",
path: "/private",
component: Photos,
meta: {title: $gettext("Private"), auth: true},
props: {staticFilter: {private: true}},
},
{
name: "archive",
path: "/archive",
component: Photos,
meta: {title: $gettext("Archive"), auth: true},
props: {staticFilter: {archived: true}},
},
{
name: "places",
path: "/places",
component: Places,
meta: {title: $gettext("Places"), auth: true},
},
{
name: "place",
path: "/places/:q",
component: Places,
meta: {title: $gettext("Places"), auth: true},
},
{
name: "states",
path: "/states",
component: Albums,
meta: {title: $gettext("Places"), auth: true},
props: {view: "state", staticFilter: {type: "state"}},
},
{
name: "state",
path: "/states/:uid/:slug",
component: AlbumPhotos,
meta: {title: $gettext("Places"), auth: true},
},
{
name: "files",
path: "/library/files*",
component: Files,
meta: {title: $gettext("File Browser"), auth: true},
},
{
name: "hidden",
path: "/library/hidden",
component: Photos,
meta: {title: $gettext("Hidden Files"), auth: true},
props: {staticFilter: {hidden: true}},
},
{
name: "errors",
path: "/library/errors",
component: Errors,
meta: {title: c.name, auth: true},
},
{
name: "labels",
path: "/labels",
component: Labels,
meta: {title: $gettext("Labels"), auth: true},
},
{
name: "browse",
path: "/browse",
component: Photos,
meta: {title: $gettext("Search"), auth: true},
props: {staticFilter: {quality: 0}},
},
{
name: "people",
path: "/people",
component: People,
meta: {title: $gettext("People"), auth: true},
},
{
name: "library",
path: "/library",
component: Library,
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
props: {tab: "library-index"},
},
{
name: "library_import",
path: "/library/import",
component: Library,
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
props: {tab: "library-import"},
},
{
name: "library_logs",
path: "/library/logs",
component: Library,
meta: {title: $gettext("Library"), auth: true, background: "application-light"},
props: {tab: "library-logs"},
},
{
name: "settings",
path: "/settings",
component: Settings,
meta: {title: $gettext("Settings"), auth: true, settings: true, background: "application-light"},
props: {tab: "settings-general"},
},
{
name: "settings_library",
path: "/settings/library",
component: Settings,
meta: {title: $gettext("Settings"), auth: true, settings: true, background: "application-light"},
props: {tab: "settings-library"},
},
{
name: "settings_sync",
path: "/settings/sync",
component: Settings,
meta: {title: $gettext("Settings"), auth: true, settings: true, background: "application-light"},
props: {tab: "settings-sync"},
},
{
name: "settings_account",
path: "/settings/account",
component: Settings,
meta: {title: $gettext("Settings"), auth: true, settings: true, background: "application-light"},
props: {tab: "settings-account"},
},
{
name: "discover",
path: "/discover",
component: Discover,
meta: {title: $gettext("Discover"), auth: true, background: "application-light"},
props: {tab: 0},
},
{
name: "discover_similar",
path: "/discover/similar",
component: Discover,
meta: {title: $gettext("Discover"), auth: true, background: "application-light"},
props: {tab: 1},
},
{
name: "discover_season",
path: "/discover/season",
component: Discover,
meta: {title: $gettext("Discover"), auth: true, background: "application-light"},
props: {tab: 2},
},
{
name: "discover_random",
path: "/discover/random",
component: Discover,
meta: {title: $gettext("Discover"), auth: true, background: "application-light"},
props: {tab: 3},
},
{
path: "*", redirect: "/photos",
}, },
props: { tab: "settings-account" },
},
{
name: "discover",
path: "/discover",
component: Discover,
meta: { title: $gettext("Discover"), auth: true, background: "application-light" },
props: { tab: 0 },
},
{
name: "discover_similar",
path: "/discover/similar",
component: Discover,
meta: { title: $gettext("Discover"), auth: true, background: "application-light" },
props: { tab: 1 },
},
{
name: "discover_season",
path: "/discover/season",
component: Discover,
meta: { title: $gettext("Discover"), auth: true, background: "application-light" },
props: { tab: 2 },
},
{
name: "discover_random",
path: "/discover/random",
component: Discover,
meta: { title: $gettext("Discover"), auth: true, background: "application-light" },
props: { tab: 3 },
},
{
path: "*",
redirect: "/photos",
},
]; ];

View File

@@ -41,8 +41,8 @@ import Log from "common/log";
import PhotoPrism from "share.vue"; import PhotoPrism from "share.vue";
import Router from "vue-router"; import Router from "vue-router";
import Routes from "share/routes"; import Routes from "share/routes";
import {config, session} from "session"; import { config, session } from "session";
import {Settings} from "luxon"; import { Settings } from "luxon";
import Socket from "common/websocket"; import Socket from "common/websocket";
import Viewer from "common/viewer"; import Viewer from "common/viewer";
import Vue from "vue"; import Vue from "vue";
@@ -53,13 +53,15 @@ import VueFullscreen from "vue-fullscreen";
import VueInfiniteScroll from "vue-infinite-scroll"; import VueInfiniteScroll from "vue-infinite-scroll";
import VueModal from "vue-js-modal"; import VueModal from "vue-js-modal";
import Hls from "hls.js"; import Hls from "hls.js";
import {$gettext, Mount} from "common/vm"; import { $gettext, Mount } from "common/vm";
// Initialize helpers // Initialize helpers
const viewer = new Viewer(); const viewer = new Viewer();
const clipboard = new Clipboard(window.localStorage, "photo_clipboard"); const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
const isPublic = config.get("public"); const isPublic = config.get("public");
const isMobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
// HTTP Live Streaming (video support) // HTTP Live Streaming (video support)
window.Hls = Hls; window.Hls = Hls;
@@ -77,23 +79,23 @@ Vue.prototype.$clipboard = clipboard;
Vue.prototype.$isMobile = isMobile; Vue.prototype.$isMobile = isMobile;
// Register Vuetify // Register Vuetify
Vue.use(Vuetify, {"theme": config.theme}); Vue.use(Vuetify, { theme: config.theme });
Vue.config.language = config.values.settings.ui.language; Vue.config.language = config.values.settings.ui.language;
Settings.defaultLocale = Vue.config.language.substring(0, 2); Settings.defaultLocale = Vue.config.language.substring(0, 2);
// Register other VueJS plugins // Register other VueJS plugins
Vue.use(GetTextPlugin, { Vue.use(GetTextPlugin, {
translations: config.translations, translations: config.translations,
silent: true, // !config.values.debug, silent: true, // !config.values.debug,
defaultLanguage: Vue.config.language, defaultLanguage: Vue.config.language,
autoAddKeyAttributes: true, autoAddKeyAttributes: true,
}); });
Vue.use(VueLuxon); Vue.use(VueLuxon);
Vue.use(VueInfiniteScroll); Vue.use(VueInfiniteScroll);
Vue.use(VueFullscreen); Vue.use(VueFullscreen);
Vue.use(VueModal, {dynamic: true, dynamicDefaults: {clickToClose: true}}); Vue.use(VueModal, { dynamic: true, dynamicDefaults: { clickToClose: true } });
Vue.use(VueFilters); Vue.use(VueFilters);
Vue.use(Components); Vue.use(Components);
Vue.use(Dialogs); Vue.use(Dialogs);
@@ -101,52 +103,52 @@ Vue.use(Router);
// Configure client-side routing // Configure client-side routing
const router = new Router({ const router = new Router({
routes: Routes, routes: Routes,
mode: "history", mode: "history",
saveScrollPosition: true, saveScrollPosition: true,
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.settings) && config.values.disable.settings) { if (to.matched.some((record) => record.meta.settings) && config.values.disable.settings) {
next({name: "home"}); next({ name: "home" });
} else if (to.matched.some(record => record.meta.admin)) { } else if (to.matched.some((record) => record.meta.admin)) {
if (isPublic || session.isAdmin()) { if (isPublic || session.isAdmin()) {
next(); next();
} else {
next({
name: "login",
params: {nextUrl: to.fullPath},
});
}
} else if (to.matched.some(record => record.meta.auth)) {
if (isPublic || session.isUser()) {
next();
} else {
next({
name: "login",
params: {nextUrl: to.fullPath},
});
}
} else { } else {
next(); next({
name: "login",
params: { nextUrl: to.fullPath },
});
} }
} else if (to.matched.some((record) => record.meta.auth)) {
if (isPublic || session.isUser()) {
next();
} else {
next({
name: "login",
params: { nextUrl: to.fullPath },
});
}
} else {
next();
}
}); });
router.afterEach((to) => { router.afterEach((to) => {
if (to.meta.title && config.values.siteTitle !== to.meta.title) { if (to.meta.title && config.values.siteTitle !== to.meta.title) {
config.page.title = $gettext(to.meta.title); config.page.title = $gettext(to.meta.title);
window.document.title = config.page.title; window.document.title = config.page.title;
} else { } else {
config.page.title = config.values.siteTitle; config.page.title = config.values.siteTitle;
window.document.title = config.values.siteTitle; window.document.title = config.values.siteTitle;
} }
}); });
// Pull client config every 10 minutes in case push fails (except on mobile to save battery). // Pull client config every 10 minutes in case push fails (except on mobile to save battery).
if (isMobile) { if (isMobile) {
document.body.classList.add("mobile"); document.body.classList.add("mobile");
} else { } else {
setInterval(() => config.update(), 600000); setInterval(() => config.update(), 600000);
} }
// Start application. // Start application.

View File

@@ -43,17 +43,17 @@ import PAlbumClipboard from "./album/clipboard.vue";
const components = {}; const components = {};
components.install = (Vue) => { components.install = (Vue) => {
Vue.component("p-notify", PNotify); Vue.component("PNotify", PNotify);
Vue.component("p-navigation", PNavigation); Vue.component("PNavigation", PNavigation);
Vue.component("p-scroll-top", PScrollTop); Vue.component("PScrollTop", PScrollTop);
Vue.component("p-loading-bar", PLoadingBar); Vue.component("PLoadingBar", PLoadingBar);
Vue.component("p-video-player", PVideoPlayer); Vue.component("PVideoPlayer", PVideoPlayer);
Vue.component("p-photo-viewer", PPhotoViewer); Vue.component("PPhotoViewer", PPhotoViewer);
Vue.component("p-photo-cards", PPhotoCards); Vue.component("PPhotoCards", PPhotoCards);
Vue.component("p-photo-mosaic", PPhotoMosaic); Vue.component("PPhotoMosaic", PPhotoMosaic);
Vue.component("p-photo-list", PPhotoList); Vue.component("PPhotoList", PPhotoList);
Vue.component("p-photo-clipboard", PPhotoClipboard); Vue.component("PPhotoClipboard", PPhotoClipboard);
Vue.component("p-album-clipboard", PAlbumClipboard); Vue.component("PAlbumClipboard", PAlbumClipboard);
}; };
export default components; export default components;

View File

@@ -2,28 +2,30 @@ import Albums from "share/albums.vue";
import AlbumPhotos from "share/photos.vue"; import AlbumPhotos from "share/photos.vue";
const c = window.__CONFIG__; const c = window.__CONFIG__;
const shareTitle = c.settings.share.title ? c.settings.share.title : c.siteAuthor ? c.siteAuthor : c.name; const siteTitle = c.siteAuthor ? c.siteAuthor : c.name;
const shareTitle = c.settings.share.title ? c.settings.share.title : siteTitle;
export default [ export default [
{ {
name: "home", name: "home",
path: "/", path: "/",
redirect: {name: "albums"}, redirect: { name: "albums" },
}, },
{ {
name: "albums", name: "albums",
path: "/s/:token", path: "/s/:token",
component: Albums, component: Albums,
meta: {title: shareTitle, auth: true}, meta: { title: shareTitle, auth: true },
props: {view: "album", staticFilter: {type: "album"}}, props: { view: "album", staticFilter: { type: "album" } },
}, },
{ {
name: "album", name: "album",
path: "/s/:token/:uid", path: "/s/:token/:uid",
component: AlbumPhotos, component: AlbumPhotos,
meta: {title: shareTitle, auth: true}, meta: { title: shareTitle, auth: true },
}, },
{ {
path: "*", redirect: {name: "albums"}, path: "*",
}, redirect: { name: "albums" },
},
]; ];

View File

@@ -36,205 +36,199 @@ const webpack = require("webpack");
const isDev = process.env.NODE_ENV !== "production"; const isDev = process.env.NODE_ENV !== "production";
if(isDev) { if (isDev) {
console.log("Building frontend in DEVELOPMENT mode. Please wait."); console.log("Building frontend in DEVELOPMENT mode. Please wait.");
} else { } else {
console.log("Building frontend in PRODUCTION mode. Please wait."); console.log("Building frontend in PRODUCTION mode. Please wait.");
} }
const PATHS = { const PATHS = {
app: path.join(__dirname, "src/app.js"), app: path.join(__dirname, "src/app.js"),
share: path.join(__dirname, "src/share.js"), share: path.join(__dirname, "src/share.js"),
js: path.join(__dirname, "src"), js: path.join(__dirname, "src"),
css: path.join(__dirname, "src/css"), css: path.join(__dirname, "src/css"),
build: path.join(__dirname, "../assets/static/build"), build: path.join(__dirname, "../assets/static/build"),
}; };
const config = { const config = {
mode: isDev ? "development" : "production", mode: isDev ? "development" : "production",
devtool: isDev ? "inline-source-map" : false, devtool: isDev ? "inline-source-map" : false,
entry: { entry: {
app: PATHS.app, app: PATHS.app,
share: PATHS.share, share: PATHS.share,
},
output: {
path: PATHS.build,
filename: "[name].js",
},
resolve: {
modules: [path.join(__dirname, "src"), path.join(__dirname, "node_modules")],
alias: {
vue: isDev ? "vue/dist/vue.js" : "vue/dist/vue.min.js",
}, },
output: { },
path: PATHS.build, plugins: [
filename: "[name].js", new MiniCssExtractPlugin({
}, filename: "[name].css",
resolve: { }),
modules: [ ],
path.join(__dirname, "src"), node: {
path.join(__dirname, "node_modules"), fs: "empty",
], },
alias: { performance: {
vue: isDev ? "vue/dist/vue.js" : "vue/dist/vue.min.js", hints: isDev ? false : "error",
maxEntrypointSize: 4000000,
maxAssetSize: 4000000,
},
module: {
rules: [
{
test: /\.js$/,
include: PATHS.app,
exclude: /node_modules/,
enforce: "pre",
loader: "eslint-loader",
options: {
formatter: require("eslint-formatter-pretty"),
}, },
}, },
plugins: [ {
new MiniCssExtractPlugin({ test: /\.vue$/,
filename: "[name].css", loader: "vue-loader",
}), include: PATHS.js,
], options: {
node: { loaders: {
fs: "empty", js: "babel-loader",
}, css: "css-loader",
performance: { },
hints: isDev ? false : "error", },
maxEntrypointSize: 4000000, },
maxAssetSize: 4000000, {
}, test: /\.js$/,
module: { loader: "babel-loader",
rules: [ include: PATHS.js,
{ exclude: (file) => /node_modules/.test(file),
test: /\.js$/, query: {
include: PATHS.app, presets: ["@babel/preset-env"],
exclude: /node_modules/, compact: false,
enforce: "pre", },
loader: "eslint-loader", },
options: { {
formatter: require("eslint-formatter-pretty"), test: /\.css$/,
include: PATHS.css,
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: false,
fallback: "vue-style-loader",
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: isDev,
},
}, },
}, {
{ loader: "postcss-loader",
test: /\.vue$/, options: {
loader: "vue-loader", sourceMap: isDev,
include: PATHS.js, config: {
options: { path: path.resolve(__dirname, "./postcss.config.js"),
loaders: {
js: "babel-loader",
css: "css-loader",
}, },
},
}, },
"resolve-url-loader",
],
publicPath: PATHS.build,
}, },
{ },
test: /\.js$/, "css-loader",
loader: "babel-loader",
include: PATHS.js,
exclude: file => (
/node_modules/.test(file)
),
query: {
presets: ["@babel/preset-env"],
compact: false,
},
},
{
test: /\.css$/,
include: PATHS.css,
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: false,
fallback: "vue-style-loader",
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: isDev,
},
},
{
loader: "postcss-loader",
options: {
sourceMap: isDev,
config: {
path: path.resolve(__dirname, "./postcss.config.js"),
},
},
},
"resolve-url-loader",
],
publicPath: PATHS.build,
},
},
"css-loader",
],
},
{
test: /\.css$/,
include: /node_modules/,
loaders: [
"vue-style-loader",
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 1, sourceMap: isDev },
},
{
loader: "postcss-loader",
options: {
sourceMap: isDev,
config: {
path: path.resolve(__dirname, "./postcss.config.js"),
},
},
},
"resolve-url-loader",
],
},
{
test: /\.s[c|a]ss$/,
use: [
"vue-style-loader",
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 2, sourceMap: isDev },
},
{
loader: "postcss-loader",
options: {
sourceMap: isDev,
config: {
path: path.resolve(__dirname, "./postcss.config.js"),
},
},
},
"resolve-url-loader",
"sass-loader",
],
},
{
test: /\.(png|jpg|jpeg|gif)$/,
loader: "file-loader",
options: {
name: "[hash].[ext]",
publicPath: "/static/build/img",
outputPath: "img",
},
},
{
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: "file-loader",
options: {
name: "[hash].[ext]",
publicPath: "/static/build/fonts",
outputPath: "fonts",
},
},
{
test: /\.svg/,
use: {
loader: "svg-url-loader",
options: {},
},
},
], ],
}, },
{
test: /\.css$/,
include: /node_modules/,
loaders: [
"vue-style-loader",
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 1, sourceMap: isDev },
},
{
loader: "postcss-loader",
options: {
sourceMap: isDev,
config: {
path: path.resolve(__dirname, "./postcss.config.js"),
},
},
},
"resolve-url-loader",
],
},
{
test: /\.s[c|a]ss$/,
use: [
"vue-style-loader",
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 2, sourceMap: isDev },
},
{
loader: "postcss-loader",
options: {
sourceMap: isDev,
config: {
path: path.resolve(__dirname, "./postcss.config.js"),
},
},
},
"resolve-url-loader",
"sass-loader",
],
},
{
test: /\.(png|jpg|jpeg|gif)$/,
loader: "file-loader",
options: {
name: "[hash].[ext]",
publicPath: "/static/build/img",
outputPath: "img",
},
},
{
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
loader: "file-loader",
options: {
name: "[hash].[ext]",
publicPath: "/static/build/fonts",
outputPath: "fonts",
},
},
{
test: /\.svg/,
use: {
loader: "svg-url-loader",
options: {},
},
},
],
},
}; };
// No sourcemap for production // No sourcemap for production
if (isDev) { if (isDev) {
const devToolPlugin = new webpack.SourceMapDevToolPlugin({ const devToolPlugin = new webpack.SourceMapDevToolPlugin({
filename: "[file].map", filename: "[file].map",
}); });
config.plugins.push(devToolPlugin); config.plugins.push(devToolPlugin);
} }
module.exports = config; module.exports = config;