mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Merge remote-tracking branch 'photoprism/develop' into gorm2
This commit is contained in:
84
.gitignore
vendored
84
.gitignore
vendored
@@ -1,4 +1,42 @@
|
||||
# Local build files and directories
|
||||
# Ignore temporary build files, dependencies, logs, and test output:
|
||||
*.socket
|
||||
*.lock
|
||||
*.sock
|
||||
*.pid
|
||||
*.log
|
||||
*.jsonl
|
||||
*.db
|
||||
*.db-journal
|
||||
*.sqlite
|
||||
*.override.yml
|
||||
*.tmp.yml
|
||||
*.override.yaml
|
||||
*.tmp.yaml
|
||||
*.out
|
||||
*.test
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/*.zip
|
||||
/coverage.*
|
||||
__pycache__
|
||||
venv
|
||||
.venv
|
||||
.env
|
||||
.tmp
|
||||
.eslintcache
|
||||
/tmp/
|
||||
/test/
|
||||
*-lock.json
|
||||
/node_modules
|
||||
/frontend/node_modules/*
|
||||
/frontend/tests/*.html
|
||||
/frontend/tests/*.log
|
||||
/frontend/tests/screenshots
|
||||
/frontend/src/locales/*.mo
|
||||
/frontend/tests_output
|
||||
frontend/coverage/
|
||||
/photoprism
|
||||
/photoprism-*
|
||||
/photos/originals/*
|
||||
@@ -10,50 +48,10 @@
|
||||
/assets/nasnet
|
||||
/assets/nsfw
|
||||
/assets/static/build/
|
||||
/test/
|
||||
/pro
|
||||
/plus
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/*.zip
|
||||
|
||||
# Test output and coverage files
|
||||
*.out
|
||||
*.test
|
||||
/coverage.*
|
||||
frontend/coverage/
|
||||
|
||||
# Frontend cache and dependencies
|
||||
/node_modules
|
||||
/frontend/node_modules/*
|
||||
/frontend/tests/*.html
|
||||
/frontend/tests/*.log
|
||||
/frontend/tests/screenshots
|
||||
/frontend/src/locales/*.mo
|
||||
/frontend/tests_output
|
||||
/frontend/.eslintcache
|
||||
/package-lock.json
|
||||
|
||||
# Python cache and dependencies
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
|
||||
# Custom config, database, log, and temporary files
|
||||
/tmp/
|
||||
*.log
|
||||
*.jsonl
|
||||
*.pid
|
||||
*.db
|
||||
*.db-journal
|
||||
*.override.yml
|
||||
*.tmp.yml
|
||||
*.override.yaml
|
||||
*.tmp.yaml
|
||||
|
||||
# Automatically generated files, e.g. by editors and operating systems
|
||||
# Files created automatically by editors and/or operating systems:
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
@@ -69,5 +67,3 @@ Thumbs.db
|
||||
.c9revisions
|
||||
.settings
|
||||
.swp
|
||||
.tmp
|
||||
.env
|
||||
|
||||
12
Makefile
12
Makefile
@@ -309,22 +309,22 @@ test-js:
|
||||
(cd frontend && env TZ=UTC BUILD_ENV=development NODE_ENV=development BABEL_ENV=test npm run test)
|
||||
acceptance:
|
||||
$(info Running public-mode tests in Chrome...)
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Multi-Window)\:*" --test-meta mode=public --config-file ./testcaferc.json --experimental-multiple-windows "tests/acceptance" && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./testcaferc.json "tests/acceptance")
|
||||
acceptance-short:
|
||||
$(info Running JS acceptance tests in Chrome...)
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=public,type=short --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Multi-Window)\:*" --test-meta mode=public --config-file ./testcaferc.json --experimental-multiple-windows "tests/acceptance" && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=public,type=short --config-file ./testcaferc.json "tests/acceptance")
|
||||
acceptance-firefox:
|
||||
$(info Running JS acceptance tests in Firefox...)
|
||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./testcaferc.json --disable-native-automation "tests/acceptance")
|
||||
acceptance-auth:
|
||||
$(info Running JS acceptance-auth tests in Chrome...)
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Multi-Window)\:*" --test-meta mode=auth --config-file ./testcaferc.json --experimental-multiple-windows "tests/acceptance" && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
|
||||
acceptance-auth-short:
|
||||
$(info Running JS acceptance-auth tests in Chrome...)
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=auth,type=short --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- "chrome --headless=new" --test-grep "^(Multi-Window)\:*" --test-meta mode=auth --config-file ./testcaferc.json --experimental-multiple-windows "tests/acceptance" && npm run testcafe -- "chrome --headless=new" --test-grep "^(Common|Core)\:*" --test-meta mode=auth,type=short --config-file ./testcaferc.json "tests/acceptance")
|
||||
acceptance-auth-firefox:
|
||||
$(info Running JS acceptance-auth tests in Firefox...)
|
||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
|
||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json --disable-native-automation "tests/acceptance")
|
||||
reset-mariadb:
|
||||
$(info Resetting photoprism database...)
|
||||
mysql < scripts/sql/reset-photoprism.sql
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
{{if .redirect }}<meta http-equiv="refresh" content="3; url={{ .redirect }}">{{end}}
|
||||
|
||||
@@ -32,4 +32,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="busy-overlay"><div class="splash-center"><progress id="busy-progress" class="html-progress" max="100"></progress></div></div>
|
||||
<div id="busy-overlay"><div class="splash-center"><progress id="busy-progress" class="html-progress" max="100"></progress></div></div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
@@ -66,5 +66,5 @@ services:
|
||||
## Join shared "photoprism-develop" network
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
external: true
|
||||
|
||||
@@ -73,5 +73,5 @@ services:
|
||||
## Join shared "photoprism-develop" network
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
external: true
|
||||
|
||||
@@ -110,5 +110,5 @@ services:
|
||||
## Join shared "photoprism-develop" network
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
external: true
|
||||
|
||||
@@ -18,5 +18,5 @@ services:
|
||||
## Join shared "photoprism-develop" network
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
external: true
|
||||
|
||||
@@ -66,5 +66,5 @@ services:
|
||||
## Join shared "photoprism-develop" network
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
external: true
|
||||
|
||||
@@ -274,5 +274,5 @@ volumes:
|
||||
## Create shared "photoprism-develop" network for connecting with services in other compose.yaml files
|
||||
networks:
|
||||
default:
|
||||
name: photoprism-develop
|
||||
name: photoprism
|
||||
driver: bridge
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="overflow-y-hidden">
|
||||
<html lang="en" dir="auto" data-color-mode="dark" data-light-theme="light" data-dark-theme="dark" class="loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
1111
frontend/package-lock.json
generated
1111
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,15 +22,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
"@babel/core": "^7.26.7",
|
||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/core": "^7.26.8",
|
||||
"@babel/plugin-transform-runtime": "^7.26.8",
|
||||
"@babel/preset-env": "^7.26.8",
|
||||
"@babel/register": "^7.25.9",
|
||||
"@babel/runtime": "^7.26.7",
|
||||
"@lcdp/offline-plugin": "^5.1.1",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"@vvo/tzdb": "^6.160.0",
|
||||
"@vvo/tzdb": "^6.161.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"babel-loader": "^9.2.1",
|
||||
@@ -45,15 +45,15 @@
|
||||
"cssnano": "^7.0.6",
|
||||
"easygettext": "^2.17.0",
|
||||
"eslint": ">=8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-plugin-html": "^8.1.1",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-html": "^8.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-plugin-vuetify": "^2.5.1",
|
||||
"eslint-webpack-plugin": "^4.2.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"file-loader": "^6.2.0",
|
||||
@@ -61,6 +61,7 @@
|
||||
"floating-vue": "^5.2.2",
|
||||
"hls.js": "^1.5.20",
|
||||
"i": "^0.3.7",
|
||||
"standard": "^17.1.2",
|
||||
"karma": "^6.4.4",
|
||||
"karma-chrome-launcher": "^3.2.0",
|
||||
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||
@@ -73,22 +74,22 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"minimist": ">=1.2.8",
|
||||
"mocha": "^11.0.1",
|
||||
"mocha": "^11.1.0",
|
||||
"node-storage-shim": "^2.0.1",
|
||||
"passive-events-support": "^1.1.0",
|
||||
"photoswipe": "^5.4.4",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss": "^8.5.2",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-preset-env": "^10.1.3",
|
||||
"postcss-reporter": "^7.1.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier": "^3.5.0",
|
||||
"pubsub-js": "^1.9.5",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"sass": "^1.83.4",
|
||||
"sass": "^1.84.0",
|
||||
"sass-loader": "^16.0.4",
|
||||
"server": "^1.0.41",
|
||||
"sockette": "^2.0.6",
|
||||
@@ -106,7 +107,7 @@
|
||||
"vue-sanitize-directive": "^0.2.1",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue3-gettext": "^2.4.0",
|
||||
"vuetify": "^3.7.9",
|
||||
"vuetify": "^3.7.11",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
@@ -114,7 +115,7 @@
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
"webpack-md5-hash": "^0.0.6",
|
||||
"webpack-merge": "^6.0.1",
|
||||
"webpack-plugin-vuetify": "^3.0.3"
|
||||
"webpack-plugin-vuetify": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0",
|
||||
@@ -125,7 +126,6 @@
|
||||
">0.25% and last 2 years"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vue/language-server": "^2.2.0",
|
||||
"eslint-plugin-vuetify": "^2.4.0"
|
||||
"@vue/language-server": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ Additional information can be found in our Developer Guide:
|
||||
|
||||
*/
|
||||
|
||||
import "common/debug";
|
||||
import "core-js/stable";
|
||||
import "regenerator-runtime/runtime";
|
||||
import "common/navigation";
|
||||
import $api from "common/api";
|
||||
import $notify from "common/notify";
|
||||
import $modal from "common/modal";
|
||||
import { $view } from "common/view";
|
||||
import { $lightbox } from "common/lightbox";
|
||||
import { PhotoClipboard } from "common/clipboard";
|
||||
import $event from "pubsub-js";
|
||||
import $log from "common/log";
|
||||
@@ -68,6 +70,8 @@ const $isMobile =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
|
||||
|
||||
window.$isMobile = $isMobile;
|
||||
|
||||
$config.progress(50);
|
||||
|
||||
$config.update().finally(() => {
|
||||
@@ -92,7 +96,8 @@ $config.update().finally(() => {
|
||||
// Assign helpers to VueJS prototype.
|
||||
app.config.globalProperties.$event = $event;
|
||||
app.config.globalProperties.$notify = $notify;
|
||||
app.config.globalProperties.$modal = $modal;
|
||||
app.config.globalProperties.$view = $view;
|
||||
app.config.globalProperties.$lightbox = $lightbox;
|
||||
app.config.globalProperties.$session = $session;
|
||||
app.config.globalProperties.$api = $api;
|
||||
app.config.globalProperties.$log = $log;
|
||||
@@ -159,13 +164,13 @@ $config.update().finally(() => {
|
||||
components.install(app);
|
||||
|
||||
// Make scroll-pos-restore compatible with bfcache (required to work in PWA mode on iOS).
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (event.persisted) {
|
||||
window.addEventListener("pagehide", (ev) => {
|
||||
if (ev.persisted) {
|
||||
localStorage.setItem("lastScrollPosBeforePageHide", JSON.stringify({ x: window.scrollX, y: window.scrollY }));
|
||||
}
|
||||
});
|
||||
window.addEventListener("pageshow", (event) => {
|
||||
if (event.persisted) {
|
||||
window.addEventListener("pageshow", (ev) => {
|
||||
if (ev.persisted) {
|
||||
const lastSavedScrollPos = localStorage.getItem("lastScrollPosBeforePageHide");
|
||||
if (lastSavedScrollPos !== undefined && lastSavedScrollPos !== null && lastSavedScrollPos !== "") {
|
||||
window.positionToRestore = JSON.parse(localStorage.getItem("lastScrollPosBeforePageHide"));
|
||||
@@ -214,8 +219,8 @@ $config.update().finally(() => {
|
||||
});
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if ($modal.active()) {
|
||||
// Disable navigation when a fullscreen dialog or viewer is open.
|
||||
if ($view.preventNavigation) {
|
||||
// Disable navigation when a fullscreen dialog or lightbox is open.
|
||||
return false;
|
||||
} else if (to.matched.some((record) => record.meta.settings) && $config.values.disable.settings) {
|
||||
return { name: "home" };
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</v-main>
|
||||
</v-app>
|
||||
|
||||
<p-viewer ref="viewer"></p-viewer>
|
||||
<p-dialogs></p-dialogs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ import Event from "pubsub-js";
|
||||
import PLoadingBar from "component/loading-bar.vue";
|
||||
import PNotify from "component/notify.vue";
|
||||
import PNavigation from "component/navigation.vue";
|
||||
import PViewer from "component/viewer.vue";
|
||||
import PDialogs from "component/dialogs.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
PLoadingBar,
|
||||
PNotify,
|
||||
PNavigation,
|
||||
PViewer,
|
||||
PDialogs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -48,19 +48,24 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("touchstart", this.onTouchStart.bind(this), { passive: true });
|
||||
window.addEventListener("touchmove", this.onTouchMove.bind(this), { passive: true });
|
||||
// TODO: Find a better solution that plays nice with modal dialogs.
|
||||
// document.addEventListener("touchstart", this.onTouchStart.bind(this), { passive: true });
|
||||
// document.addEventListener("touchmove", this.onTouchMove.bind(this), { passive: true });
|
||||
|
||||
this.subscriptions["view.refresh"] = Event.subscribe("view.refresh", (ev, data) => this.onRefresh(data));
|
||||
this.$config.setVuetify(this.$vuetify);
|
||||
},
|
||||
mounted() {
|
||||
this.$view.enter(this);
|
||||
},
|
||||
unmounted() {
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
Event.unsubscribe(this.subscriptions[i]);
|
||||
}
|
||||
|
||||
window.removeEventListener("touchstart", this.onTouchStart.bind(this), false);
|
||||
window.removeEventListener("touchmove", this.onTouchMove.bind(this), false);
|
||||
// TODO: Find a better solution that plays nice with modal dialogs.
|
||||
// document.removeEventListener("touchstart", this.onTouchStart.bind(this), false);
|
||||
// document.removeEventListener("touchmove", this.onTouchMove.bind(this), false);
|
||||
},
|
||||
methods: {
|
||||
onRefresh(config) {
|
||||
@@ -70,11 +75,11 @@ export default {
|
||||
this.touchStart = ev.touches[0].pageY;
|
||||
},
|
||||
onTouchMove(ev) {
|
||||
if (!this.touchStart || this.$modal.active()) {
|
||||
if (!this.touchStart /* || this.$view.isDialog() */) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't fire event when a dialog or the photo/video viewer is open.
|
||||
// Don't fire event when a dialog or the photo/video lightbox is open.
|
||||
if (document.querySelector(".v-overlay--active, .pswp--open") !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,21 +25,22 @@ Additional information can be found in our Developer Guide:
|
||||
|
||||
import * as media from "common/media";
|
||||
|
||||
// see https://tools.woolyss.com/html5-canplaytype-tester/
|
||||
export const useVideo = !!document.createElement("video").canPlayType;
|
||||
export const useAVC = useVideo // AVC
|
||||
export const useMp4Avc = useVideo // AVC
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4AvcMain)
|
||||
: false;
|
||||
export const useHEVC = useVideo // HEVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4HvcMain)
|
||||
export const useMp4Hvc = useVideo // HEVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4HvcMain10)
|
||||
: false;
|
||||
export const useHEV1 = useVideo // HEV1, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4HevMain)
|
||||
export const useMp4Hev = useVideo // HEV1, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4HevMain10)
|
||||
: false;
|
||||
export const useVVC = useVideo // VVC, Basic Support
|
||||
export const useMp4Vvc = useVideo // VVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4Vvc)
|
||||
: false;
|
||||
export const useOGV = useVideo // Ogg Theora
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeOgg)
|
||||
export const useMp4Evc = useVideo // EVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4Evc)
|
||||
: false;
|
||||
export const useWebM = useVideo // Google WebM
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeWebm)
|
||||
@@ -50,6 +51,15 @@ export const useVP8 = useVideo // Google WebM, VP8
|
||||
export const useVP9 = useVideo // Google WebM, VP9
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeWebmVp9)
|
||||
: false;
|
||||
export const useAV1 = useVideo // AV1, Main Profile
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeWebmAv1)
|
||||
export const useMp4Av1 = useVideo // AV1 in MP4, Main Profile 10-bit HDR
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMp4Av1Main10)
|
||||
: false;
|
||||
export const useWebmAv1 = useVideo // AV1 in WebM, Main Profile 10-bit HDR
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeWebmAv1Main10)
|
||||
: false;
|
||||
export const useMkvAv1 = useVideo // AV1 in MKV, Main Profile 10-bit HDR
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeMkvAv1Main10)
|
||||
: false;
|
||||
export const useTheora = useVideo // Ogg Theora
|
||||
? !!document.createElement("video").canPlayType(media.ContentTypeOgg)
|
||||
: false;
|
||||
|
||||
@@ -128,8 +128,11 @@ class PhotoSwipeDynamicCaption {
|
||||
if (slide.captionFadeTimeout) {
|
||||
clearTimeout(slide.captionFadeTimeout);
|
||||
}
|
||||
|
||||
slide.captionFadeTimeout = setTimeout(() => {
|
||||
captionElement.style.visibility = "hidden";
|
||||
if (captionElement) {
|
||||
captionElement.style.visibility = "hidden";
|
||||
}
|
||||
delete slide.captionFadeTimeout;
|
||||
}, 400);
|
||||
}
|
||||
@@ -150,9 +153,14 @@ class PhotoSwipeDynamicCaption {
|
||||
slide.dynamicCaption.hidden = false;
|
||||
captionElement.style.visibility = "visible";
|
||||
|
||||
clearTimeout(slide.captionFadeTimeout);
|
||||
if (slide.captionFadeTimeout) {
|
||||
clearTimeout(slide.captionFadeTimeout);
|
||||
}
|
||||
|
||||
slide.captionFadeTimeout = setTimeout(() => {
|
||||
captionElement.classList.remove("pswp__dynamic-caption--faded");
|
||||
if (captionElement) {
|
||||
captionElement.classList.remove("pswp__dynamic-caption--faded");
|
||||
}
|
||||
delete slide.captionFadeTimeout;
|
||||
}, 50);
|
||||
}
|
||||
@@ -162,7 +170,7 @@ class PhotoSwipeDynamicCaption {
|
||||
const isOnHorizontalEdge = x <= this.options.horizontalEdgeThreshold;
|
||||
captionEl.classList[isOnHorizontalEdge ? "add" : "remove"]("pswp__dynamic-caption--on-hor-edge");
|
||||
|
||||
if (document.dir === "rtl" && isOnHorizontalEdge) {
|
||||
if (document.dir === "rtl") {
|
||||
captionEl.style.right = x + "px";
|
||||
} else {
|
||||
captionEl.style.left = x + "px";
|
||||
@@ -304,18 +312,18 @@ class PhotoSwipeDynamicCaption {
|
||||
slide.panAreaSize.y -= captionHeight;
|
||||
this.recalculateZoomLevelAndBounds(slide);
|
||||
} else {
|
||||
// Lift up the image only by caption height
|
||||
// Lift the image by the height of the caption only.
|
||||
|
||||
// vertical ending of the image
|
||||
// Get vertical ending of the image.
|
||||
const verticalEnding = imageHeight + slide.bounds.center.y;
|
||||
|
||||
// height between bottom of the screen and ending of the image
|
||||
// (before any adjustments applied)
|
||||
// Get height between bottom of the screen and ending of the image,
|
||||
// before any adjustments applied.
|
||||
const verticalLeftover = slide.panAreaSize.y - verticalEnding;
|
||||
const initialPanAreaHeight = slide.panAreaSize.y;
|
||||
|
||||
if (verticalLeftover <= captionHeight) {
|
||||
// lift up the image to give more space for caption
|
||||
// Lift the image to make more room for the caption.
|
||||
slide.panAreaSize.y -= Math.min((captionHeight - verticalLeftover) * 2, captionHeight);
|
||||
|
||||
// we reduce viewport size, thus we need to update zoom level and pan bounds
|
||||
|
||||
@@ -150,7 +150,8 @@ export default class Config {
|
||||
return this.updating;
|
||||
}
|
||||
|
||||
this.updating = $api.get("config")
|
||||
this.updating = $api
|
||||
.get("config")
|
||||
.then(
|
||||
(resp) => {
|
||||
return this.setValues(resp.data);
|
||||
@@ -173,7 +174,7 @@ export default class Config {
|
||||
}
|
||||
|
||||
if (values.jsUri && this.values.jsUri !== values.jsUri) {
|
||||
$event.publish("dialog.reload", { values });
|
||||
$event.publish("dialog.update", { values });
|
||||
}
|
||||
|
||||
for (let key in values) {
|
||||
@@ -239,7 +240,12 @@ export default class Config {
|
||||
.filter((m) => m.UID === values.UID)
|
||||
.forEach((m) => {
|
||||
for (let key in values) {
|
||||
if (key !== "UID" && values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
|
||||
if (
|
||||
key !== "UID" &&
|
||||
values.hasOwnProperty(key) &&
|
||||
values[key] != null &&
|
||||
typeof values[key] !== "object"
|
||||
) {
|
||||
m[key] = values[key];
|
||||
}
|
||||
}
|
||||
@@ -539,10 +545,8 @@ export default class Config {
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = document.getElementsByTagName("html");
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
tags[0].setAttribute("data-color-mode", value);
|
||||
if (document.documentElement) {
|
||||
document.documentElement.setAttribute("data-color-mode", value);
|
||||
}
|
||||
|
||||
if (value === "dark") {
|
||||
|
||||
17
frontend/src/common/debug.js
Normal file
17
frontend/src/common/debug.js
Normal file
@@ -0,0 +1,17 @@
|
||||
let lastLog = Date.now();
|
||||
let log = console.log;
|
||||
|
||||
console.log = function () {
|
||||
if (!window.__CONFIG__?.debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (log && arguments.length) {
|
||||
const args = Array.from(arguments);
|
||||
if (typeof args[0] === "string") {
|
||||
args[0] += ` [${Date.now() - lastLog}ms]`;
|
||||
}
|
||||
log.apply(console, args);
|
||||
lastLog = Date.now();
|
||||
}
|
||||
};
|
||||
@@ -157,6 +157,28 @@ export class rules {
|
||||
return !isNaN(Number(v));
|
||||
}
|
||||
|
||||
static isNumberRange(v, min, max) {
|
||||
if (typeof v !== "string" || !v || v === "-1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
v = Number(v);
|
||||
|
||||
if (isNaN(v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof min === "number" && v < min) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof max === "number" && v > max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static isTime(v) {
|
||||
return /^(2[0-3]|[0-1][0-9])\D[0-5][0-9]\D[0-5][0-9]$/.test(v); // 23:59:59
|
||||
}
|
||||
@@ -188,10 +210,7 @@ export class rules {
|
||||
|
||||
static lat(required) {
|
||||
if (required) {
|
||||
return [
|
||||
(v) => !!v || $gettext("This field is required"),
|
||||
(v) => this.isLat(v) || $gettext("Invalid"),
|
||||
];
|
||||
return [(v) => !!v || $gettext("This field is required"), (v) => this.isLat(v) || $gettext("Invalid")];
|
||||
} else {
|
||||
return [(v) => this.isLat(v) || $gettext("Invalid")];
|
||||
}
|
||||
@@ -199,10 +218,7 @@ export class rules {
|
||||
|
||||
static lng(required) {
|
||||
if (required) {
|
||||
return [
|
||||
(v) => !!v || $gettext("This field is required"),
|
||||
(v) => this.isLng(v) || $gettext("Invalid"),
|
||||
];
|
||||
return [(v) => !!v || $gettext("This field is required"), (v) => this.isLng(v) || $gettext("Invalid")];
|
||||
} else {
|
||||
return [(v) => this.isLng(v) || $gettext("Invalid")];
|
||||
}
|
||||
@@ -296,10 +312,10 @@ export class rules {
|
||||
if (required) {
|
||||
return [
|
||||
(v) => !!v || Number(v) < -1 || $gettext("This field is required"),
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || (v >= 1 && v <= 31))) || $gettext("Invalid"),
|
||||
(v) => this.isNumberRange(v, 1, 31) || $gettext("Invalid"),
|
||||
];
|
||||
} else {
|
||||
return [(v) => !v || (this.isNumber(v) && (v === -1 || (v >= 1 && v <= 31))) || $gettext("Invalid")];
|
||||
return [(v) => this.isNumberRange(v, 1, 31) || $gettext("Invalid")];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,10 +323,10 @@ export class rules {
|
||||
if (required) {
|
||||
return [
|
||||
(v) => !!v || Number(v) < -1 || $gettext("This field is required"),
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || (v >= 1 && v <= 12))) || $gettext("Invalid"),
|
||||
(v) => this.isNumberRange(v, 1, 12) || $gettext("Invalid"),
|
||||
];
|
||||
} else {
|
||||
return [(v) => !v || (this.isNumber(v) && (v === -1 || (v >= 1 && v <= 12))) || $gettext("Invalid")];
|
||||
return [(v) => this.isNumberRange(v, 1, 12) || $gettext("Invalid")];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,14 +342,10 @@ export class rules {
|
||||
if (required) {
|
||||
return [
|
||||
(v) => !!v || Number(v) < -1 || $gettext("This field is required"),
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || v >= min)) || $gettext("Invalid"),
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || v <= max)) || $gettext("Invalid"),
|
||||
(v) => this.isNumberRange(v, min, max) || $gettext("Invalid"),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || v >= min)) || $gettext("Invalid"),
|
||||
(v) => !v || (this.isNumber(v) && (v === -1 || v <= max)) || $gettext("Invalid"),
|
||||
];
|
||||
return [(v) => this.isNumberRange(v, min, max) || $gettext("Invalid")];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ export class Input {
|
||||
return InputInvalid;
|
||||
}
|
||||
|
||||
if (Math.abs(this.touches[0].screenX - ev.changedTouches[0].screenX) > 4 || Math.abs(this.touches[0].screenY - ev.changedTouches[0].screenY) > 4) {
|
||||
if (
|
||||
Math.abs(this.touches[0].screenX - ev.changedTouches[0].screenX) > 4 ||
|
||||
Math.abs(this.touches[0].screenY - ev.changedTouches[0].screenY) > 4
|
||||
) {
|
||||
return InputInvalid;
|
||||
}
|
||||
}
|
||||
|
||||
18
frontend/src/common/lightbox.js
Normal file
18
frontend/src/common/lightbox.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import $event from "pubsub-js";
|
||||
|
||||
// Opens the lightbox dialog with the specified options.
|
||||
export class Lightbox {
|
||||
open(options) {
|
||||
$event.publish("lightbox.open", options);
|
||||
}
|
||||
|
||||
openModels(models, index) {
|
||||
$event.publish("lightbox.open", { models, index });
|
||||
}
|
||||
|
||||
openView(view, index) {
|
||||
$event.publish("lightbox.open", { view, index });
|
||||
}
|
||||
}
|
||||
|
||||
export const $lightbox = new Lightbox();
|
||||
@@ -14,7 +14,15 @@ const langFallbackDecorate = function (style, cfg) {
|
||||
|
||||
for (let i = layers.length - 1; i >= 0; i--) {
|
||||
let layer = layers[i];
|
||||
if (!(lf[0] === "in" && lfProp === "layout.text-field" && layer.layout && layer.layout["text-field"] && lfValues.indexOf(layer.layout["text-field"]) >= 0)) {
|
||||
if (
|
||||
!(
|
||||
lf[0] === "in" &&
|
||||
lfProp === "layout.text-field" &&
|
||||
layer.layout &&
|
||||
layer.layout["text-field"] &&
|
||||
lfValues.indexOf(layer.layout["text-field"]) >= 0
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let j = decorators.length - 1; j >= 0; j--) {
|
||||
@@ -87,19 +95,51 @@ maplibregl.Map.prototype.setLanguage = function (language, noAlt) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isNonlatin = ["ar", "hy", "be", "bg", "zh", "ka", "el", "he", "ja", "ja_kana", "kn", "kk", "ko", "mk", "ru", "sr", "th", "uk"].indexOf(language) >= 0;
|
||||
let isNonlatin =
|
||||
[
|
||||
"ar",
|
||||
"hy",
|
||||
"be",
|
||||
"bg",
|
||||
"zh",
|
||||
"ka",
|
||||
"el",
|
||||
"he",
|
||||
"ja",
|
||||
"ja_kana",
|
||||
"kn",
|
||||
"kk",
|
||||
"ko",
|
||||
"mk",
|
||||
"ru",
|
||||
"sr",
|
||||
"th",
|
||||
"uk",
|
||||
].indexOf(language) >= 0;
|
||||
|
||||
let style = JSON.parse(JSON.stringify(this.styleUndecorated));
|
||||
let langCfg = {
|
||||
"layer-filter": ["in", "layout.text-field", "{name}", "{name_de}", "{name_en}", "{name:latin}", "{name:latin} {name:nonlatin}", "{name:latin}\n{name:nonlatin}"],
|
||||
decorators: [
|
||||
"layer-filter": [
|
||||
"in",
|
||||
"layout.text-field",
|
||||
"{name}",
|
||||
"{name_de}",
|
||||
"{name_en}",
|
||||
"{name:latin}",
|
||||
"{name:latin} {name:nonlatin}",
|
||||
"{name:latin}\n{name:nonlatin}",
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"layout.text-field": isNonlatin ? "{name:nonlatin}" + (noAlt ? "" : "\n{name:latin}") : "{name:latin}" + (noAlt ? "" : "\n{name:nonlatin}"),
|
||||
"layout.text-field": isNonlatin
|
||||
? "{name:nonlatin}" + (noAlt ? "" : "\n{name:latin}")
|
||||
: "{name:latin}" + (noAlt ? "" : "\n{name:nonlatin}"),
|
||||
"filter-all-part": ["!has", "name:" + language],
|
||||
},
|
||||
{
|
||||
"layer-name-postfix": language,
|
||||
"layout.text-field": "{name:" + language + "}" + (noAlt ? "" : "\n{name:" + (isNonlatin ? "latin" : "nonlatin") + "}"),
|
||||
"layout.text-field":
|
||||
"{name:" + language + "}" + (noAlt ? "" : "\n{name:" + (isNonlatin ? "latin" : "nonlatin") + "}"),
|
||||
"filter-all-part": ["has", "name:" + language],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Media types.
|
||||
// Media types supported by PhotoPrism:
|
||||
export const Animated = "animated";
|
||||
export const Audio = "audio";
|
||||
export const Document = "document";
|
||||
@@ -9,53 +9,69 @@ export const Live = "live";
|
||||
export const Vector = "vector";
|
||||
export const Video = "video";
|
||||
|
||||
// Video codec names.
|
||||
//
|
||||
// Browser support can be tested by visiting one of the following sites:
|
||||
// - https://ott.dolby.com/codec_test/index.html
|
||||
// - https://dmnsgn.github.io/media-codecs/
|
||||
// - https://cconcolato.github.io/media-mime-support/
|
||||
// - https://thorium.rocks/misc/h265-tester.html
|
||||
export const CodecAvc = "avc1";
|
||||
// Video codec names, see https://mp4ra.org/registered-types/codecs:
|
||||
export const CodecAvc1 = "avc1";
|
||||
export const CodecAvc3 = "avc3";
|
||||
export const CodecHvc = "hvc1";
|
||||
export const CodecHev = "hev1";
|
||||
export const CodecVvc = "vvc1";
|
||||
export const CodecEvc = "evc1";
|
||||
export const CodecAvc4 = "avc4";
|
||||
export const CodecHvc1 = "hvc1";
|
||||
export const CodecHev1 = "hev1";
|
||||
export const CodecVvc1 = "vvc1";
|
||||
export const CodecEvc1 = "evc1";
|
||||
export const CodecTheora = "ogv";
|
||||
export const CodecVp8 = "vp8";
|
||||
export const CodecVp9 = "vp09";
|
||||
export const CodecVp08 = "vp08";
|
||||
export const CodecVp09 = "vp09";
|
||||
export const CodecAv1 = "av01";
|
||||
export const CodecAv1C = "av1c";
|
||||
|
||||
// Media file formats.
|
||||
// Video file formats:
|
||||
export const FormatMp4 = "mp4";
|
||||
export const FormatAvc = "avc";
|
||||
export const FormatHvc = "hvc";
|
||||
export const FormatHev = "hev";
|
||||
export const FormatVvc = "vvc";
|
||||
export const FormatEvc = "evc";
|
||||
export const FormatTheora = "ogg";
|
||||
export const FormatWebm = "webm";
|
||||
export const FormatVp8 = "vp8";
|
||||
export const FormatVp9 = "vp9";
|
||||
export const FormatAv1 = "av1";
|
||||
export const FormatWebmAv1 = "webm_av1";
|
||||
export const FormatMkvAv1 = "mkv_av1";
|
||||
export const FormatTheora = "ogg";
|
||||
export const FormatWebp = "webp";
|
||||
|
||||
// Image file formats:
|
||||
export const FormatJpeg = "jpg";
|
||||
export const FormatJpegXL = "jxl";
|
||||
export const FormatPng = "png";
|
||||
export const FormatGif = "gif";
|
||||
|
||||
// Vector file formats:
|
||||
export const FormatSVG = "svg";
|
||||
|
||||
// HTTP Content types (MIME).
|
||||
// Content type strings for common media formats, see https://tools.woolyss.com/html5-canplaytype-tester/:
|
||||
export const ContentTypeMp4 = "video/mp4";
|
||||
export const ContentTypeMp4AvcMain = ContentTypeMp4 + '; codecs="avc1.4d0028"'; // AVC High Profile Level 4
|
||||
export const ContentTypeMp4HvcMain = ContentTypeMp4 + '; codecs="hvc1.1.6.L93.B0"';
|
||||
export const ContentTypeMp4HvcMain10 = ContentTypeMp4 + '; codecs="hvc1.2.4.L153.B0"';
|
||||
export const ContentTypeMp4HevMain = ContentTypeMp4 + '; codecs="hev1.1.6.L93.B0"';
|
||||
export const ContentTypeMp4HevMain10 = ContentTypeMp4 + '; codecs="hev1.2.4.L153.B0'; // MPEG-4 HEVC Bitstream, Main 10 Profile, not supported on macOS
|
||||
export const ContentTypeMp4Vvc = ContentTypeMp4 + '; codecs="vvc1"';
|
||||
export const ContentTypeMp4Evc = ContentTypeMp4 + '; codecs="evc1"';
|
||||
export const ContentTypeMp4Av1 = ContentTypeMp4 + '; codecs="av01"'; // AV1 in MP4 container
|
||||
export const ContentTypeMp4Av1Main = ContentTypeMp4 + '; codecs="av01.0.08M.08"'; // AV1 Main Profile, level 4.0, High tier, 8 bits
|
||||
export const ContentTypeMp4Av1Main10 = ContentTypeMp4 + '; codecs="av01.0.08H.10"'; // AV1 Main Profile, level 4.0, High tier, 10 bits
|
||||
export const ContentTypeMp4Av1Main12 = ContentTypeMp4 + '; codecs="av01.0.08H.12"'; // AV1 Main Profile, level 4.0, High tier, 12 bits
|
||||
export const ContentTypeOgg = "video/ogg";
|
||||
export const ContentTypeOggTheora = ContentTypeOgg + '; codecs="theora, vorbis"';
|
||||
export const ContentTypeWebm = "video/webm";
|
||||
export const ContentTypeWebmVp8 = ContentTypeWebm + '; codecs="vp8"';
|
||||
export const ContentTypeWebmVp9 = ContentTypeWebm + '; codecs="vp09.00.10.08"';
|
||||
export const ContentTypeWebmAv1 = ContentTypeWebm + '; codecs="av01.2.10M.10"';
|
||||
export const ContentTypeWebmAv1 = ContentTypeWebm + '; codecs="av01"'; // AV1 in WebM container
|
||||
export const ContentTypeWebmAv1Main = ContentTypeWebm + '; codecs="av01.0.08M.08"'; // AV1 Main Profile, level 4.0, High tier, 8 bits
|
||||
export const ContentTypeWebmAv1Main10 = ContentTypeWebm + '; codecs="av01.0.08H.10"'; // AV1 Main Profile, level 4.0, High tier, 10 bits
|
||||
export const ContentTypeWebmAv1Main12 = ContentTypeWebm + '; codecs="av01.0.08H.12"'; // AV1 Main Profile, level 4.0, High tier, 12 bits
|
||||
export const ContentTypeMkv = "video/matroska";
|
||||
export const ContentTypeMkvAv1 = ContentTypeMkv + '; codecs="av01"'; // AV1 in MKV container
|
||||
export const ContentTypeMkvAv1Main = ContentTypeMkv + '; codecs="av01.0.08M.08"'; // AV1 Main Profile, level 4.0, High tier, 8 bits
|
||||
export const ContentTypeMkvAv1Main10 = ContentTypeMkv + '; codecs="av01.0.08H.10"'; // AV1 Main Profile, level 4.0, High tier, 10 bits
|
||||
export const ContentTypeMkvAv1Main12 = ContentTypeMkv + '; codecs="av01.0.08H.12"'; // AV1 Main Profile, level 4.0, High tier, 12 bits
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
|
||||
*/
|
||||
|
||||
let active = 0;
|
||||
let scrollbarHidden = document.body.classList.contains("hide-scrollbar");
|
||||
|
||||
const Modal = {
|
||||
html: function () {
|
||||
return document.getElementsByTagName("html")[0];
|
||||
},
|
||||
body: function () {
|
||||
return document.body;
|
||||
},
|
||||
preventDefault(ev) {
|
||||
if (ev && typeof ev.preventDefault === "function") {
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
update: function (preserveOverflow) {
|
||||
const htmlEl = this.html();
|
||||
const bodyEl = this.body();
|
||||
|
||||
if (!htmlEl || !bodyEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
if (!bodyEl.classList.contains("disable-gestures")) {
|
||||
bodyEl.classList.add("disable-gestures");
|
||||
document.addEventListener("touchmove", this.preventDefault, false);
|
||||
}
|
||||
} else if (bodyEl.classList.contains("disable-gestures")) {
|
||||
bodyEl.classList.remove("disable-gestures");
|
||||
document.removeEventListener("touchmove", this.preventDefault, false);
|
||||
}
|
||||
|
||||
if (this.scrollbarHidden()) {
|
||||
if (!preserveOverflow) {
|
||||
htmlEl.setAttribute("class", "overflow-y-hidden");
|
||||
}
|
||||
|
||||
if (!bodyEl.classList.contains("hide-scrollbar")) {
|
||||
bodyEl.classList.add("hide-scrollbar");
|
||||
}
|
||||
} else {
|
||||
htmlEl.removeAttribute("class");
|
||||
|
||||
if (bodyEl.classList.contains("hide-scrollbar")) {
|
||||
bodyEl.classList.remove("hide-scrollbar");
|
||||
}
|
||||
}
|
||||
},
|
||||
leave: function () {
|
||||
if (active > 0) {
|
||||
active--;
|
||||
}
|
||||
|
||||
this.update();
|
||||
},
|
||||
enter: function (preserveOverflow) {
|
||||
active++;
|
||||
|
||||
this.update(preserveOverflow);
|
||||
},
|
||||
active: function () {
|
||||
return active > 0;
|
||||
},
|
||||
scrollbarHidden: function () {
|
||||
return this.active() || scrollbarHidden;
|
||||
},
|
||||
};
|
||||
|
||||
Modal.update();
|
||||
|
||||
export default Modal;
|
||||
@@ -61,7 +61,8 @@ export function restart(uri) {
|
||||
$notify.wait();
|
||||
$notify.ajaxStart();
|
||||
|
||||
return $api.post("server/stop")
|
||||
return $api
|
||||
.post("server/stop")
|
||||
.then(() => {
|
||||
return poll(1000, 180)
|
||||
.then(() => {
|
||||
|
||||
@@ -269,7 +269,7 @@ export default class Util {
|
||||
return "GIF";
|
||||
case "dng":
|
||||
return "Adobe Digital Negative";
|
||||
case media.CodecAvc:
|
||||
case media.CodecAvc1:
|
||||
case media.FormatAvc:
|
||||
return "Advanced Video Coding (AVC) / H.264";
|
||||
case media.CodecAvc3:
|
||||
@@ -280,14 +280,14 @@ export default class Util {
|
||||
return "AVIF Image Sequence";
|
||||
case "hev":
|
||||
case "hvc":
|
||||
case media.CodecHvc:
|
||||
case media.CodecHvc1:
|
||||
case media.FormatHvc:
|
||||
return "High Efficiency Video Coding (HEVC) / H.265";
|
||||
case media.CodecHev:
|
||||
case media.CodecHev1:
|
||||
case media.FormatHev:
|
||||
return "High Efficiency Video Coding (HEVC) Bitstream";
|
||||
case media.FormatEvc:
|
||||
case media.CodecEvc:
|
||||
case media.CodecEvc1:
|
||||
return "Essential Video Coding (MPEG-5 Part 1)";
|
||||
case "m4v":
|
||||
return "Apple iTunes Multimedia Container";
|
||||
@@ -301,10 +301,10 @@ export default class Util {
|
||||
return "Google WebP";
|
||||
case media.FormatWebm:
|
||||
return "Google WebM";
|
||||
case media.CodecVp8:
|
||||
case media.CodecVp08:
|
||||
case media.FormatVp8:
|
||||
return "Google VP8";
|
||||
case media.CodecVp9:
|
||||
case media.CodecVp09:
|
||||
case media.FormatVp9:
|
||||
return "Google VP9";
|
||||
case "flv":
|
||||
@@ -362,28 +362,29 @@ export default class Util {
|
||||
case media.CodecAv1C:
|
||||
case media.CodecAv1:
|
||||
return "AV1";
|
||||
case media.CodecAvc:
|
||||
case media.CodecAvc1:
|
||||
case media.CodecAvc3:
|
||||
case media.CodecAvc4:
|
||||
case media.FormatAvc:
|
||||
return "AVC";
|
||||
case "hvc":
|
||||
case media.CodecHev:
|
||||
case media.CodecHev1:
|
||||
case media.FormatHev:
|
||||
case media.CodecHvc:
|
||||
case media.CodecHvc1:
|
||||
case media.FormatHvc:
|
||||
return "HEVC";
|
||||
case media.CodecVvc:
|
||||
case media.CodecVvc1:
|
||||
case media.FormatVvc:
|
||||
return "VVC";
|
||||
case media.CodecEvc:
|
||||
case media.CodecEvc1:
|
||||
case media.FormatEvc:
|
||||
return "EVC";
|
||||
case media.FormatWebm:
|
||||
return "WebM";
|
||||
case media.CodecVp8:
|
||||
case media.CodecVp08:
|
||||
case media.FormatVp8:
|
||||
return "VP8";
|
||||
case media.CodecVp9:
|
||||
case media.CodecVp09:
|
||||
case media.FormatVp9:
|
||||
return "VP9";
|
||||
case "extended webp":
|
||||
@@ -407,23 +408,23 @@ export default class Util {
|
||||
case "qt ":
|
||||
return "Apple QuickTime (MOV)";
|
||||
case "avc":
|
||||
case media.CodecAvc:
|
||||
case media.CodecAvc1:
|
||||
return "Advanced Video Coding (AVC) / H.264";
|
||||
case media.CodecAvc3:
|
||||
return "Advanced Video Coding (AVC) Bitstream";
|
||||
case "hvc":
|
||||
case "hev":
|
||||
case media.CodecHvc:
|
||||
case media.CodecHvc1:
|
||||
case media.FormatHvc:
|
||||
return "High Efficiency Video Coding (HEVC) / H.265";
|
||||
case media.CodecHev:
|
||||
case media.CodecHev1:
|
||||
case media.FormatHev:
|
||||
return "High Efficiency Video Coding (HEVC) Bitstream";
|
||||
case media.FormatVvc:
|
||||
case media.CodecVvc:
|
||||
case media.CodecVvc1:
|
||||
return "Versatile Video Coding (VVC) / H.266";
|
||||
case media.FormatEvc:
|
||||
case media.CodecEvc:
|
||||
case media.CodecEvc1:
|
||||
return "Essential Video Coding (MPEG-5 Part 1)";
|
||||
case "av1":
|
||||
case "av1c":
|
||||
@@ -576,31 +577,28 @@ export default class Util {
|
||||
static videoFormat(codec, mime) {
|
||||
if ((!codec && !mime) || mime?.startsWith('video/mp4; codecs="avc')) {
|
||||
return media.FormatAvc;
|
||||
} else if (can.useHEVC && (codec === media.CodecHvc || mime?.startsWith('video/mp4; codecs="hvc'))) {
|
||||
return media.FormatHvc;
|
||||
} else if (can.useHEV1 && (codec === media.CodecHev || mime?.startsWith('video/mp4; codecs="hev'))) {
|
||||
return media.FormatHev; // HEVC Bitstream
|
||||
} else if (
|
||||
can.useVVC &&
|
||||
(codec === media.CodecVvc || codec === media.FormatVvc || mime?.startsWith('video/mp4; codecs="vvc'))
|
||||
) {
|
||||
} else if (can.useMp4Hvc && (codec === media.CodecHvc1 || mime?.startsWith('video/mp4; codecs="hvc'))) {
|
||||
return media.FormatHvc; // HEVC video with parameter sets not in the Samples
|
||||
} else if (can.useMp4Hev && (codec === media.CodecHev1 || mime?.startsWith('video/mp4; codecs="hev'))) {
|
||||
return media.FormatHev; // HEVC video with parameter sets also in the Samples, won't play on macOS
|
||||
} else if (can.useMp4Vvc && (codec === media.CodecVvc1 || mime?.startsWith('video/mp4; codecs="vvc'))) {
|
||||
return media.FormatVvc;
|
||||
} else if (can.useOGV && (codec === media.CodecTheora || codec === media.FormatTheora || mime === media.ContentTypeOgg)) {
|
||||
return media.FormatTheora;
|
||||
} else if (can.useVP8 && (codec === "vp8" || codec === "vp08" || mime?.startsWith('video/mp4; codecs="vp8'))) {
|
||||
} else if (can.useMp4Evc && (codec === media.CodecEvc1 || mime?.startsWith('video/mp4; codecs="evc'))) {
|
||||
return media.FormatEvc;
|
||||
} else if (can.useVP8 && (codec === media.CodecVp08 || mime?.startsWith('video/mp4; codecs="vp8'))) {
|
||||
return media.FormatVp8;
|
||||
} else if (can.useVP9 && (codec === "vp9" || codec === "vp09" || mime?.startsWith('video/mp4; codecs="vp09'))) {
|
||||
} else if (can.useVP9 && (codec === media.CodecVp09 || mime?.startsWith('video/mp4; codecs="vp09'))) {
|
||||
return media.FormatVp9;
|
||||
} else if (
|
||||
can.useAV1 &&
|
||||
(codec === media.CodecAv1 ||
|
||||
codec === media.CodecAv1C ||
|
||||
codec === media.FormatAv1 ||
|
||||
mime?.startsWith('video/webm; codecs="av01'))
|
||||
) {
|
||||
} else if (can.useMp4Av1 && (mime?.startsWith('video/mp4; codecs="av01') || mime?.startsWith("video/AV1"))) {
|
||||
return media.FormatAv1;
|
||||
} else if (can.useWebmAv1 && mime?.startsWith('video/webm; codecs="av01')) {
|
||||
return media.FormatWebmAv1;
|
||||
} else if (can.useMkvAv1 && mime?.startsWith('video/matroska; codecs="av01')) {
|
||||
return media.FormatMkvAv1;
|
||||
} else if (can.useWebM && (codec === media.FormatWebm || mime === media.ContentTypeWebm)) {
|
||||
return media.FormatWebm;
|
||||
} else if (can.useTheora && (codec === media.CodecTheora || mime === media.ContentTypeOgg)) {
|
||||
return media.FormatTheora;
|
||||
}
|
||||
|
||||
return media.FormatAvc;
|
||||
@@ -626,28 +624,24 @@ export default class Util {
|
||||
switch (this.videoFormat(codec, mime)) {
|
||||
case media.FormatAvc:
|
||||
return media.ContentTypeMp4AvcMain;
|
||||
case media.CodecTheora:
|
||||
return media.ContentTypeOgg;
|
||||
case media.CodecVp8:
|
||||
case media.FormatVp8:
|
||||
return media.ContentTypeWebmVp8;
|
||||
case media.CodecVp9:
|
||||
case media.FormatVp9:
|
||||
return media.ContentTypeWebmVp9;
|
||||
case media.CodecAv1C:
|
||||
case media.CodecAv1:
|
||||
case media.FormatAv1:
|
||||
return media.ContentTypeWebmAv1;
|
||||
case media.FormatWebm:
|
||||
return media.ContentTypeWebm;
|
||||
case media.CodecHvc:
|
||||
case media.FormatHvc:
|
||||
return media.ContentTypeMp4HvcMain;
|
||||
case media.CodecHev:
|
||||
case media.FormatHev:
|
||||
return media.ContentTypeMp4HevMain;
|
||||
case media.FormatVvc:
|
||||
return media.ContentTypeMp4Vvc;
|
||||
case media.FormatVp8:
|
||||
return media.ContentTypeWebmVp8;
|
||||
case media.FormatVp9:
|
||||
return media.ContentTypeWebmVp9;
|
||||
case media.FormatWebmAv1:
|
||||
return media.ContentTypeWebmAv1Main10;
|
||||
case media.FormatMkvAv1:
|
||||
return media.ContentTypeMkvAv1Main10;
|
||||
case media.FormatWebm:
|
||||
return media.ContentTypeWebm;
|
||||
case media.FormatTheora:
|
||||
return media.ContentTypeOgg;
|
||||
default:
|
||||
return "video/mp4";
|
||||
}
|
||||
|
||||
331
frontend/src/common/view.js
Normal file
331
frontend/src/common/view.js
Normal file
@@ -0,0 +1,331 @@
|
||||
import { toRaw } from "vue";
|
||||
|
||||
const TouchStartEvent = "touchstart";
|
||||
const TouchMoveEvent = "touchmove";
|
||||
|
||||
// If true, logging is enabled.
|
||||
const debug = window.__CONFIG__?.debug;
|
||||
|
||||
// Returns the <html> element.
|
||||
export function getHtmlElement() {
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
// Initializes the <html> element by removing the "class" attribute.
|
||||
export function initHtmlElement() {
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
if (htmlElement && htmlElement.hasAttribute("class")) {
|
||||
if (debug) {
|
||||
console.log(`html: removed class="${htmlElement.getAttribute("class")}"`);
|
||||
}
|
||||
|
||||
// Remove the class="loading" attribute from <html> when the application has loaded.
|
||||
htmlElement.removeAttribute("class");
|
||||
htmlElement.setAttribute("style", "");
|
||||
|
||||
// If requested, hide the scrollbar permanently by adding class="hide-scrollbar" to <html>.
|
||||
if (document.body.classList.contains("hide-scrollbar")) {
|
||||
htmlElement.setAttribute("class", "hide-scrollbar");
|
||||
|
||||
if (debug) {
|
||||
console.log('html: added class="hide-scrollbar" to permanently hide the scrollbar');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set a :root style variable, or removes it if the value is empty.
|
||||
export function setHtmlStyle(key, value) {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const htmlElement = getHtmlElement();
|
||||
|
||||
if (!htmlElement) {
|
||||
return false;
|
||||
} else if (value) {
|
||||
htmlElement.style.setProperty(key, value);
|
||||
} else {
|
||||
htmlElement.style.removeProperty(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns the <body>> element.
|
||||
export function getBodyElement() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
// Checks if the element is a button.
|
||||
export function isInputElement(el) {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return el instanceof HTMLButtonElement;
|
||||
}
|
||||
|
||||
// Checks if the element is an image, video, or canvas.
|
||||
export function isMediaElement(el) {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return el instanceof HTMLImageElement || el instanceof HTMLVideoElement || el instanceof HTMLCanvasElement;
|
||||
}
|
||||
|
||||
// Prevents the default navigation touch gestures.
|
||||
export function preventNavigationTouchEvent(ev) {
|
||||
if (ev instanceof TouchEvent && ev.cancelable) {
|
||||
// console.log(`${ev.type} @ ${ev.touches[0].clientX.toString()} x ${ev.touches[0].clientY.toString()}`, ev.target);
|
||||
if (ev.type === TouchStartEvent && (isMediaElement(ev.target) || ev.touches[0].clientX <= 30)) {
|
||||
if (window.innerHeight - ev.touches[0].clientY > 128 || ev.touches[0].clientX <= 30) {
|
||||
ev.preventDefault();
|
||||
// console.log(`prevented ${ev.type} @ ${ev.touches[0].clientX.toString()} x ${ev.touches[0].clientY.toString()}`);
|
||||
}
|
||||
} else if (ev.type === TouchMoveEvent && !isInputElement(ev.target)) {
|
||||
ev.preventDefault();
|
||||
// console.log(`prevented ${ev.type} @ ${ev.touches[0].clientX.toString()} x ${ev.touches[0].clientY.toString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a random string that can be used as an identifier.
|
||||
export function generateRandomId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2, 18);
|
||||
}
|
||||
|
||||
// View keeps track of the visible components and dialogs,
|
||||
// and updates the window and <html> body as needed.
|
||||
export class View {
|
||||
// Initializes the instance properties with the default values.
|
||||
constructor() {
|
||||
this.uid = 0;
|
||||
this.scopes = [];
|
||||
this.preventNavigation = false;
|
||||
}
|
||||
|
||||
// Changes the view context to the specified component,
|
||||
// and updates the window and <html> body as needed.
|
||||
enter(c) {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRoot()) {
|
||||
initHtmlElement();
|
||||
}
|
||||
|
||||
this.scopes.push(c);
|
||||
|
||||
this.apply(c);
|
||||
|
||||
return this.scopes.length;
|
||||
}
|
||||
|
||||
// Returns to the parent view context of the specified component,
|
||||
// and updates the window and <html> body as needed.
|
||||
leave(c) {
|
||||
if (this.scopes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (c) {
|
||||
const i = this.scopes.findLastIndex((s) => s === c);
|
||||
if (i > 0) {
|
||||
this.scopes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scopes.length) {
|
||||
this.apply(this.scopes[this.scopes.length - 1]);
|
||||
}
|
||||
|
||||
return this.scopes.length;
|
||||
}
|
||||
|
||||
// Updates the window and the <html> body elements based on the specified component.
|
||||
apply(c) {
|
||||
if (!c || typeof c !== "object" || !Number.isInteger(c?.$?.uid)) {
|
||||
console.log(`view: invalid component passed to apply (#${this.uid.toString()})`, c);
|
||||
return;
|
||||
}
|
||||
|
||||
const htmlEl = getHtmlElement();
|
||||
|
||||
if (!htmlEl) {
|
||||
console.log(`view: failed to get HTML element (#${this.uid.toString()})`, c);
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyEl = getBodyElement();
|
||||
|
||||
if (!bodyEl) {
|
||||
console.log(`view: failed to get BODY element (#${this.uid.toString()})`, c);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the component's numeric unique ID, if any.
|
||||
const uid = c.$.uid;
|
||||
|
||||
// Return, as it should not be necessary to apply the same state twice.
|
||||
if (this.uid === uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = c?.$options?.name ? c.$options.name : "";
|
||||
|
||||
let hideScrollbar = false;
|
||||
let disableScrolling = false;
|
||||
let disableNavigationGestures = false;
|
||||
let preventNavigation = uid > 0 && !name.startsWith("PPage");
|
||||
|
||||
switch (name) {
|
||||
case "PPagePlaces":
|
||||
hideScrollbar = true;
|
||||
break;
|
||||
case "PPageLogin":
|
||||
hideScrollbar = true;
|
||||
preventNavigation = true;
|
||||
break;
|
||||
case "PPhotoEditDialog":
|
||||
hideScrollbar = window.innerWidth < 960;
|
||||
disableScrolling = true;
|
||||
preventNavigation = true;
|
||||
break;
|
||||
case "PPhotoUploadDialog":
|
||||
hideScrollbar = window.innerWidth < 1280;
|
||||
disableScrolling = true;
|
||||
preventNavigation = true;
|
||||
break;
|
||||
case "PLightbox":
|
||||
hideScrollbar = true;
|
||||
disableScrolling = true;
|
||||
disableNavigationGestures = true;
|
||||
preventNavigation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.preventNavigation = preventNavigation;
|
||||
|
||||
if (debug && name && uid) {
|
||||
const scope = this.scopes.map((s) => `${s?.$options?.name} #${s?.$?.uid.toString()}`).join(" › ");
|
||||
console.log(`view: ${scope}`, toRaw(c?.$data));
|
||||
}
|
||||
|
||||
if (hideScrollbar) {
|
||||
if (!bodyEl.classList.contains("hide-scrollbar")) {
|
||||
bodyEl.classList.add("hide-scrollbar");
|
||||
setHtmlStyle("scrollbar-width", "none");
|
||||
setHtmlStyle("overflow-y", "hidden");
|
||||
if (debug) {
|
||||
console.log(`html: added style="scrollbar-width: none; overflow-y: hidden;"`);
|
||||
}
|
||||
}
|
||||
} else if (bodyEl.classList.contains("hide-scrollbar")) {
|
||||
bodyEl.classList.remove("hide-scrollbar");
|
||||
setHtmlStyle("scrollbar-width");
|
||||
setHtmlStyle("overflow-y");
|
||||
if (debug) {
|
||||
console.log(`html: removed style="scrollbar-width: none; overflow-y: hidden;"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (disableScrolling) {
|
||||
if (!bodyEl.classList.contains("disable-scrolling")) {
|
||||
bodyEl.classList.add("disable-scrolling");
|
||||
if (debug) {
|
||||
console.log(`body: added class="disable-scrolling"`);
|
||||
}
|
||||
}
|
||||
} else if (bodyEl.classList.contains("disable-scrolling")) {
|
||||
bodyEl.classList.remove("disable-scrolling");
|
||||
if (debug) {
|
||||
console.log(`body: removed class="disable-scrolling"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (disableNavigationGestures) {
|
||||
if (!bodyEl.classList.contains("disable-navigation-gestures")) {
|
||||
bodyEl.classList.add("disable-navigation-gestures");
|
||||
window.addEventListener(TouchStartEvent, preventNavigationTouchEvent, { passive: false });
|
||||
window.addEventListener(TouchMoveEvent, preventNavigationTouchEvent, { passive: false });
|
||||
if (debug) {
|
||||
console.log(`view: disabled touch navigation gestures`);
|
||||
}
|
||||
}
|
||||
} else if (bodyEl.classList.contains("disable-navigation-gestures")) {
|
||||
bodyEl.classList.remove("disable-navigation-gestures");
|
||||
window.removeEventListener(TouchStartEvent, preventNavigationTouchEvent, false);
|
||||
window.removeEventListener(TouchMoveEvent, preventNavigationTouchEvent, false);
|
||||
if (debug) {
|
||||
console.log(`view: re-enabled touch navigation gestures`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the currently active view component or null if none exists.
|
||||
current() {
|
||||
if (this.scopes.length) {
|
||||
return this.scopes[this.scopes.length - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the currently active view data or an empty reactive object otherwise.
|
||||
data() {
|
||||
const c = this.current();
|
||||
|
||||
if (c && c.$data) {
|
||||
return c.$data;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the specified view component is currently inactive, e.g. hidden in the background.
|
||||
isHidden(c) {
|
||||
return !this.isVisible(c);
|
||||
}
|
||||
|
||||
// Returns true if the specified view component is currently active, e.g. visible in the foreground.
|
||||
isVisible(c) {
|
||||
if (!c || this.isApp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const context = this.scopes[this.scopes.length - 1];
|
||||
|
||||
if (typeof c === "object") {
|
||||
return c === context;
|
||||
} else if (typeof c === "string") {
|
||||
return context?.$options?.name === c;
|
||||
} else if (typeof c === "number") {
|
||||
return context?.$?.uid === c;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if no view is currently active.
|
||||
isRoot() {
|
||||
return !this.scopes.length;
|
||||
}
|
||||
|
||||
// Returns true if no view or the main view of the app is currently active.
|
||||
isApp() {
|
||||
if (this.isRoot()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const c = this.scopes[this.scopes.length - 1];
|
||||
|
||||
return c?.$options?.name === "App" || c?.$?.uid === 0;
|
||||
}
|
||||
}
|
||||
|
||||
// $view is the default View instance.
|
||||
export const $view = new View();
|
||||
@@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<footer class="p-about-footer text-ltr">
|
||||
<p class="flex-fill text-sm-start">
|
||||
<div class="flex-fill text-sm-start">
|
||||
<strong>
|
||||
<router-link to="/about" class="text-link text-selectable">{{ about }} {{ getMembership() }}</router-link>
|
||||
</strong>
|
||||
<span class="body-link text-selectable">
|
||||
<span class="cursor-text" @click.stop.prevent="$util.copyText(about, version)">Build</span>
|
||||
<a href="https://docs.photoprism.app/release-notes/" target="_blank" :title="version" class="body-link">{{
|
||||
build
|
||||
}}</a>
|
||||
<a
|
||||
v-tooltip="version"
|
||||
href="https://docs.photoprism.app/release-notes/"
|
||||
target="_blank"
|
||||
class="body-link text-truncate"
|
||||
>{{ build }}</a
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p class="hidden-xs text-sm-end">
|
||||
</div>
|
||||
<div class="hidden-xs text-sm-end">
|
||||
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE" target="_blank" class="text-link"
|
||||
>3rd-party software packages</a
|
||||
>
|
||||
<a href="https://www.photoprism.app/about/team/" target="_blank" class="body-link">© 2018-2025 PhotoPrism UG</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -24,15 +28,12 @@
|
||||
export default {
|
||||
name: "PAboutFooter",
|
||||
data() {
|
||||
const ver = this.$config.getVersion().split("-");
|
||||
const build = ver.slice(0, 2).join("-");
|
||||
const about = this.$config.getAbout();
|
||||
const membership = this.$config.getMembership();
|
||||
const customer = this.$config.getCustomer();
|
||||
|
||||
return {
|
||||
rtl: this.$rtl,
|
||||
build: build,
|
||||
about: about,
|
||||
membership: membership,
|
||||
customer: customer,
|
||||
@@ -40,6 +41,15 @@ export default {
|
||||
isDemo: this.$config.isDemo(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
build() {
|
||||
if (this.$vuetify.display.xs) {
|
||||
return this.$config.getVersion().split("-").slice(0, 1).join("-");
|
||||
} else {
|
||||
return this.$config.getVersion().split("-").slice(0, 2).join("-");
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getMembership() {
|
||||
if (this.isDemo) {
|
||||
|
||||
@@ -91,12 +91,12 @@
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<p-photo-album-dialog
|
||||
:show="dialog.album"
|
||||
:visible="dialog.album"
|
||||
@close="dialog.album = false"
|
||||
@confirm="cloneAlbums"
|
||||
></p-photo-album-dialog>
|
||||
<p-album-delete-dialog
|
||||
:show="dialog.delete"
|
||||
:visible="dialog.delete"
|
||||
@close="dialog.delete = false"
|
||||
@confirm="batchDelete"
|
||||
></p-album-delete-dialog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-album-delete-dialog" @keydown.esc="close">
|
||||
<v-dialog :model-value="visible" persistent max-width="350" class="p-dialog p-album-delete-dialog" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon icon="mdi-delete-outline" size="54" color="primary"></v-icon>
|
||||
@@ -20,7 +20,7 @@
|
||||
export default {
|
||||
name: "PAlbumDeleteDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="500"
|
||||
class="dialog-album-edit"
|
||||
@@ -119,7 +119,7 @@ import Album from "model/album";
|
||||
export default {
|
||||
name: "PAlbumEditDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
album: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -149,10 +149,13 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.model = this.album.clone();
|
||||
this.category = this.model.Category ? this.model.Category : null;
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -86,19 +86,19 @@
|
||||
</div>
|
||||
|
||||
<p-share-dialog
|
||||
:show="dialog.share"
|
||||
:visible="dialog.share"
|
||||
:model="album"
|
||||
@upload="webdavUpload"
|
||||
@close="dialog.share = false"
|
||||
></p-share-dialog>
|
||||
<p-service-upload
|
||||
:show="dialog.upload"
|
||||
:visible="dialog.upload"
|
||||
:items="{ albums: album.getId() }"
|
||||
:model="album"
|
||||
@close="dialog.upload = false"
|
||||
@confirm="dialog.upload = false"
|
||||
></p-service-upload>
|
||||
<p-album-edit-dialog :show="dialog.edit" :album="album" @close="dialog.edit = false"></p-album-edit-dialog>
|
||||
<p-album-edit-dialog :visible="dialog.edit" :album="album" @close="dialog.edit = false"></p-album-edit-dialog>
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@@ -4,7 +4,7 @@ import PScroll from "component/scroll.vue";
|
||||
import PNavigation from "component/navigation.vue";
|
||||
import PUpdate from "component/update.vue";
|
||||
import PLoadingBar from "component/loading-bar.vue";
|
||||
import PViewer from "component/viewer.vue";
|
||||
import PLightbox from "component/lightbox.vue";
|
||||
|
||||
// Icons.
|
||||
import IconLivePhoto from "component/icon/live-photo.vue";
|
||||
@@ -75,7 +75,7 @@ export function install(app) {
|
||||
app.component("PScroll", PScroll);
|
||||
app.component("PNavigation", PNavigation);
|
||||
app.component("PLoadingBar", PLoadingBar);
|
||||
app.component("PViewer", PViewer);
|
||||
app.component("PLightbox", PLightbox);
|
||||
app.component("PUpdate", PUpdate);
|
||||
|
||||
app.component("IconLivePhoto", IconLivePhoto);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-confirm-action" @keydown.esc="close">
|
||||
<v-dialog :model-value="visible" persistent max-width="350" class="p-dialog p-confirm-action" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon :icon="icon" :size="iconSize" color="primary"></v-icon>
|
||||
@@ -20,7 +20,7 @@
|
||||
export default {
|
||||
name: "PConfirmAction",
|
||||
props: {
|
||||
show: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="575"
|
||||
class="p-dialog modal-dialog sponsor-dialog"
|
||||
@@ -49,7 +49,7 @@
|
||||
export default {
|
||||
name: "PDialogSponsor",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -107,6 +107,7 @@ export default {
|
||||
color: "background",
|
||||
flat: true,
|
||||
ripple: false,
|
||||
transition: false,
|
||||
},
|
||||
VTab: {
|
||||
color: "on-surface",
|
||||
@@ -126,6 +127,7 @@ export default {
|
||||
},
|
||||
VToolbar: {
|
||||
flat: true,
|
||||
transition: false,
|
||||
},
|
||||
VListItem: {
|
||||
ripple: false,
|
||||
@@ -135,11 +137,21 @@ export default {
|
||||
itemsPerPage: -1,
|
||||
hover: true,
|
||||
},
|
||||
VImg: {
|
||||
transition: false,
|
||||
},
|
||||
VDialog: {
|
||||
scrim: true,
|
||||
scrollable: true,
|
||||
retainFocus: true,
|
||||
transition: false,
|
||||
persistent: true,
|
||||
attach: document.body,
|
||||
},
|
||||
VOverlay: {
|
||||
scrim: true,
|
||||
transition: false,
|
||||
attach: document.body,
|
||||
},
|
||||
VExpansionPanel: {
|
||||
tile: true,
|
||||
|
||||
121
frontend/src/component/dialogs.vue
Normal file
121
frontend/src/component/dialogs.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div id="p-dialogs">
|
||||
<p-photo-upload-dialog
|
||||
:visible="upload.visible"
|
||||
:data="upload.data"
|
||||
@close="upload.visible = false"
|
||||
@confirm="upload.visible = false"
|
||||
></p-photo-upload-dialog>
|
||||
<p-photo-edit-dialog
|
||||
:visible="edit.visible"
|
||||
:selection="edit.selection"
|
||||
:index="edit.index"
|
||||
:album="edit.album"
|
||||
:tab="edit.tab"
|
||||
@close="edit.visible = false"
|
||||
></p-photo-edit-dialog>
|
||||
<p-update :visible="update.visible" @close="update.visible = false"></p-update>
|
||||
<p-lightbox></p-lightbox>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Album from "model/album";
|
||||
|
||||
import PPhotoUploadDialog from "component/photo/upload/dialog.vue";
|
||||
import PPhotoEditDialog from "component/photo/edit/dialog.vue";
|
||||
import PUpdate from "component/update.vue";
|
||||
import PLightbox from "component/lightbox.vue";
|
||||
|
||||
export default {
|
||||
name: "PDialogs",
|
||||
components: {
|
||||
PPhotoEditDialog,
|
||||
PPhotoUploadDialog,
|
||||
PUpdate,
|
||||
PLightbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
update: {
|
||||
visible: false,
|
||||
},
|
||||
upload: {
|
||||
visible: false,
|
||||
data: {},
|
||||
},
|
||||
edit: {
|
||||
visible: false,
|
||||
album: null,
|
||||
selection: [],
|
||||
index: 0,
|
||||
tab: "",
|
||||
},
|
||||
subscriptions: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// Opens the web upload dialog.
|
||||
this.subscriptions.push(
|
||||
this.$event.subscribe("dialog.upload", (ev, data) => {
|
||||
this.openUpload(data);
|
||||
})
|
||||
);
|
||||
|
||||
// Opens the photo edit dialog.
|
||||
this.subscriptions.push(
|
||||
this.$event.subscribe("dialog.edit", (ev, data) => {
|
||||
if (this.hasAuth() && !this.edit.visible) {
|
||||
this.edit.visible = true;
|
||||
this.edit.index = data.index;
|
||||
this.edit.selection = data.selection;
|
||||
this.edit.album = data.album;
|
||||
this.edit.tab = data?.tab ? data.tab : "";
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Opens the update dialog so that users can reload the UI after updates.
|
||||
this.subscriptions.push(
|
||||
this.$event.subscribe("dialog.update", () => {
|
||||
if (!this.update.visible) {
|
||||
this.update.visible = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
this.$event.unsubscribe(this.subscriptions[i]);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasAuth() {
|
||||
return this.$session.auth || this.isPublic;
|
||||
},
|
||||
isReadOnly() {
|
||||
return this.$config.get("readonly");
|
||||
},
|
||||
openUpload(data) {
|
||||
if (this.upload.visible || !this.hasAuth() || this.isReadOnly() || !this.$config.feature("upload")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$route.name === "album" && this.$route.params?.album) {
|
||||
return new Album()
|
||||
.find(this.$route.params?.album)
|
||||
.then((m) => {
|
||||
this.upload.visible = true;
|
||||
this.upload.data = Object.assign({ albums: [m] }, data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.upload.visible = true;
|
||||
this.upload.data = Object.assign({ albums: [] }, data);
|
||||
});
|
||||
} else {
|
||||
this.upload.visible = true;
|
||||
this.upload.data = Object.assign({ albums: [] }, data);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -62,7 +62,7 @@
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<p-photo-album-dialog
|
||||
:show="dialog.album"
|
||||
:visible="dialog.album"
|
||||
@close="dialog.album = false"
|
||||
@confirm="addToAlbum"
|
||||
></p-photo-album-dialog>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-file-delete-dialog" @keydown.esc="close">
|
||||
<v-dialog
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="350"
|
||||
class="p-dialog p-file-delete-dialog"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="54" color="primary">mdi-delete-outline</v-icon>
|
||||
@@ -20,7 +26,7 @@
|
||||
export default {
|
||||
name: "PFileDeleteDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<p-photo-album-dialog
|
||||
:show="dialog.album"
|
||||
:visible="dialog.album"
|
||||
@close="dialog.album = false"
|
||||
@confirm="addToAlbum"
|
||||
></p-photo-album-dialog>
|
||||
<p-label-delete-dialog
|
||||
:show="dialog.delete"
|
||||
:visible="dialog.delete"
|
||||
@close="dialog.delete = false"
|
||||
@confirm="batchDelete"
|
||||
></p-label-delete-dialog>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-label-delete-dialog" @keydown.esc="close">
|
||||
<v-dialog
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="350"
|
||||
class="p-dialog p-label-delete-dialog"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="54" color="primary">mdi-delete-outline</v-icon>
|
||||
@@ -20,7 +26,10 @@
|
||||
export default {
|
||||
name: "PLabelDeleteDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="500"
|
||||
class="p-dialog dialog-label-edit"
|
||||
@@ -63,7 +63,10 @@ import Label from "model/label";
|
||||
export default {
|
||||
name: "PLabelEditDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -77,9 +80,12 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.model = this.label.clone();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition id="p-loading-bar" :css="false" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
|
||||
<div v-if="show" class="top-progress" :style="barStyle">
|
||||
<div v-if="visible" class="top-progress" :style="barStyle">
|
||||
<div class="peg" :style="pegStyle"></div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -92,8 +92,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
error: false,
|
||||
show: false,
|
||||
progress: 0,
|
||||
opacity: 1,
|
||||
status: null,
|
||||
@@ -202,10 +202,10 @@ export default {
|
||||
start() {
|
||||
this.isPaused = false;
|
||||
|
||||
if (this.show) {
|
||||
if (this.visible) {
|
||||
this._runStart();
|
||||
} else {
|
||||
this.show = true;
|
||||
this.visible = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -227,7 +227,7 @@ export default {
|
||||
setTimeout(() => {
|
||||
this.opacity = 0;
|
||||
setTimeout(() => {
|
||||
this.show = false;
|
||||
this.visible = false;
|
||||
this.error = false;
|
||||
next();
|
||||
}, this.speed);
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
class="nav-small elevation-2"
|
||||
@click.stop.prevent
|
||||
>
|
||||
<v-btn icon variant="text" class="bg-transparent nav-logo" @click.stop.prevent="showNavigation()">
|
||||
<v-btn icon variant="text" class="bg-transparent nav-logo" @click.stop.prevent="toggleDrawer">
|
||||
<img :src="appIcon" :alt="appName" :class="{ 'animate-hue': indexing }" />
|
||||
</v-btn>
|
||||
<v-toolbar-title class="nav-toolbar-title">
|
||||
<span :class="{ clickable: auth }" @click.stop.prevent="showNavigation()">{{ page.title }}</span>
|
||||
<span :class="{ clickable: auth }" @click.stop.prevent="toggleDrawer">{{ page.title }}</span>
|
||||
</v-toolbar-title>
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
@@ -61,7 +61,13 @@
|
||||
>
|
||||
<div class="nav-container">
|
||||
<v-toolbar flat :density="$vuetify.display.smAndDown ? 'compact' : 'default'">
|
||||
<v-list class="navigation-home elevation-0" bg-color="navigation-home" width="100%" density="compact">
|
||||
<v-list
|
||||
class="navigation-home elevation-0"
|
||||
bg-color="navigation-home"
|
||||
width="100%"
|
||||
density="compact"
|
||||
@click.capture="toggleDrawer"
|
||||
>
|
||||
<v-list-item class="px-3" :elevation="0" :ripple="false" @click.stop.prevent="goHome">
|
||||
<template #prepend>
|
||||
<div class="v-avatar bg-transparent nav-logo">
|
||||
@@ -377,15 +383,33 @@
|
||||
<span v-show="config.count.favorites > 0" class="nav-count-item">{{ config.count.favorites }}</span>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="isRestricted && $config.feature('places')"
|
||||
:to="{ name: 'states' }"
|
||||
variant="text"
|
||||
class="nav-states nav-regions"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-icon class="ma-auto">mdi-near-me</v-icon>
|
||||
</v-list-item>
|
||||
<template v-if="isRestricted && $config.feature('places')">
|
||||
<v-list-item
|
||||
v-if="isMini"
|
||||
:to="{ name: 'states' }"
|
||||
variant="text"
|
||||
class="nav-states nav-regions"
|
||||
:ripple="false"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-icon class="ma-auto">mdi-near-me</v-icon>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-else-if="!isMini"
|
||||
:to="{ name: 'states' }"
|
||||
variant="text"
|
||||
class="nav-states nav-regions"
|
||||
:ripple="false"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-list-item-title class="nav-menu-item">
|
||||
<v-icon>mdi-near-me</v-icon>
|
||||
<p class="nav-item-title">
|
||||
{{ $gettext(`Regions`) }}
|
||||
</p>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template v-if="canSearchPlaces">
|
||||
<v-list-item
|
||||
@@ -709,7 +733,7 @@
|
||||
class="nav-upgrade"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-icon v-if="isPro" class="ma-auto">mdi-check-circle</v-icon>
|
||||
<v-icon v-if="isPro" class="ma-auto">mdi-check-decagram</v-icon>
|
||||
<v-icon v-else class="ma-auto">mdi-diamond</v-icon>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@@ -720,7 +744,7 @@
|
||||
@click.stop=""
|
||||
>
|
||||
<v-list-item-title v-if="isPro" class="nav-menu-item">
|
||||
<v-icon>mdi-check-circle</v-icon>
|
||||
<v-icon>mdi-check-decagram</v-icon>
|
||||
<p class="nav-item-title">
|
||||
{{ $gettext(`Upgrade`) }}
|
||||
</p>
|
||||
@@ -879,13 +903,9 @@
|
||||
</div>
|
||||
<div v-if="featUpgrade" class="menu-action nav-upgrade">
|
||||
<router-link :to="{ name: 'upgrade' }">
|
||||
<template #default="{ href, navigate, isActive }">
|
||||
<a :href="href" :class="{ active: isActive }" @click="navigate">
|
||||
<v-icon v-if="isPro">mdi-check-circle</v-icon>
|
||||
<v-icon v-else>mdi-diamond</v-icon>
|
||||
{{ $gettext(`Upgrade`) }}
|
||||
</a>
|
||||
</template>
|
||||
<v-icon v-if="isPro">mdi-check-decagram</v-icon>
|
||||
<v-icon v-else>mdi-diamond</v-icon>
|
||||
{{ $gettext(`Upgrade`) }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="config.legalUrl" class="menu-action nav-legal">
|
||||
@@ -897,38 +917,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p-update :show="reload.dialog" @close="reload.dialog = false"></p-update>
|
||||
<p-photo-upload-dialog
|
||||
:show="upload.dialog"
|
||||
:data="upload.data"
|
||||
@close="upload.dialog = false"
|
||||
@confirm="upload.dialog = false"
|
||||
></p-photo-upload-dialog>
|
||||
<p-photo-edit-dialog
|
||||
:show="edit.dialog"
|
||||
:selection="edit.selection"
|
||||
:index="edit.index"
|
||||
:album="edit.album"
|
||||
:tab="edit.tab"
|
||||
@close="edit.dialog = false"
|
||||
></p-photo-edit-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Event from "pubsub-js";
|
||||
import Album from "model/album";
|
||||
import PUpdate from "component/update.vue";
|
||||
import PPhotoEditDialog from "component/photo/edit/dialog.vue";
|
||||
import PPhotoUploadDialog from "component/photo/upload/dialog.vue";
|
||||
|
||||
export default {
|
||||
name: "PNavigation",
|
||||
components: {
|
||||
PUpdate,
|
||||
PPhotoEditDialog,
|
||||
PPhotoUploadDialog,
|
||||
},
|
||||
data() {
|
||||
const appName = this.$config.getName();
|
||||
|
||||
@@ -978,20 +974,6 @@ export default {
|
||||
config: this.$config.values,
|
||||
page: this.$config.page,
|
||||
user: this.$session.getUser(),
|
||||
reload: {
|
||||
dialog: false,
|
||||
},
|
||||
upload: {
|
||||
dialog: false,
|
||||
data: {},
|
||||
},
|
||||
edit: {
|
||||
dialog: false,
|
||||
album: null,
|
||||
selection: [],
|
||||
index: 0,
|
||||
tab: "",
|
||||
},
|
||||
speedDial: false,
|
||||
rtl: this.$rtl,
|
||||
subscriptions: [],
|
||||
@@ -1027,28 +1009,6 @@ export default {
|
||||
created() {
|
||||
this.subscriptions.push(Event.subscribe("index", this.onIndex));
|
||||
this.subscriptions.push(Event.subscribe("import", this.onIndex));
|
||||
this.subscriptions.push(Event.subscribe("dialog.reload", () => (this.reload.dialog = true)));
|
||||
this.subscriptions.push(
|
||||
Event.subscribe("dialog.upload", (ev, data) => {
|
||||
if (data) {
|
||||
this.upload.data = data;
|
||||
} else {
|
||||
this.upload.data = {};
|
||||
}
|
||||
this.upload.dialog = true;
|
||||
})
|
||||
);
|
||||
this.subscriptions.push(
|
||||
Event.subscribe("dialog.edit", (ev, data) => {
|
||||
if (!this.edit.dialog) {
|
||||
this.edit.dialog = true;
|
||||
this.edit.index = data.index;
|
||||
this.edit.selection = data.selection;
|
||||
this.edit.album = data.album;
|
||||
this.edit.tab = data?.tab ? data.tab : "";
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||
@@ -1069,38 +1029,44 @@ export default {
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
},
|
||||
openUpload() {
|
||||
if (this.auth && !this.isReadOnly && this.$config.feature("upload")) {
|
||||
if (this.$route.name === "album" && this.$route.params?.album) {
|
||||
return new Album()
|
||||
.find(this.$route.params?.album)
|
||||
.then((m) => {
|
||||
this.upload.dialog = true;
|
||||
this.upload.data = { albums: [m] };
|
||||
})
|
||||
.catch(() => {
|
||||
this.upload.dialog = true;
|
||||
this.upload.data = { albums: [] };
|
||||
});
|
||||
} else {
|
||||
this.upload.dialog = true;
|
||||
this.upload.data = { albums: [] };
|
||||
}
|
||||
} else {
|
||||
this.goHome();
|
||||
}
|
||||
this.$event.publish("dialog.upload");
|
||||
},
|
||||
goHome() {
|
||||
if (this.$route.name !== "home") {
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
},
|
||||
showNavigation() {
|
||||
showDrawer() {
|
||||
if (this.auth) {
|
||||
this.drawer = true;
|
||||
this.isMini = this.isRestricted;
|
||||
}
|
||||
},
|
||||
hideDrawer() {
|
||||
if (this.auth) {
|
||||
this.drawer = false;
|
||||
this.isMini = this.isRestricted;
|
||||
}
|
||||
},
|
||||
toggleDrawer(ev) {
|
||||
ev?.preventDefault();
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this.$vuetify.display.smAndDown) {
|
||||
if (this.drawer) {
|
||||
this.hideDrawer();
|
||||
} else {
|
||||
this.showDrawer();
|
||||
}
|
||||
} else {
|
||||
this.toggleIsMini();
|
||||
}
|
||||
},
|
||||
toggleIsMini() {
|
||||
if (this.isRestricted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isMini = !this.isMini;
|
||||
localStorage.setItem("last_navigation_mode", `${this.isMini}`);
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<p-photo-album-dialog
|
||||
:show="dialog.album"
|
||||
:visible="dialog.album"
|
||||
@close="dialog.album = false"
|
||||
@confirm="addToAlbum"
|
||||
></p-photo-album-dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="500"
|
||||
class="dialog-person-edit"
|
||||
@@ -73,7 +73,7 @@ import Subject from "model/subject";
|
||||
export default {
|
||||
name: "PPeopleEditDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
person: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -87,9 +87,12 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.model = this.person.clone();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-people-merge-dialog" @keydown.esc="cancel">
|
||||
<v-dialog :model-value="visible" persistent max-width="350" class="p-dialog p-people-merge-dialog" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="54" color="primary">mdi-account-multiple</v-icon>
|
||||
<p class="text-subtitle-1">{{ prompt }}</p>
|
||||
</v-card-title>
|
||||
<v-card-actions class="dialog-merge action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel" @click.stop="cancel">
|
||||
<v-btn variant="flat" color="button" class="action-cancel" @click.stop="close">
|
||||
{{ $gettext(`No`) }}
|
||||
</v-btn>
|
||||
<v-btn color="highlight" variant="flat" class="action-confirm" @click.stop="confirm">
|
||||
@@ -22,7 +22,7 @@ import Subject from "model/subject";
|
||||
export default {
|
||||
name: "PPeopleMergeDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
subj1: {
|
||||
type: Object,
|
||||
default: new Subject(),
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
confirm() {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="390" class="p-dialog p-photo-album-dialog" @keydown.esc="close">
|
||||
<v-dialog
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="390"
|
||||
class="p-dialog p-photo-album-dialog"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<v-form ref="form" validate-on="invalid-input" accept-charset="UTF-8" @submit.prevent="confirm">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
@@ -52,7 +58,7 @@ const MaxResults = 10000;
|
||||
export default {
|
||||
name: "PPhotoAlbumDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -68,10 +74,13 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.reset();
|
||||
this.load("");
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="350"
|
||||
class="p-dialog p-photo-archive-dialog"
|
||||
@@ -26,7 +26,7 @@
|
||||
export default {
|
||||
name: "PPhotoArchiveDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
||||
@@ -158,22 +158,22 @@
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
<p-photo-archive-dialog
|
||||
:show="dialog.archive"
|
||||
:visible="dialog.archive"
|
||||
@close="dialog.archive = false"
|
||||
@confirm="batchArchive"
|
||||
></p-photo-archive-dialog>
|
||||
<p-photo-delete-dialog
|
||||
:show="dialog.delete"
|
||||
:visible="dialog.delete"
|
||||
@close="dialog.delete = false"
|
||||
@confirm="batchDelete"
|
||||
></p-photo-delete-dialog>
|
||||
<p-photo-album-dialog
|
||||
:show="dialog.album"
|
||||
:visible="dialog.album"
|
||||
@close="dialog.album = false"
|
||||
@confirm="addToAlbum"
|
||||
></p-photo-album-dialog>
|
||||
<p-service-upload
|
||||
:show="dialog.share"
|
||||
:visible="dialog.share"
|
||||
:items="{ photos: selection }"
|
||||
:model="album"
|
||||
@close="dialog.share = false"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="360" class="p-dialog p-photo-delete-dialog" @keydown.esc="close">
|
||||
<v-dialog
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="360"
|
||||
class="p-dialog p-photo-delete-dialog"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="54" color="primary">mdi-delete-outline</v-icon>
|
||||
@@ -26,7 +32,7 @@
|
||||
export default {
|
||||
name: "PPhotoDeleteDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<v-col cols="3" sm="2" class="form-thumb">
|
||||
<div>
|
||||
<img
|
||||
:alt="model.Title"
|
||||
:src="model.thumbnailUrl('tile_500')"
|
||||
:alt="view.model.Title"
|
||||
:src="view.model.thumbnailUrl('tile_500')"
|
||||
class="clickable"
|
||||
@click.stop.prevent.exact="openPhoto()"
|
||||
/>
|
||||
@@ -22,8 +22,8 @@
|
||||
</v-col>
|
||||
<v-col cols="9" sm="10" class="d-flex align-self-stretch flex-column ga-4">
|
||||
<v-text-field
|
||||
v-model="model.Title"
|
||||
:append-inner-icon="model.TitleSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Title"
|
||||
:append-inner-icon="view.model.TitleSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
@@ -34,8 +34,8 @@
|
||||
class="input-title"
|
||||
></v-text-field>
|
||||
<v-textarea
|
||||
v-model="model.Caption"
|
||||
:append-inner-icon="model.CaptionSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Caption"
|
||||
:append-inner-icon="view.model.CaptionSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -50,11 +50,13 @@
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="4" lg="2">
|
||||
<v-autocomplete
|
||||
v-model="model.Day"
|
||||
<v-combobox
|
||||
:model-value="view.model.Day > 0 ? view.model.Day : null"
|
||||
:disabled="disabled"
|
||||
:error="invalidDate"
|
||||
:label="$gettext('Day')"
|
||||
:placeholder="$gettext('Unknown')"
|
||||
:prepend-inner-icon="$vuetify.display.xs ? undefined : 'mdi-calendar'"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
hide-no-data
|
||||
@@ -63,18 +65,19 @@
|
||||
item-value="value"
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.day(true)"
|
||||
:rules="rules.day(false)"
|
||||
class="input-day"
|
||||
@update:model-value="syncTime"
|
||||
@update:model-value="setDay"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="4" lg="2">
|
||||
<v-autocomplete
|
||||
v-model="model.Month"
|
||||
<v-combobox
|
||||
:model-value="view.model.Month > 0 ? view.model.Month : null"
|
||||
:disabled="disabled"
|
||||
:error="invalidDate"
|
||||
:label="$gettext('Month')"
|
||||
:placeholder="$gettext('Unknown')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
hide-no-data
|
||||
@@ -83,38 +86,40 @@
|
||||
item-value="value"
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.month(true)"
|
||||
:rules="rules.month(false)"
|
||||
class="input-month"
|
||||
@update:model-value="syncTime"
|
||||
@update:model-value="setMonth"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="4" lg="2">
|
||||
<v-autocomplete
|
||||
v-model="model.Year"
|
||||
<v-combobox
|
||||
:model-value="view.model.Year > 0 ? view.model.Year : null"
|
||||
:disabled="disabled"
|
||||
:error="invalidDate"
|
||||
:label="$gettext('Year')"
|
||||
:placeholder="$gettext('Unknown')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
hide-no-data
|
||||
:items="options.Years(1000)"
|
||||
:items="options.Years(1900)"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.year(true, 1000)"
|
||||
:rules="rules.year(false, 1000)"
|
||||
class="input-year"
|
||||
@update:model-value="syncTime"
|
||||
@update:model-value="setYear"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="6" lg="2">
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
:append-inner-icon="model.TakenSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:append-inner-icon="view.model.TakenSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:label="model.timeIsUTC() ? $gettext('Time UTC') : $gettext('Local Time')"
|
||||
:label="view.model.timeIsUTC() ? $gettext('Time UTC') : $gettext('Local Time')"
|
||||
:prepend-inner-icon="$vuetify.display.xs ? undefined : 'mdi-clock-time-eight-outline'"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
@@ -128,7 +133,7 @@
|
||||
</v-col>
|
||||
<v-col cols="6" lg="4">
|
||||
<v-autocomplete
|
||||
v-model="model.TimeZone"
|
||||
v-model="view.model.TimeZone"
|
||||
:disabled="disabled"
|
||||
:label="$gettext('Time Zone')"
|
||||
hide-no-data
|
||||
@@ -142,10 +147,10 @@
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" md="4">
|
||||
<v-autocomplete
|
||||
v-model="model.Country"
|
||||
:append-inner-icon="model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Country"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:readonly="!!(model.Lat || model.Lng)"
|
||||
:readonly="!!(view.model.Lat || view.model.Lng)"
|
||||
:placeholder="$gettext('Country')"
|
||||
hide-details
|
||||
hide-no-data
|
||||
@@ -163,7 +168,7 @@
|
||||
</v-col>
|
||||
<v-col cols="4" md="2">
|
||||
<v-text-field
|
||||
v-model="model.Altitude"
|
||||
v-model="view.model.Altitude"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
flat
|
||||
@@ -181,8 +186,8 @@
|
||||
</v-col>
|
||||
<v-col cols="4" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.Lat"
|
||||
:append-inner-icon="model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Lat"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -199,8 +204,8 @@
|
||||
</v-col>
|
||||
<v-col cols="4" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.Lng"
|
||||
:append-inner-icon="model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Lng"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -217,8 +222,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="p-camera-select">
|
||||
<v-select
|
||||
v-model="model.CameraID"
|
||||
:append-inner-icon="model.CameraSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.CameraID"
|
||||
:append-inner-icon="view.model.CameraSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:placeholder="$gettext('Camera')"
|
||||
:menu-props="{ maxHeight: 346 }"
|
||||
@@ -235,7 +240,7 @@
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.Iso"
|
||||
v-model="view.model.Iso"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -251,7 +256,7 @@
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.Exposure"
|
||||
v-model="view.model.Exposure"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -267,8 +272,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="p-lens-select">
|
||||
<v-select
|
||||
v-model="model.LensID"
|
||||
:append-inner-icon="model.CameraSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.LensID"
|
||||
:append-inner-icon="view.model.CameraSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:placeholder="$gettext('Lens')"
|
||||
:menu-props="{ maxHeight: 346 }"
|
||||
@@ -285,7 +290,7 @@
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.FNumber"
|
||||
v-model="view.model.FNumber"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -301,7 +306,7 @@
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="model.FocalLength"
|
||||
v-model="view.model.FocalLength"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -317,8 +322,8 @@
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-textarea
|
||||
v-model="model.Details.Subject"
|
||||
:append-inner-icon="model.Details.SubjectSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.Subject"
|
||||
:append-inner-icon="view.model.Details.SubjectSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
@@ -333,8 +338,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="model.Details.Copyright"
|
||||
:append-inner-icon="model.Details.CopyrightSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.Copyright"
|
||||
:append-inner-icon="view.model.Details.CopyrightSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
@@ -347,8 +352,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="model.Details.Artist"
|
||||
:append-inner-icon="model.Details.ArtistSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.Artist"
|
||||
:append-inner-icon="view.model.Details.ArtistSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
@@ -361,8 +366,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-textarea
|
||||
v-model="model.Details.License"
|
||||
:append-inner-icon="model.Details.LicenseSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.License"
|
||||
:append-inner-icon="view.model.Details.LicenseSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:rules="[textRule]"
|
||||
hide-details
|
||||
@@ -377,8 +382,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="8">
|
||||
<v-textarea
|
||||
v-model="model.Details.Keywords"
|
||||
:append-inner-icon="model.Details.KeywordsSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.Keywords"
|
||||
:append-inner-icon="view.model.Details.KeywordsSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -392,8 +397,8 @@
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-textarea
|
||||
v-model="model.Details.Notes"
|
||||
:append-inner-icon="model.Details.NotesSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Details.Notes"
|
||||
:append-inner-icon="view.model.Details.NotesSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
@@ -416,7 +421,7 @@
|
||||
<v-btn
|
||||
color="highlight"
|
||||
variant="flat"
|
||||
:disabled="!model?.wasChanged() && !inReview"
|
||||
:disabled="!view.model?.wasChanged() && !inReview"
|
||||
class="action-apply action-approve"
|
||||
@click.stop="save(false)"
|
||||
>
|
||||
@@ -432,17 +437,12 @@
|
||||
<script>
|
||||
import countries from "options/countries.json";
|
||||
import Thumb from "model/thumb";
|
||||
import Photo from "model/photo";
|
||||
import * as options from "options/options";
|
||||
import { rules } from "common/form";
|
||||
|
||||
export default {
|
||||
name: "PTabPhotoDetails",
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => new Photo(false),
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -450,6 +450,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
view: this.$view.data(),
|
||||
disabled: !this.$config.feature("edit"),
|
||||
config: this.$config.values,
|
||||
all: {
|
||||
@@ -476,13 +477,10 @@ export default {
|
||||
return this.config.lenses;
|
||||
},
|
||||
inReview() {
|
||||
return this.featReview && this.model.Quality < 3;
|
||||
return this.featReview && this.view.model.Quality < 3;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
model() {
|
||||
this.syncTime();
|
||||
},
|
||||
uid() {
|
||||
this.syncTime();
|
||||
},
|
||||
@@ -491,21 +489,54 @@ export default {
|
||||
this.syncTime();
|
||||
},
|
||||
methods: {
|
||||
setDay(v) {
|
||||
if (Number.isInteger(v?.value)) {
|
||||
this.view.model.Day = v?.value;
|
||||
this.syncTime();
|
||||
} else if (!v) {
|
||||
this.view.model.Day = -1;
|
||||
} else if (this.rules.isNumberRange(v, 1, 31)) {
|
||||
this.view.model.Day = Number(v);
|
||||
this.syncTime();
|
||||
}
|
||||
},
|
||||
setMonth(v) {
|
||||
if (Number.isInteger(v?.value)) {
|
||||
this.view.model.Month = v?.value;
|
||||
this.syncTime();
|
||||
} else if (!v) {
|
||||
this.view.model.Month = -1;
|
||||
} else if (this.rules.isNumberRange(v, 1, 12)) {
|
||||
this.view.model.Month = Number(v);
|
||||
this.syncTime();
|
||||
}
|
||||
},
|
||||
setYear(v) {
|
||||
if (Number.isInteger(v?.value)) {
|
||||
this.view.model.Year = v?.value;
|
||||
this.syncTime();
|
||||
} else if (!v) {
|
||||
this.view.model.Year = -1;
|
||||
} else if (this.rules.isNumberRange(v, 1000, Number(new Date().getUTCFullYear()))) {
|
||||
this.view.model.Year = Number(v);
|
||||
this.syncTime();
|
||||
}
|
||||
},
|
||||
setTime() {
|
||||
if (this.rules.isTime(this.time)) {
|
||||
this.updateModel();
|
||||
}
|
||||
},
|
||||
syncTime() {
|
||||
if (!this.model.hasId()) {
|
||||
if (!this.view?.model.hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taken = this.model.getDateTime();
|
||||
const taken = this.view.model.getDateTime();
|
||||
this.time = taken.toFormat("HH:mm:ss");
|
||||
},
|
||||
pastePosition(event) {
|
||||
// Auto-fills the lat and lng fields if the text in the clipboard contains two float values.
|
||||
// Autofill the lat and lng fields if the text in the clipboard contains two float values.
|
||||
const clipboard = event.clipboardData ? event.clipboardData : window.clipboardData;
|
||||
|
||||
if (!clipboard) {
|
||||
@@ -526,20 +557,20 @@ export default {
|
||||
|
||||
// Lat and long must be valid floating point numbers.
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 && !isNaN(lng) && lng >= -180 && lng <= 180) {
|
||||
// Update model values.
|
||||
this.model.Lat = lat;
|
||||
this.model.Lng = lng;
|
||||
// Update view.model values.
|
||||
this.view.model.Lat = lat;
|
||||
this.view.model.Lng = lng;
|
||||
// Prevent default action.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
updateModel() {
|
||||
if (!this.model.hasId()) {
|
||||
if (!this.view?.model.hasId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let localDate = this.model.localDate(this.time);
|
||||
let localDate = this.view.model.localDate(this.time);
|
||||
|
||||
this.invalidDate = !localDate.isValid;
|
||||
|
||||
@@ -547,16 +578,16 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.Day === 0) {
|
||||
this.model.Day = parseInt(localDate.toFormat("d"));
|
||||
if (this.view.model.Day === 0) {
|
||||
this.view.model.Day = parseInt(localDate.toFormat("d"));
|
||||
}
|
||||
|
||||
if (this.model.Month === 0) {
|
||||
this.model.Month = parseInt(localDate.toFormat("L"));
|
||||
if (this.view.model.Month === 0) {
|
||||
this.view.model.Month = parseInt(localDate.toFormat("L"));
|
||||
}
|
||||
|
||||
if (this.model.Year === 0) {
|
||||
this.model.Year = parseInt(localDate.toFormat("y"));
|
||||
if (this.view.model.Year === 0) {
|
||||
this.view.model.Year = parseInt(localDate.toFormat("y"));
|
||||
}
|
||||
|
||||
const isoTime =
|
||||
@@ -565,10 +596,10 @@ export default {
|
||||
includeOffset: false,
|
||||
}) + "Z";
|
||||
|
||||
this.model.TakenAtLocal = isoTime;
|
||||
this.view.model.TakenAtLocal = isoTime;
|
||||
|
||||
if (this.model.currentTimeZoneUTC()) {
|
||||
this.model.TakenAt = isoTime;
|
||||
if (this.view.model.currentTimeZoneUTC()) {
|
||||
this.view.model.TakenAt = isoTime;
|
||||
}
|
||||
},
|
||||
left() {
|
||||
@@ -578,7 +609,7 @@ export default {
|
||||
this.$emit("prev");
|
||||
},
|
||||
openPhoto() {
|
||||
this.$root.$refs.viewer.showThumbs(Thumb.fromFiles([this.model]), 0);
|
||||
this.$lightbox.openModels(Thumb.fromFiles([this.view.model]), 0);
|
||||
},
|
||||
save(close) {
|
||||
if (this.invalidDate) {
|
||||
@@ -588,7 +619,7 @@ export default {
|
||||
|
||||
this.updateModel();
|
||||
|
||||
this.model.update().then(() => {
|
||||
this.view.model.update().then(() => {
|
||||
if (close) {
|
||||
this.$emit("close");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
ref="dialog"
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
:fullscreen="$vuetify.display.smAndDown"
|
||||
:transition="false"
|
||||
persistent
|
||||
scrim
|
||||
scrollable
|
||||
@@ -69,37 +68,30 @@
|
||||
<v-badge v-if="model.Files.length" color="surface-variant" inline :content="model.Files.length"></v-badge>
|
||||
</v-tab>
|
||||
|
||||
<v-tab v-if="$config.feature('edit')" id="tab-info" value="info" ripple>
|
||||
<v-tab v-if="canEdit" id="tab-info" value="info" ripple>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window v-model="active">
|
||||
<v-tabs-window-item value="details">
|
||||
<p-tab-photo-details
|
||||
ref="details"
|
||||
:model="model"
|
||||
:uid="uid"
|
||||
@close="close"
|
||||
@prev="prev"
|
||||
@next="next"
|
||||
></p-tab-photo-details>
|
||||
<p-tab-photo-details ref="details" :uid="uid" @close="close" @prev="prev" @next="next"></p-tab-photo-details>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="labels">
|
||||
<p-tab-photo-labels :model="model" :uid="uid" @close="close"></p-tab-photo-labels>
|
||||
<p-tab-photo-labels :uid="uid" @close="close"></p-tab-photo-labels>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="people">
|
||||
<p-tab-photo-people :model="model" :uid="uid" @close="close"></p-tab-photo-people>
|
||||
<p-tab-photo-people :uid="uid" @close="close"></p-tab-photo-people>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="files">
|
||||
<p-tab-photo-files :model="model" :uid="uid" @close="close"></p-tab-photo-files>
|
||||
<p-tab-photo-files :uid="uid" @close="close"></p-tab-photo-files>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item v-if="$config.feature('edit')" value="info">
|
||||
<p-tab-photo-info :model="model" :uid="uid" @close="close"></p-tab-photo-info>
|
||||
<v-tabs-window-item v-if="canEdit" value="info">
|
||||
<p-tab-photo-info :uid="uid" @close="close"></p-tab-photo-info>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</v-card>
|
||||
@@ -107,12 +99,13 @@
|
||||
</template>
|
||||
<script>
|
||||
import Photo from "model/photo";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
import PhotoDetails from "component/photo/edit/details.vue";
|
||||
import PhotoLabels from "component/photo/edit/labels.vue";
|
||||
import PhotoPeople from "component/photo/edit/people.vue";
|
||||
import PhotoFiles from "component/photo/edit/files.vue";
|
||||
import PhotoInfo from "component/photo/edit/info.vue";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
export default {
|
||||
name: "PPhotoEditDialog",
|
||||
@@ -124,11 +117,11 @@ export default {
|
||||
"p-tab-photo-info": PhotoInfo,
|
||||
},
|
||||
props: {
|
||||
visible: Boolean,
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
show: Boolean,
|
||||
selection: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -151,6 +144,7 @@ export default {
|
||||
loading: false,
|
||||
search: null,
|
||||
items: [],
|
||||
canEdit: this.$config.feature("edit"),
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
rtl: this.$rtl,
|
||||
@@ -174,17 +168,15 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
// Disable the browser scrollbar.
|
||||
this.$modal.enter();
|
||||
this.$view.enter(this);
|
||||
if (this.tab) {
|
||||
this.active = this.tab;
|
||||
}
|
||||
this.find(this.index);
|
||||
} else {
|
||||
// Re-enable the browser scrollbar.
|
||||
this.$modal.leave();
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -249,19 +241,19 @@ export default {
|
||||
},
|
||||
find(index) {
|
||||
if (this.loading) {
|
||||
return;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (!this.selection || !this.selection[index]) {
|
||||
this.$notify.error(this.$gettext("Invalid photo selected"));
|
||||
return;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.selected = index;
|
||||
this.selectedId = this.selection[index];
|
||||
|
||||
this.model
|
||||
return this.model
|
||||
.find(this.selectedId)
|
||||
.then((model) => {
|
||||
model.refreshFileAttr();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="p-tab p-tab-photo-files">
|
||||
<v-expansion-panels v-model="expanded" class="pa-0 elevation-0" variant="accordion" multiple>
|
||||
<v-expansion-panel
|
||||
v-for="file in model.fileModels().filter((f) => !f.Missing)"
|
||||
v-for="file in view.model.fileModels().filter((f) => !f.Missing)"
|
||||
:key="file.UID"
|
||||
class="pa-0 elevation-0"
|
||||
style="margin-top: 1px"
|
||||
@@ -361,7 +361,7 @@
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<p-file-delete-dialog
|
||||
:show="deleteFile.dialog"
|
||||
:visible="deleteFile.dialog"
|
||||
@close="closeDeleteDialog"
|
||||
@confirm="confirmDeleteFile"
|
||||
></p-file-delete-dialog>
|
||||
@@ -378,10 +378,6 @@ import * as options from "options/options";
|
||||
export default {
|
||||
name: "PTabPhotoFiles",
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -389,6 +385,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
view: this.$view.data(),
|
||||
expanded: [0],
|
||||
deleteFile: {
|
||||
dialog: false,
|
||||
@@ -433,7 +430,6 @@ export default {
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
orientationClass(file) {
|
||||
if (!file) {
|
||||
@@ -463,7 +459,7 @@ export default {
|
||||
return Util.codecName(file.Codec);
|
||||
},
|
||||
openFile(file) {
|
||||
this.$root.$refs.viewer.showThumbs([Thumb.fromFile(this.model, file)], 0);
|
||||
this.$lightbox.openModels([Thumb.fromFile(this.view.model, file)], 0);
|
||||
},
|
||||
openFolder(file) {
|
||||
if (!file) {
|
||||
@@ -504,25 +500,33 @@ export default {
|
||||
},
|
||||
confirmDeleteFile() {
|
||||
if (this.deleteFile.file && this.deleteFile.file.UID) {
|
||||
this.model.deleteFile(this.deleteFile.file.UID).finally(() => this.closeDeleteDialog());
|
||||
this.view.model.deleteFile(this.deleteFile.file.UID).finally(() => this.closeDeleteDialog());
|
||||
} else {
|
||||
this.closeDeleteDialog();
|
||||
}
|
||||
},
|
||||
unstackFile(file) {
|
||||
this.model.unstackFile(file.UID);
|
||||
if (!file || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.model.unstackFile(file.UID);
|
||||
},
|
||||
setPrimaryFile(file) {
|
||||
this.model.setPrimaryFile(file.UID);
|
||||
if (!file || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.model.setPrimaryFile(file.UID);
|
||||
},
|
||||
changeOrientation(file) {
|
||||
if (!file) {
|
||||
if (!file || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.busy = true;
|
||||
|
||||
this.model
|
||||
this.view.model
|
||||
.changeFileOrientation(file)
|
||||
.then(() => {
|
||||
this.$notify.success(this.$gettext("Changes successfully saved"));
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
<tr>
|
||||
<td>UID</td>
|
||||
<td class="text-break">
|
||||
<span class="clickable text-uppercase" @click.stop.prevent="$util.copyText(model.UID)">{{
|
||||
model.UID
|
||||
<span class="clickable text-uppercase" @click.stop.prevent="$util.copyText(view.model.UID)">{{
|
||||
view.model.UID
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.DocumentID">
|
||||
<tr v-if="view.model.DocumentID">
|
||||
<td>Document ID</td>
|
||||
<td class="text-break">
|
||||
<span class="clickable text-uppercase" @click.stop.prevent="$util.copyText(model.DocumentID)">{{
|
||||
model.DocumentID
|
||||
<span class="clickable text-uppercase" @click.stop.prevent="$util.copyText(view.model.DocumentID)">{{
|
||||
view.model.DocumentID
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -24,10 +24,10 @@
|
||||
<td>
|
||||
<span>{{ $gettext(`Type`) }}</span>
|
||||
</td>
|
||||
<td v-tooltip="formatSource(model?.TypeSrc, $gettext('Default'))">
|
||||
<td v-tooltip="formatSource(view.model?.TypeSrc, $gettext('Default'))">
|
||||
<v-select
|
||||
v-model="model.Type"
|
||||
:append-icon="model.TypeSrc === 'manual' ? 'mdi-check' : ''"
|
||||
v-model="view.model.Type"
|
||||
:append-icon="view.model.TypeSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:list-props="{ density: 'compact' }"
|
||||
max-width="160"
|
||||
variant="solo"
|
||||
@@ -43,12 +43,14 @@
|
||||
></v-select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.Path">
|
||||
<tr v-if="view.model.Path">
|
||||
<td>
|
||||
{{ $gettext(`Folder`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
<span class="clickable" @click.stop.prevent="$util.copyText(model.Path)">{{ model.Path }}</span>
|
||||
<span class="clickable" @click.stop.prevent="$util.copyText(view.model.Path)">{{
|
||||
view.model.Path
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -56,16 +58,18 @@
|
||||
{{ $gettext(`Name`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
<span class="clickable" @click.stop.prevent="$util.copyText(model.Name)">{{ model.Name }}</span>
|
||||
<span class="clickable" @click.stop.prevent="$util.copyText(view.model.Name)">{{
|
||||
view.model.Name
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.OriginalName">
|
||||
<tr v-if="view.model.OriginalName">
|
||||
<td>
|
||||
{{ $gettext(`Original Name`) }}
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model="model.OriginalName"
|
||||
v-model="view.model.OriginalName"
|
||||
flat
|
||||
variant="solo"
|
||||
bg-color="transparent"
|
||||
@@ -83,10 +87,12 @@
|
||||
<span>{{ $gettext(`Title`) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-tooltip="formatSource(model?.TitleSrc, $gettext('Generated'))" class="text-flex text-break">
|
||||
<span class="clickable text-break" @click.stop.prevent="$util.copyText(model.Title)">{{ model.Title }}</span>
|
||||
<v-icon v-if="model.TitleSrc === 'name'" icon="mdi-file" class="src"></v-icon>
|
||||
<v-icon v-else-if="model.TitleSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
<div v-tooltip="formatSource(view.model?.TitleSrc, $gettext('Generated'))" class="text-flex text-break">
|
||||
<span class="clickable text-break" @click.stop.prevent="$util.copyText(view.model.Title)">{{
|
||||
view.model.Title
|
||||
}}</span>
|
||||
<v-icon v-if="view.model.TitleSrc === 'name'" icon="mdi-file" class="src"></v-icon>
|
||||
<v-icon v-else-if="view.model.TitleSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -95,13 +101,13 @@
|
||||
<span>{{ $gettext(`Taken`) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div v-tooltip="formatSource(model?.TakenSrc, $gettext('File'))" class="text-flex text-break">
|
||||
<div>{{ model.getDateString() }}</div>
|
||||
<v-icon v-if="model.TakenSrc === ''" icon="mdi-file-clock-outline" class="src"></v-icon>
|
||||
<!-- v-icon v-else-if="model.TakenSrc === 'meta'" icon="mdi-camera" class="src"></v-icon -->
|
||||
<v-icon v-else-if="model.TakenSrc === 'name'" icon="mdi-file-tree-outline" class="src"></v-icon>
|
||||
<v-icon v-else-if="model.TakenSrc === 'estimate'" icon="mdi-file-question" class="src"></v-icon>
|
||||
<v-icon v-else-if="model.TakenSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
<div v-tooltip="formatSource(view.model?.TakenSrc, $gettext('File'))" class="text-flex text-break">
|
||||
<div>{{ view.model.getDateString() }}</div>
|
||||
<v-icon v-if="view.model.TakenSrc === ''" icon="mdi-file-clock-outline" class="src"></v-icon>
|
||||
<!-- v-icon v-else-if="view.model.TakenSrc === 'meta'" icon="mdi-camera" class="src"></v-icon -->
|
||||
<v-icon v-else-if="view.model.TakenSrc === 'name'" icon="mdi-file-tree-outline" class="src"></v-icon>
|
||||
<v-icon v-else-if="view.model.TakenSrc === 'estimate'" icon="mdi-file-question" class="src"></v-icon>
|
||||
<v-icon v-else-if="view.model.TakenSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -120,39 +126,39 @@
|
||||
{{ $gettext(`Quality Score`) }}
|
||||
</td>
|
||||
<td>
|
||||
<v-rating v-model="model.Quality" :length="7" size="small" density="compact" readonly></v-rating>
|
||||
<v-rating v-model="view.model.Quality" :length="7" size="small" density="compact" readonly></v-rating>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ $gettext(`Resolution`) }}
|
||||
</td>
|
||||
<td>{{ model.Resolution }} MP</td>
|
||||
<td>{{ view.model.Resolution }} MP</td>
|
||||
</tr>
|
||||
<tr v-if="model.Faces > 0">
|
||||
<tr v-if="view.model.Faces > 0">
|
||||
<td>
|
||||
{{ $gettext(`Faces`) }}
|
||||
</td>
|
||||
<td>{{ model.Faces }}</td>
|
||||
<td>{{ view.model.Faces }}</td>
|
||||
</tr>
|
||||
<tr v-if="model.CameraSerial">
|
||||
<tr v-if="view.model.CameraSerial">
|
||||
<td>
|
||||
{{ $gettext(`Camera Serial`) }}
|
||||
</td>
|
||||
<td class="text-break">{{ model.CameraSerial }}</td>
|
||||
<td class="text-break">{{ view.model.CameraSerial }}</td>
|
||||
</tr>
|
||||
<tr v-if="model.Stack < 1">
|
||||
<tr v-if="view.model.Stack < 1">
|
||||
<td>
|
||||
{{ $gettext(`Stackable`) }}
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
v-model="model.Stack"
|
||||
v-model="view.model.Stack"
|
||||
hide-details
|
||||
class="input-stackable"
|
||||
:true-value="0"
|
||||
:false-value="-1"
|
||||
:label="model.Stack > -1 ? $gettext('Yes') : $gettext('No')"
|
||||
:label="view.model.Stack > -1 ? $gettext('Yes') : $gettext('No')"
|
||||
@update:model-value="save"
|
||||
></v-switch>
|
||||
</td>
|
||||
@@ -163,10 +169,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
v-model="model.Favorite"
|
||||
v-model="view.model.Favorite"
|
||||
hide-details
|
||||
class="input-favorite ml-2"
|
||||
:label="model.Favorite ? $gettext('Yes') : $gettext('No')"
|
||||
:label="view.model.Favorite ? $gettext('Yes') : $gettext('No')"
|
||||
@update:model-value="save"
|
||||
></v-switch>
|
||||
</td>
|
||||
@@ -177,10 +183,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
v-model="model.Private"
|
||||
v-model="view.model.Private"
|
||||
hide-details
|
||||
class="input-private ml-2"
|
||||
:label="model.Private ? $gettext('Yes') : $gettext('No')"
|
||||
:label="view.model.Private ? $gettext('Yes') : $gettext('No')"
|
||||
@update:model-value="save"
|
||||
></v-switch>
|
||||
</td>
|
||||
@@ -191,10 +197,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
v-model="model.Scan"
|
||||
v-model="view.model.Scan"
|
||||
hide-details
|
||||
class="input-scan ml-2"
|
||||
:label="model.Scan ? $gettext('Yes') : $gettext('No')"
|
||||
:label="view.model.Scan ? $gettext('Yes') : $gettext('No')"
|
||||
@update:model-value="save"
|
||||
></v-switch>
|
||||
</td>
|
||||
@@ -205,10 +211,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<v-switch
|
||||
v-model="model.Panorama"
|
||||
v-model="view.model.Panorama"
|
||||
hide-details
|
||||
class="input-panorama ml-2"
|
||||
:label="model.Panorama ? $gettext('Yes') : $gettext('No')"
|
||||
:label="view.model.Panorama ? $gettext('Yes') : $gettext('No')"
|
||||
@update:model-value="save"
|
||||
></v-switch>
|
||||
</td>
|
||||
@@ -218,43 +224,43 @@
|
||||
{{ $gettext(`Place`) }}
|
||||
</td>
|
||||
<td>
|
||||
<div v-tooltip="formatSource(model.PlaceSrc, $gettext('Missing'))" class="text-flex">
|
||||
<div>{{ model.locationInfo() }}</div>
|
||||
<v-icon v-if="model.PlaceSrc === 'estimate'" icon="mdi-map-clock-outline" class="src"></v-icon>
|
||||
<!-- v-icon v-else-if="model.PlaceSrc === 'meta'" icon="mdi-camera" class="src"></v-icon -->
|
||||
<v-icon v-else-if="model.PlaceSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
<div v-tooltip="formatSource(view.model.PlaceSrc, $gettext('Missing'))" class="text-flex">
|
||||
<div>{{ view.model.locationInfo() }}</div>
|
||||
<v-icon v-if="view.model.PlaceSrc === 'estimate'" icon="mdi-map-clock-outline" class="src"></v-icon>
|
||||
<!-- v-icon v-else-if="view.model.PlaceSrc === 'meta'" icon="mdi-camera" class="src"></v-icon -->
|
||||
<v-icon v-else-if="view.model.PlaceSrc === 'manual'" icon="mdi-check" class="src"></v-icon>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.Lat">
|
||||
<tr v-if="view.model.Lat">
|
||||
<td>
|
||||
{{ $gettext(`Latitude`) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ model.Lat }}
|
||||
{{ view.model.Lat }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.Lng">
|
||||
<tr v-if="view.model.Lng">
|
||||
<td>
|
||||
{{ $gettext(`Longitude`) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ model.Lng }}
|
||||
{{ view.model.Lng }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.Altitude">
|
||||
<tr v-if="view.model.Altitude">
|
||||
<td>
|
||||
{{ $gettext(`Altitude`) }}
|
||||
</td>
|
||||
<td>{{ model.Altitude }} m</td>
|
||||
<td>{{ view.model.Altitude }} m</td>
|
||||
</tr>
|
||||
<tr v-if="model.Lat">
|
||||
<tr v-if="view.model.Lat">
|
||||
<td>
|
||||
{{ $gettext(`Accuracy`) }}
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model="model.CellAccuracy"
|
||||
v-model="view.model.CellAccuracy"
|
||||
variant="solo"
|
||||
bg-color="transparent"
|
||||
density="compact"
|
||||
@@ -274,7 +280,7 @@
|
||||
{{ $gettext(`Created`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
{{ formatTime(model.CreatedAt) }}
|
||||
{{ formatTime(view.model.CreatedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -282,31 +288,31 @@
|
||||
{{ $gettext(`Updated`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
{{ formatTime(model.UpdatedAt) }}
|
||||
{{ formatTime(view.model.UpdatedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.EditedAt">
|
||||
<tr v-if="view.model.EditedAt">
|
||||
<td>
|
||||
{{ $gettext(`Edited`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
{{ formatTime(model.EditedAt) }}
|
||||
{{ formatTime(view.model.EditedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.CheckedAt">
|
||||
<tr v-if="view.model.CheckedAt">
|
||||
<td>
|
||||
{{ $gettext(`Checked`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
{{ formatTime(model.CheckedAt) }}
|
||||
{{ formatTime(view.model.CheckedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="model.DeletedAt">
|
||||
<tr v-if="view.model.DeletedAt">
|
||||
<td>
|
||||
{{ $gettext(`Archived`) }}
|
||||
</td>
|
||||
<td class="text-break">
|
||||
{{ formatTime(model.DeletedAt) }}
|
||||
{{ formatTime(view.model.DeletedAt) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -318,18 +324,14 @@
|
||||
|
||||
<script>
|
||||
import Thumb from "model/thumb";
|
||||
import { DateTime, Info } from "luxon";
|
||||
import { DateTime } from "luxon";
|
||||
import * as options from "options/options";
|
||||
import {$gettext, T} from "common/gettext";
|
||||
import { $gettext, T } from "common/gettext";
|
||||
import Util from "common/util";
|
||||
|
||||
export default {
|
||||
name: "PTabPhotoAdvanced",
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -337,31 +339,21 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
view: this.$view.data(),
|
||||
options: options,
|
||||
config: this.$config.values,
|
||||
readonly: this.$config.get("readonly"),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
monthOptions() {
|
||||
let result = [{ Month: -1, Name: this.$gettext("Unknown") }];
|
||||
|
||||
const months = Info.months("long");
|
||||
|
||||
for (let i = 0; i < months.length; i++) {
|
||||
result.push({ Month: i + 1, UserName: months[i] });
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
albums() {
|
||||
if (!this.model || !this.model.Albums || this.model.Albums.length < 1) {
|
||||
if (!this.view?.model || !this.view.model.Albums || this.view.model.Albums.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
this.model.Albums.forEach((a) => results.push({ title: a.Title, url: this.albumUrl(a) }));
|
||||
this.view.model.Albums.forEach((a) => results.push({ title: a.Title, url: this.albumUrl(a) }));
|
||||
|
||||
return results;
|
||||
},
|
||||
@@ -404,14 +396,11 @@ export default {
|
||||
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_MED);
|
||||
},
|
||||
save() {
|
||||
this.model.update();
|
||||
this.view.model.update();
|
||||
},
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
openPhoto() {
|
||||
this.$root.$refs.viewer.showThumbs(Thumb.fromFiles([this.model]), 0);
|
||||
},
|
||||
albumUrl(m) {
|
||||
if (!m) {
|
||||
return "#";
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<v-col cols="0" sm="2" class="form-thumb">
|
||||
<div>
|
||||
<img
|
||||
:alt="model.Title"
|
||||
:src="model.thumbnailUrl('tile_500')"
|
||||
:alt="view?.model.Title"
|
||||
:src="view?.model.thumbnailUrl('tile_500')"
|
||||
class="clickable"
|
||||
@click.stop.prevent.exact="openPhoto()"
|
||||
/>
|
||||
@@ -68,7 +68,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="label in model.Labels" :key="label.LabelID" class="label result">
|
||||
<tr v-for="label in view.model.Labels" :key="label.LabelID" class="label result">
|
||||
<td class="text-start">
|
||||
{{ label.Label.Name }}
|
||||
<!-- TODO: add this dialog later-->
|
||||
@@ -145,7 +145,6 @@
|
||||
flat
|
||||
variant="plain"
|
||||
hide-details
|
||||
autofocus
|
||||
class="input-label ma-0 pa-0"
|
||||
@keyup.enter="addLabel"
|
||||
></v-text-field>
|
||||
@@ -186,10 +185,6 @@ import Thumb from "model/thumb";
|
||||
export default {
|
||||
name: "PTabPhotoLabels",
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -197,6 +192,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
view: this.$view.data(),
|
||||
disabled: !this.$config.feature("edit"),
|
||||
config: this.$config.values,
|
||||
readonly: this.$config.get("readonly"),
|
||||
@@ -216,7 +212,6 @@ export default {
|
||||
nameRule: (v) => v.length <= this.$config.get("clip") || this.$gettext("Name too long"),
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
refresh() {},
|
||||
sourceName(s) {
|
||||
@@ -238,33 +233,33 @@ export default {
|
||||
}
|
||||
},
|
||||
removeLabel(label) {
|
||||
if (!label) {
|
||||
if (!label || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = label.Name;
|
||||
|
||||
this.model.removeLabel(label.ID).then((m) => {
|
||||
this.view.model.removeLabel(label.ID).then((m) => {
|
||||
this.$notify.success("removed " + name);
|
||||
});
|
||||
},
|
||||
addLabel() {
|
||||
if (!this.newLabel) {
|
||||
if (!this.newLabel || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.addLabel(this.newLabel).then((m) => {
|
||||
this.view.model.addLabel(this.newLabel).then((m) => {
|
||||
this.$notify.success("added " + this.newLabel);
|
||||
|
||||
this.newLabel = "";
|
||||
});
|
||||
},
|
||||
activateLabel(label) {
|
||||
if (!label) {
|
||||
if (!label || !this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.activateLabel(label.ID);
|
||||
this.view.model.activateLabel(label.ID);
|
||||
},
|
||||
// TODO: add this dialog later
|
||||
// renameLabel(label) {
|
||||
@@ -272,14 +267,18 @@ export default {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// this.model.renameLabel(label.ID, label.Name);
|
||||
// this.view.model.renameLabel(label.ID, label.Name);
|
||||
// },
|
||||
searchLabel(label) {
|
||||
this.$router.push({ name: "all", query: { q: "label:" + label.Slug } }).catch(() => {});
|
||||
this.$emit("close");
|
||||
},
|
||||
openPhoto() {
|
||||
this.$root.$refs.viewer.showThumbs(Thumb.fromFiles([this.model]), 0);
|
||||
if (!this.view?.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$lightbox.openModels(Thumb.fromFiles([this.view.model]), 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div v-else class="v-row search-results face-results cards-view d-flex">
|
||||
<div v-for="m in markers" :key="m.UID" class="v-col-12 v-col-sm-6 v-col-md-4 v-col-lg-3 d-flex">
|
||||
<v-card :data-id="m.UID" :class="m.classes()" class="result not-selectable flex-grow-1">
|
||||
<v-img :src="m.thumbnailUrl('tile_320')" :transition="false" aspect-ratio="1" class="card">
|
||||
<v-img :src="m.thumbnailUrl('tile_320')" aspect-ratio="1" class="card">
|
||||
<v-btn
|
||||
v-if="!m.SubjUID && !m.Invalid"
|
||||
:ripple="false"
|
||||
@@ -78,7 +78,6 @@
|
||||
hide-details
|
||||
single-line
|
||||
open-on-clear
|
||||
focused
|
||||
append-icon=""
|
||||
prepend-inner-icon="mdi-account-plus"
|
||||
density="comfortable"
|
||||
@@ -94,7 +93,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p-confirm-action
|
||||
:show="confirm.show"
|
||||
:visible="confirm.visible"
|
||||
icon="mdi-account-plus"
|
||||
:icon-size="42"
|
||||
:text="confirm?.model?.Name ? $gettext('Add %{s}?', { s: confirm.model.Name }) : $gettext('Add person?')"
|
||||
@@ -112,25 +111,22 @@ export default {
|
||||
name: "PTabPhotoPeople",
|
||||
components: { PConfirmAction },
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const view = this.$view.data();
|
||||
return {
|
||||
view,
|
||||
markers: view.model.getMarkers(true),
|
||||
busy: false,
|
||||
markers: this.model.getMarkers(true),
|
||||
imageUrl: this.model.thumbnailUrl("fit_720"),
|
||||
disabled: !this.$config.feature("edit"),
|
||||
config: this.$config.values,
|
||||
readonly: this.$config.get("readonly"),
|
||||
confirm: {
|
||||
show: false,
|
||||
visible: false,
|
||||
model: new Marker(),
|
||||
text: this.$gettext("Add person?"),
|
||||
},
|
||||
@@ -151,14 +147,15 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
model: function () {
|
||||
uid: function () {
|
||||
this.refresh();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
refresh() {
|
||||
this.markers = this.model.getMarkers(true);
|
||||
this.imageUrl = this.model.thumbnailUrl("fit_720");
|
||||
if (this.view.model) {
|
||||
this.markers = this.view.model.getMarkers(true);
|
||||
}
|
||||
},
|
||||
onReject(model) {
|
||||
if (this.busy || !model) return;
|
||||
@@ -226,7 +223,7 @@ export default {
|
||||
|
||||
model.Name = name;
|
||||
model.SubjUID = "";
|
||||
this.confirm.show = true;
|
||||
this.confirm.visible = true;
|
||||
},
|
||||
onConfirmSetName() {
|
||||
if (!this.confirm?.model?.Name) {
|
||||
@@ -236,7 +233,7 @@ export default {
|
||||
this.setName(this.confirm.model);
|
||||
},
|
||||
onCancelSetName() {
|
||||
this.confirm.show = false;
|
||||
this.confirm.visible = false;
|
||||
},
|
||||
setName(model) {
|
||||
if (this.busy || !model) {
|
||||
@@ -250,7 +247,7 @@ export default {
|
||||
this.$notify.unblockUI();
|
||||
this.busy = false;
|
||||
this.confirm.model = null;
|
||||
this.confirm.show = false;
|
||||
this.confirm.visible = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,21 +7,22 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Photo from "model/photo";
|
||||
import Thumb from "model/thumb";
|
||||
|
||||
export default {
|
||||
name: "PPhotoPreview",
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => new Photo(false),
|
||||
uid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const view = this.$view.data();
|
||||
return {
|
||||
url: this.model.thumbnailUrl("tile_500"),
|
||||
title: this.model.Title ? this.model.Title : "",
|
||||
view,
|
||||
url: view.model.thumbnailUrl("tile_500"),
|
||||
title: view.model.Title ? view.model.Title : "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -30,18 +31,18 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
model() {
|
||||
this.url = this.model.thumbnailUrl("tile_500");
|
||||
this.title = this.model.Title ? this.model.Title : "";
|
||||
uid() {
|
||||
this.url = this.view.model.thumbnailUrl("tile_500");
|
||||
this.title = this.view.model.Title ? this.view.model.view.Title : "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openPhoto() {
|
||||
if (!this.$viewer || !this.model) {
|
||||
if (!this.$lightbox || !this.view.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$refs.viewer.showThumbs(Thumb.fromFiles([this.model]), 0);
|
||||
this.$root.$refs.lightbox.showThumbs(Thumb.fromFiles([this.view.model]), 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
color="surface-variant"
|
||||
class="input-search background-inherit elevation-0"
|
||||
@update:modelValue="
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
updateFilter({ q: v });
|
||||
}
|
||||
@@ -309,7 +309,7 @@
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
<p-photo-delete-dialog
|
||||
:show="dialog.delete"
|
||||
:visible="dialog.delete"
|
||||
:text="$gettext('Are you sure you want to delete all archived pictures?')"
|
||||
:action="$gettext('Delete All')"
|
||||
@close="dialog.delete = false"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
:fullscreen="$vuetify.display.mdAndDown"
|
||||
scrim
|
||||
scrollable
|
||||
@@ -133,7 +133,7 @@ import { Duration } from "luxon";
|
||||
export default {
|
||||
name: "PPhotoUploadDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -173,10 +173,9 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
// Disable the browser scrollbar.
|
||||
this.$modal.enter();
|
||||
this.$view.enter(this);
|
||||
this.reset();
|
||||
this.isDemo = this.$config.get("demo");
|
||||
this.fileLimit = this.isDemo ? 3 : 0;
|
||||
@@ -195,7 +194,7 @@ export default {
|
||||
} else {
|
||||
this.reset();
|
||||
// Re-enable the browser scrollbar.
|
||||
this.$modal.leave();
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
:style="`background-image: url(${m.thumbnailUrl('tile_500')})`"
|
||||
class="preview"
|
||||
@touchstart.passive="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onClick($event, index)"
|
||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||
@touchend.stop="onClick($event, index)"
|
||||
@mousedown.stop="input.mouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
@mouseover="playLive(m)"
|
||||
@mouseleave="pauseLive(m)"
|
||||
@@ -126,9 +126,9 @@
|
||||
<button
|
||||
v-if="m.Type !== 'image' || m.isStack()"
|
||||
class="input-open"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
>
|
||||
<i v-if="m.Type === 'raw'" class="action-raw mdi mdi-raw" :title="$gettext('RAW')" />
|
||||
@@ -147,9 +147,9 @@
|
||||
v-if="m.Type === 'image' && selectMode"
|
||||
class="input-view"
|
||||
:title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onOpen($event, index)"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="onOpen($event, index)"
|
||||
>
|
||||
<i class="mdi mdi-magnify-plus-outline" />
|
||||
@@ -172,9 +172,9 @@
|
||||
-->
|
||||
<button
|
||||
class="input-select"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onSelect($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onSelect($event, index)"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="onSelect($event, index)"
|
||||
>
|
||||
<i class="mdi mdi-check-circle select-on" />
|
||||
@@ -184,9 +184,9 @@
|
||||
<button
|
||||
v-if="!isSharedView"
|
||||
class="input-favorite"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="toggleLike($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="toggleLike($event, index)"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="toggleLike($event, index)"
|
||||
>
|
||||
<i v-if="m.Favorite" class="mdi mdi-star text-favorite favorite-on" />
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
v-else
|
||||
:style="`background-image: url(${m.thumbnailUrl('tile_224')})`"
|
||||
class="preview"
|
||||
@touchstart="onMouseDown($event, index)"
|
||||
@touchend.stop.prevent="onClick($event, index)"
|
||||
@touchstart.passive="onMouseDown($event, index)"
|
||||
@touchend.stop="onClick($event, index)"
|
||||
@mousedown="onMouseDown($event, index)"
|
||||
@contextmenu.stop="onContextMenu($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
class="media result preview"
|
||||
@contextmenu.stop="onContextMenu($event, index)"
|
||||
@touchstart.passive="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onClick($event, index)"
|
||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||
@touchend.stop="onClick($event, index)"
|
||||
@mousedown.stop="input.mouseDown($event, index)"
|
||||
@click.stop.prevent="onClick($event, index)"
|
||||
@mouseover="playLive(m)"
|
||||
@mouseleave="pauseLive(m)"
|
||||
@@ -77,9 +77,9 @@
|
||||
<button
|
||||
v-if="m.Type !== 'image' || m.isStack()"
|
||||
class="input-open"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="onOpen($event, index, !isSharedView, m.Type === 'live')"
|
||||
>
|
||||
<i v-if="m.Type === 'raw'" class="action-raw mdi mdi-raw" :title="$gettext('RAW')"></i>
|
||||
@@ -94,9 +94,9 @@
|
||||
v-if="m.Type === 'image' && selectMode"
|
||||
class="input-view"
|
||||
:title="$gettext('View')"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onOpen($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onOpen($event, index)"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="onOpen($event, index)"
|
||||
>
|
||||
<i class="mdi mdi-magnify-plus-outline" />
|
||||
@@ -119,9 +119,9 @@
|
||||
-->
|
||||
<button
|
||||
class="input-select"
|
||||
@mousedown.stop.prevent="input.mouseDown($event, index)"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="onSelect($event, index)"
|
||||
@mousedown.stop="input.mouseDown($event, index)"
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="onSelect($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@click.stop.prevent="onSelect($event, index)"
|
||||
>
|
||||
@@ -132,9 +132,9 @@
|
||||
<button
|
||||
v-if="!isSharedView"
|
||||
class="input-favorite"
|
||||
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||
@touchend.stop.prevent="toggleLike($event, index)"
|
||||
@touchmove.stop.prevent
|
||||
@touchstart.stop="input.touchStart($event, index)"
|
||||
@touchend.stop="toggleLike($event, index)"
|
||||
@touchmove.stop
|
||||
@click.stop.prevent="toggleLike($event, index)"
|
||||
>
|
||||
<i v-if="m.Favorite" class="mdi mdi-star text-favorite favorite-on" />
|
||||
|
||||
@@ -76,7 +76,12 @@ export default class MapStyleControl {
|
||||
}
|
||||
|
||||
onDocumentClick(event) {
|
||||
if (this.controlContainer && !this.controlContainer.contains(event.target) && this.mapStyleContainer && this.styleButton) {
|
||||
if (
|
||||
this.controlContainer &&
|
||||
!this.controlContainer.contains(event.target) &&
|
||||
this.mapStyleContainer &&
|
||||
this.styleButton
|
||||
) {
|
||||
this.mapStyleContainer.style.display = "none";
|
||||
this.styleButton.style.display = "block";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<transition name="fade-transition">
|
||||
<button v-if="showButton" type="button" class="p-scroll" @click.stop="scrollToTop">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"></path>
|
||||
</svg>
|
||||
{{ $gettext(`Back to top`) }}
|
||||
</button>
|
||||
</transition>
|
||||
@@ -48,14 +50,13 @@ export default {
|
||||
showButton: false,
|
||||
showButtonDistance: 100,
|
||||
maxScrollY: 0,
|
||||
onScrollOptions: { passive: true },
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("scroll", this.onScroll, this.onScrollOptions);
|
||||
window.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("scroll", this.onScroll, this.onScrollOptions);
|
||||
window.removeEventListener("scroll", this.onScroll, false);
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
|
||||
@@ -1,67 +1,72 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="500" class="p-dialog p-service-add" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-swap-horizontal</v-icon>
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext(`Add Account`) }}
|
||||
</h6>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row align="center" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
hide-details
|
||||
autofocus
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccUser"
|
||||
hide-details
|
||||
:label="$gettext('Username')"
|
||||
:placeholder="$gettext('optional')"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
autocomplete="new-password"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Password')"
|
||||
:placeholder="$gettext('optional')"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" class="text-start text-caption">
|
||||
{{
|
||||
$gettext(
|
||||
`Note: Only WebDAV servers, like Nextcloud or PhotoPrism, can be configured as remote service for backup and file upload.`
|
||||
)
|
||||
}}
|
||||
{{ $gettext(`Support for additional services, like Google Drive, will be added over time.`) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
<span>{{ label.cancel }}</span>
|
||||
</v-btn>
|
||||
<v-btn variant="flat" color="highlight" class="action-confirm" @click.stop="confirm">
|
||||
<span>{{ label.confirm }}</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-dialog :model-value="visible" persistent max-width="500" class="p-dialog p-service-add" @keydown.esc="close">
|
||||
<v-form ref="form" validate-on="invalid-input" accept-charset="UTF-8" @submit.prevent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-swap-horizontal</v-icon>
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext(`Add Account`) }}
|
||||
</h6>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row align="center" dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
hide-details
|
||||
autofocus
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccUser"
|
||||
hide-details
|
||||
:label="$gettext('Username')"
|
||||
:placeholder="$gettext('optional')"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
autocomplete="new-password"
|
||||
:label="$gettext('Password')"
|
||||
:placeholder="$gettext('optional')"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" class="text-start text-caption">
|
||||
{{
|
||||
$gettext(
|
||||
`Note: Only WebDAV servers, like Nextcloud or PhotoPrism, can be configured as remote service for backup and file upload.`
|
||||
)
|
||||
}}
|
||||
{{ $gettext(`Support for additional services, like Google Drive, will be added over time.`) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
<span>{{ label.cancel }}</span>
|
||||
</v-btn>
|
||||
<v-btn variant="flat" color="highlight" class="action-confirm" @click.stop="confirm">
|
||||
<span>{{ label.confirm }}</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
@@ -71,15 +76,14 @@ import * as options from "options/options";
|
||||
export default {
|
||||
name: "PServiceAdd",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: options,
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
search: null,
|
||||
model: new Service(false),
|
||||
model: new Service(),
|
||||
label: {
|
||||
cancel: this.$gettext("Cancel"),
|
||||
confirm: this.$gettext("Connect"),
|
||||
@@ -87,20 +91,37 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function () {},
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.loading = false;
|
||||
this.showPassword = false;
|
||||
this.model = new Service();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
confirm() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.model.save().then((a) => {
|
||||
this.loading = false;
|
||||
this.$notify.success(this.$gettext("Account created"));
|
||||
this.$emit("confirm", a.UID);
|
||||
});
|
||||
this.model
|
||||
.save()
|
||||
.then((a) => {
|
||||
this.$notify.success(this.$gettext("Account created"));
|
||||
this.$emit("confirm", a.UID);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,242 +1,244 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="500" class="p-dialog p-service-edit" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title v-if="scope === 'sharing'" class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Manual Upload") }}
|
||||
</h6>
|
||||
<v-switch v-model="model.AccShare" :disabled="model.AccType !== 'webdav'"></v-switch>
|
||||
</v-card-title>
|
||||
<v-card-title v-else-if="scope === 'sync'" class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Remote Sync") }}
|
||||
</h6>
|
||||
<v-switch v-model="model.AccSync" :disabled="model.AccType !== 'webdav'"></v-switch>
|
||||
</v-card-title>
|
||||
<v-card-title v-else class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Edit Account") }}
|
||||
</h6>
|
||||
<v-btn icon variant="text" class="action-remove" @click.stop.prevent="remove()">
|
||||
<v-icon color="surface-variant">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row v-if="scope === 'sharing'" dense>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="model.SharePath"
|
||||
hide-details
|
||||
hide-no-data
|
||||
flat
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Default Folder')"
|
||||
:disabled="!model.AccShare || loading"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="input-share-size">
|
||||
<v-select
|
||||
v-model="model.ShareSize"
|
||||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Size')"
|
||||
autocomplete="off"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.ThumbSizes()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.ShareExpires"
|
||||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Expires')"
|
||||
autocomplete="off"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Expires()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else-if="scope === 'sync'" dense>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
v-model="model.SyncPath"
|
||||
hide-details
|
||||
hide-no-data
|
||||
flat
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Folder')"
|
||||
:disabled="!model.AccSync || loading"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.SyncInterval"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Interval')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Intervals()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncDownload"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync || readonly"
|
||||
hide-details
|
||||
true-icon="mdi-radiobox-marked"
|
||||
false-icon="mdi-radiobox-blank"
|
||||
:label="$gettext('Download remote files')"
|
||||
@update:model-value="onChangeSync('download')"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncUpload"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
true-icon="mdi-radiobox-marked"
|
||||
false-icon="mdi-radiobox-blank"
|
||||
:label="$gettext('Upload local files')"
|
||||
hide-details
|
||||
@update:model-value="onChangeSync('upload')"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncFilenames"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Preserve filenames')"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncRaw"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Sync raw and video files')"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccName"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:label="$gettext('Name')"
|
||||
placeholder=""
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
autocomplete="off"
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="model.AccUser" autocomplete="off" :label="$gettext('Username')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
autocomplete="new-password"
|
||||
:label="$gettext('Password')"
|
||||
placeholder="********"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccKey"
|
||||
hide-details
|
||||
flat
|
||||
autocomplete="off"
|
||||
:label="$gettext('API Key')"
|
||||
placeholder="********"
|
||||
color="surface-variant"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="input-account-type">
|
||||
<v-select
|
||||
v-model="model.AccType"
|
||||
:label="$gettext('Type')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.AccountTypes()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.AccTimeout"
|
||||
:label="$gettext('Timeout')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Timeouts()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.RetryLimit"
|
||||
:label="$gettext('Retry Limit')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.RetryLimits()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons mt-4">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
{{ $gettext(`Cancel`) }}
|
||||
</v-btn>
|
||||
<v-btn variant="flat" color="highlight" class="action-save" @click.stop="save">
|
||||
{{ $gettext(`Save`) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-dialog :model-value="visible" persistent max-width="500" class="p-dialog p-service-edit" @keydown.esc="close">
|
||||
<v-form ref="form" validate-on="invalid-input" accept-charset="UTF-8" @submit.prevent>
|
||||
<v-card>
|
||||
<v-card-title v-if="scope === 'sharing'" class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Manual Upload") }}
|
||||
</h6>
|
||||
<v-switch v-model="model.AccShare" :disabled="model.AccType !== 'webdav'"></v-switch>
|
||||
</v-card-title>
|
||||
<v-card-title v-else-if="scope === 'sync'" class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Remote Sync") }}
|
||||
</h6>
|
||||
<v-switch v-model="model.AccSync" :disabled="model.AccType !== 'webdav'"></v-switch>
|
||||
</v-card-title>
|
||||
<v-card-title v-else class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">
|
||||
{{ $gettext("Edit Account") }}
|
||||
</h6>
|
||||
<v-btn icon variant="text" class="action-remove" @click.stop.prevent="remove()">
|
||||
<v-icon color="surface-variant">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row v-if="scope === 'sharing'" dense>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="model.SharePath"
|
||||
hide-details
|
||||
hide-no-data
|
||||
flat
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Default Folder')"
|
||||
:disabled="!model.AccShare || loading"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="input-share-size">
|
||||
<v-select
|
||||
v-model="model.ShareSize"
|
||||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Size')"
|
||||
autocomplete="off"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.ThumbSizes()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.ShareExpires"
|
||||
:disabled="!model.AccShare"
|
||||
:label="$gettext('Expires')"
|
||||
autocomplete="off"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Expires()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else-if="scope === 'sync'" dense>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
v-model="model.SyncPath"
|
||||
hide-details
|
||||
hide-no-data
|
||||
flat
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Folder')"
|
||||
:disabled="!model.AccSync || loading"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.SyncInterval"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Interval')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Intervals()"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncDownload"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync || readonly"
|
||||
hide-details
|
||||
true-icon="mdi-radiobox-marked"
|
||||
false-icon="mdi-radiobox-blank"
|
||||
:label="$gettext('Download remote files')"
|
||||
@update:model-value="onChangeSync('download')"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncUpload"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
true-icon="mdi-radiobox-marked"
|
||||
false-icon="mdi-radiobox-blank"
|
||||
:label="$gettext('Upload local files')"
|
||||
hide-details
|
||||
@update:model-value="onChangeSync('upload')"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncFilenames"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Preserve filenames')"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-checkbox
|
||||
v-model="model.SyncRaw"
|
||||
density="comfortable"
|
||||
:disabled="!model.AccSync"
|
||||
:label="$gettext('Sync raw and video files')"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccName"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:label="$gettext('Name')"
|
||||
placeholder=""
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="model.AccURL"
|
||||
autocomplete="off"
|
||||
:label="$gettext('Service URL')"
|
||||
placeholder="https://www.example.com/"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field v-model="model.AccUser" autocomplete="off" :label="$gettext('Username')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccPass"
|
||||
hide-details
|
||||
autocomplete="new-password"
|
||||
:label="$gettext('Password')"
|
||||
placeholder="********"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="model.AccKey"
|
||||
hide-details
|
||||
flat
|
||||
autocomplete="off"
|
||||
:label="$gettext('API Key')"
|
||||
placeholder="********"
|
||||
color="surface-variant"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="input-account-type">
|
||||
<v-select
|
||||
v-model="model.AccType"
|
||||
:label="$gettext('Type')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.AccountTypes()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.AccTimeout"
|
||||
:label="$gettext('Timeout')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.Timeouts()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="model.RetryLimit"
|
||||
:label="$gettext('Retry Limit')"
|
||||
autocomplete="off"
|
||||
hide-details
|
||||
flat
|
||||
color="surface-variant"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
:items="options.RetryLimits()"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons mt-4">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
{{ $gettext(`Cancel`) }}
|
||||
</v-btn>
|
||||
<v-btn variant="flat" color="highlight" class="action-save" @click.stop="save">
|
||||
{{ $gettext(`Save`) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
@@ -245,7 +247,7 @@ import * as options from "options/options";
|
||||
export default {
|
||||
name: "PServiceEdit",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
scope: {
|
||||
type: String,
|
||||
default: "",
|
||||
@@ -283,9 +285,14 @@ export default {
|
||||
this.pathItems = this.paths.concat([{ abs: q }]);
|
||||
}
|
||||
},
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.loading = false;
|
||||
this.showPassword = false;
|
||||
this.onChange();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -332,6 +339,10 @@ export default {
|
||||
}
|
||||
},
|
||||
onChange() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onChangeSync();
|
||||
this.paths = [{ abs: "/" }];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="350" class="p-dialog p-service-delete" @keydown.esc="close">
|
||||
<v-dialog :model-value="visible" persistent max-width="350" class="p-dialog p-service-delete" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="54" color="primary">mdi-delete-outline</v-icon>
|
||||
@@ -20,7 +20,7 @@
|
||||
export default {
|
||||
name: "PServiceDelete",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -31,18 +31,36 @@ export default {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
confirm() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.model.remove().then(() => {
|
||||
this.loading = false;
|
||||
this.$notify.success(this.$gettext("Account deleted"));
|
||||
this.$emit("confirm");
|
||||
});
|
||||
this.model
|
||||
.remove()
|
||||
.then(() => {
|
||||
this.$notify.success(this.$gettext("Account deleted"));
|
||||
this.$emit("confirm");
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,72 +1,74 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="400" class="p-dialog p-service-upload" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-cloud</v-icon>
|
||||
<h6 class="text-h6">{{ $gettext(`WebDAV Upload`) }}</h6>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row align="center" dense>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="service"
|
||||
hide-details
|
||||
hide-no-data
|
||||
:label="$gettext('Account')"
|
||||
item-title="AccName"
|
||||
item-value="ID"
|
||||
return-object
|
||||
:disabled="loading || noServices"
|
||||
:items="services"
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
v-model="path"
|
||||
hide-details
|
||||
hide-no-data
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
:disabled="loading || noServices"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Folder')"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
{{ $gettext(`Cancel`) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="noServices"
|
||||
:disabled="isPublic && !isDemo"
|
||||
color="highlight"
|
||||
variant="flat"
|
||||
class="action-setup"
|
||||
@click.stop="setup"
|
||||
>
|
||||
{{ $gettext(`Setup`) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
:disabled="noServices"
|
||||
color="highlight"
|
||||
variant="flat"
|
||||
class="action-upload"
|
||||
@click.stop="confirm"
|
||||
>
|
||||
{{ $gettext(`Upload`) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-dialog :model-value="visible" persistent max-width="400" class="p-dialog p-service-upload" @keydown.esc="close">
|
||||
<v-form ref="form" validate-on="invalid-input" accept-charset="UTF-8" @submit.prevent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-cloud</v-icon>
|
||||
<h6 class="text-h6">{{ $gettext(`WebDAV Upload`) }}</h6>
|
||||
</v-card-title>
|
||||
<v-card-text class="dense">
|
||||
<v-row align="center" dense>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="service"
|
||||
hide-details
|
||||
hide-no-data
|
||||
:label="$gettext('Account')"
|
||||
item-title="AccName"
|
||||
item-value="ID"
|
||||
return-object
|
||||
:disabled="loading || noServices"
|
||||
:items="services"
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-autocomplete
|
||||
:model-value="service ? path : null"
|
||||
hide-details
|
||||
hide-no-data
|
||||
autocomplete="off"
|
||||
:hint="$gettext('Folder')"
|
||||
:search.sync="search"
|
||||
:items="pathItems"
|
||||
:loading="loading"
|
||||
:disabled="loading || noServices"
|
||||
item-title="abs"
|
||||
item-value="abs"
|
||||
:label="$gettext('Folder')"
|
||||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
{{ $gettext(`Cancel`) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="noServices"
|
||||
:disabled="isPublic && !isDemo"
|
||||
color="highlight"
|
||||
variant="flat"
|
||||
class="action-setup"
|
||||
@click.stop="setup"
|
||||
>
|
||||
{{ $gettext(`Setup`) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
:disabled="noServices || !service"
|
||||
color="highlight"
|
||||
variant="flat"
|
||||
class="action-upload"
|
||||
@click.stop="confirm"
|
||||
>
|
||||
{{ $gettext(`Upload`) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script>
|
||||
@@ -76,7 +78,7 @@ import Selection from "common/selection";
|
||||
export default {
|
||||
name: "PServiceUpload",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
items: {
|
||||
type: Object,
|
||||
default: null,
|
||||
@@ -91,9 +93,9 @@ export default {
|
||||
isDemo: this.$config.get("demo"),
|
||||
isPublic: this.$config.get("public"),
|
||||
noServices: false,
|
||||
loading: true,
|
||||
loading: false,
|
||||
search: null,
|
||||
service: {},
|
||||
service: null,
|
||||
services: [],
|
||||
selection: new Selection({}),
|
||||
path: "/",
|
||||
@@ -104,7 +106,9 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
search(q) {
|
||||
if (this.loading) return;
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = this.paths.findIndex((p) => p.value === q);
|
||||
|
||||
@@ -116,11 +120,14 @@ export default {
|
||||
this.pathItems = this.paths.concat([{ abs: q }]);
|
||||
}
|
||||
},
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.loading = false;
|
||||
this.load();
|
||||
} else if (this.selection) {
|
||||
this.selection.clear();
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -157,9 +164,14 @@ export default {
|
||||
.catch(() => (this.loading = false));
|
||||
},
|
||||
onChange() {
|
||||
this.paths = [{ abs: "/" }];
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.paths = [{ abs: "/" }];
|
||||
|
||||
this.service
|
||||
.Folders()
|
||||
.then((p) => {
|
||||
@@ -173,6 +185,10 @@ export default {
|
||||
.finally(() => (this.loading = false));
|
||||
},
|
||||
load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.selection.clear().addItems(this.items);
|
||||
@@ -199,13 +215,14 @@ export default {
|
||||
this.noServices = true;
|
||||
this.loading = false;
|
||||
this.services.length = 0;
|
||||
this.service = null;
|
||||
} else {
|
||||
this.service = response.models[0];
|
||||
this.services = response.models;
|
||||
this.onChange();
|
||||
}
|
||||
})
|
||||
.catch(() => (this.loading = false));
|
||||
.finally(() => (this.loading = false));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="610"
|
||||
class="p-dialog modal-dialog p-settings-apps"
|
||||
@@ -234,7 +234,7 @@
|
||||
</v-card>
|
||||
</v-form>
|
||||
<p-confirm-action
|
||||
:show="revoke.dialog"
|
||||
:visible="revoke.dialog"
|
||||
icon="mdi-delete-outline"
|
||||
@close="revoke.dialog = false"
|
||||
@confirm="onRevoked"
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
PConfirmAction,
|
||||
},
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => new User(null),
|
||||
@@ -324,10 +324,13 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.reset();
|
||||
this.find();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="500"
|
||||
class="p-dialog modal-dialog p-settings-passcode"
|
||||
@@ -253,7 +253,7 @@
|
||||
export default {
|
||||
name: "PSettingsPasscode",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => this.$session.getUser(),
|
||||
@@ -295,9 +295,12 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.reset();
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
:model-value="visible"
|
||||
persistent
|
||||
max-width="500"
|
||||
class="p-dialog modal-dialog p-settings-password"
|
||||
@@ -96,7 +96,7 @@ import User from "model/user";
|
||||
export default {
|
||||
name: "PSettingsPassword",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => new User(null),
|
||||
@@ -127,6 +127,15 @@ export default {
|
||||
return !sessionUser.SuperAdmin || this.model.getId() === sessionUser.getId();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.isPublic && !this.isDemo) {
|
||||
this.$emit("close");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog v-model="visible" max-width="580" class="p-dialog p-settings-webdav">
|
||||
<v-dialog :model-value="visible" persistent max-width="580" class="p-dialog p-settings-webdav" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-swap-horizontal</v-icon>
|
||||
@@ -80,21 +80,19 @@
|
||||
export default {
|
||||
name: "PSettingsWebdav",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
user: this.$session.getUser(),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show(val) {
|
||||
this.visible = val;
|
||||
},
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.close();
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" persistent max-width="540" class="p-dialog p-share-dialog" @keydown.esc="close">
|
||||
<v-dialog :model-value="visible" persistent max-width="540" class="p-dialog p-share-dialog" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center ga-3">
|
||||
<h6 class="text-h6">{{ $gettext(`Share %{s}`, { s: model.modelName() }) }}</h6>
|
||||
@@ -135,7 +135,7 @@ import * as options from "options/options";
|
||||
export default {
|
||||
name: "PShareDialog",
|
||||
props: {
|
||||
show: Boolean,
|
||||
visible: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -161,8 +161,9 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show: function (show) {
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
this.links = [];
|
||||
this.loading = true;
|
||||
this.expanded = [];
|
||||
@@ -177,6 +178,8 @@ export default {
|
||||
}
|
||||
})
|
||||
.finally(() => (this.loading = false));
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-dialog :model-value="show" max-width="400" class="p-dialog p-update">
|
||||
<v-dialog :model-value="visible" persistent max-width="400" class="p-dialog p-update" @keydown.esc="close">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-start align-center flex-nowrap ga-3">
|
||||
<v-icon icon="mdi-alert-decagram-outline" size="28" color="primary"></v-icon>
|
||||
@@ -27,23 +27,17 @@
|
||||
export default {
|
||||
name: "PUpdate",
|
||||
props: {
|
||||
show: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: this.show,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
show(val) {
|
||||
this.visible = val;
|
||||
},
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.close();
|
||||
visible: function (show) {
|
||||
if (show) {
|
||||
this.$view.enter(this);
|
||||
} else {
|
||||
this.$view.leave(this);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ Additional information can be found in our Developer Guide:
|
||||
@import url("splash.css");
|
||||
@import url("body.css");
|
||||
@import url("text.css");
|
||||
@import url("viewer.css");
|
||||
@import url("lightbox.css");
|
||||
@import url("controls.css");
|
||||
@import url("wallpapers.css");
|
||||
@import url("themes.css");
|
||||
@@ -91,7 +91,7 @@ main {
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
padding: .4rem .8rem;
|
||||
padding: .5rem 1rem;
|
||||
margin: auto;
|
||||
opacity: 0.87;
|
||||
color: rgb(var(--v-theme-on-button, #333333));
|
||||
|
||||
@@ -6,33 +6,6 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-color-mode=light][data-light-theme*=dark],
|
||||
[data-color-mode=dark][data-dark-theme*=dark] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root,
|
||||
[data-color-mode=light][data-light-theme*=light],
|
||||
[data-color-mode=dark][data-dark-theme*=light] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body.nojs::-webkit-scrollbar,
|
||||
body.viewer::-webkit-scrollbar,
|
||||
body.player::-webkit-scrollbar,
|
||||
body.hide-scrollbar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.nojs,
|
||||
body.viewer,
|
||||
body.player,
|
||||
body.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
body.dark-theme {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
@@ -69,18 +42,17 @@ body.firefox.dark-theme {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(242, 242, 242, .26);
|
||||
background-color: rgba(var(--v-theme-secondary), .20);
|
||||
border: solid transparent;
|
||||
border-width: 0 0 0 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, .64);
|
||||
background-color: rgba(var(--v-theme-on-surface), .28);
|
||||
border-style: solid;
|
||||
border-color: rgba(242, 242, 242, .26);
|
||||
border-color: rgba(var(--v-theme-secondary), .56);
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
min-height: 28px;
|
||||
padding: 100px 0 0;
|
||||
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ footer.p-about-footer {
|
||||
padding: 12px 24px 4px;
|
||||
}
|
||||
|
||||
footer.p-about-footer p {
|
||||
footer.p-about-footer>div {
|
||||
user-select: text;
|
||||
font-size: .875rem;
|
||||
font-weight: 400;
|
||||
@@ -233,27 +233,24 @@ footer.p-about-footer p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer.p-about-footer p > * {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
footer.p-about-footer > div > * {
|
||||
display: inline;
|
||||
float: right;
|
||||
max-width: 45vw;
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
/* footer.p-about-footer p > *,
|
||||
footer.p-about-footer div > * {
|
||||
display: inline-block;
|
||||
} */
|
||||
/* .footer .text-link {
|
||||
float: left;
|
||||
footer.p-about-footer > div > strong {
|
||||
float: left;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
footer.p-about-footer>div > * {
|
||||
display: block;
|
||||
float: none!important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer .body-link {
|
||||
float: right;
|
||||
} */
|
||||
}
|
||||
|
||||
/* .footer .footer-actions {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
} */
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
/* Photo/Video Viewer Styles */
|
||||
/* Media Viewer Styles */
|
||||
|
||||
.p-viewer {
|
||||
.p-lightbox {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
z-index: 2500;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.p-viewer__content {
|
||||
.p-lightbox__container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -26,7 +26,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.p-viewer__underlay {
|
||||
.p-lightbox__underlay {
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
position: absolute;
|
||||
@@ -44,7 +44,7 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox {
|
||||
.p-lightbox__container > .p-lightbox__content {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -53,7 +53,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__sidebar {
|
||||
.p-lightbox__container > .p-lightbox__sidebar {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
@@ -62,7 +62,7 @@
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
/* Image and Video Content */
|
||||
/* Media Content */
|
||||
|
||||
.pswp__content {
|
||||
display: flex;
|
||||
@@ -90,7 +90,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Dynamic Content Captions */
|
||||
/* Dynamic Captions */
|
||||
|
||||
.pswp__dynamic-caption {
|
||||
position: absolute;
|
||||
@@ -102,15 +102,15 @@
|
||||
color: #f3f3f3;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.slideshow-active .pswp__dynamic-caption {
|
||||
.p-lightbox__container > .p-lightbox__content.slideshow-active .pswp__dynamic-caption {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.sidebar-visible .pswp__dynamic-caption {
|
||||
.p-lightbox__container > .p-lightbox__content.sidebar-visible .pswp__dynamic-caption {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox:not(.slideshow-active) .pswp__dynamic-caption--faded {
|
||||
.p-lightbox__container > .p-lightbox__content:not(.slideshow-active) .pswp__dynamic-caption--faded {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.is-playable .pswp__dynamic-caption--mobile {
|
||||
.p-lightbox__container > .is-playable .pswp__dynamic-caption--mobile {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
@@ -200,7 +200,7 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Top Bar Button and Toggle Controls */
|
||||
/* Top Bar Controls */
|
||||
|
||||
.pswp__top-bar {
|
||||
align-items: center;
|
||||
@@ -240,63 +240,78 @@
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Zoom Button */
|
||||
/* Zoom Toggle */
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.is-playable .pswp__button.pswp__button--zoom,
|
||||
.p-viewer__content > .p-viewer__lightbox.slideshow-active .pswp__button.pswp__button--zoom {
|
||||
.p-lightbox__container > .is-playable .pswp__button.pswp__button--zoom,
|
||||
.p-lightbox__container > .p-lightbox__content.slideshow-active .pswp__button.pswp__button--zoom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Slideshow Toggle Icon and Animation */
|
||||
/* Slideshow Toggle and Animation */
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.slideshow-active .pswp__button--slideshow-toggle .pswp__icn-slideshow-off {
|
||||
.p-lightbox__container > .p-lightbox__content.slideshow-active .pswp__button--slideshow-toggle .pswp__icn-slideshow-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox:not(.slideshow-active) .pswp__button--slideshow-toggle .pswp__icn-slideshow-on {
|
||||
.p-lightbox__container > .p-lightbox__content:not(.slideshow-active) .pswp__button--slideshow-toggle .pswp__icn-slideshow-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pswp__container.slideshow-active {
|
||||
transition: transform 0.8s ease-in-out;
|
||||
.p-lightbox__container > .slideshow-active .pswp__container {
|
||||
transition: transform .8s ease-in-out;
|
||||
}
|
||||
|
||||
/* Fullscreen Toggle Icon */
|
||||
/* Fullscreen Toggle */
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.is-fullscreen .pswp__button--fullscreen-toggle .pswp__icn-fullscreen-off {
|
||||
.p-lightbox__container > .p-lightbox__content.is-fullscreen .pswp__button--fullscreen-toggle .pswp__icn-fullscreen-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox:not(.is-fullscreen) .pswp__button--fullscreen-toggle .pswp__icn-fullscreen-on {
|
||||
.p-lightbox__container > .p-lightbox__content:not(.is-fullscreen) .pswp__button--fullscreen-toggle .pswp__icn-fullscreen-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Favorite Toggle Icon */
|
||||
/* Favorite/Dislike Toggle */
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.is-favorite .pswp__button--favorite-toggle .pswp__icn-favorite-off {
|
||||
.p-lightbox__container > .p-lightbox__content.is-favorite .pswp__button--favorite-toggle .pswp__icn-favorite-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox:not(.is-favorite) .pswp__button--favorite-toggle .pswp__icn-favorite-on {
|
||||
.p-lightbox__container > .p-lightbox__content:not(.is-favorite) .pswp__button--favorite-toggle .pswp__icn-favorite-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Selection Toggle Icon */
|
||||
/* Sound Mute/Unmute Toggle */
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox.is-selected .pswp__button--select-toggle .pswp__icn-select-off {
|
||||
.p-lightbox__container > .p-lightbox__content:not(.is-playable) .pswp__button--sound-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-viewer__content > .p-viewer__lightbox:not(.is-selected) .pswp__button--select-toggle .pswp__icn-select-on {
|
||||
.p-lightbox__container > .p-lightbox__content.is-muted .pswp__button--sound-toggle .pswp__icn-sound-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Navigation Arrows */
|
||||
.p-lightbox__container > .p-lightbox__content:not(.is-muted) .pswp__button--sound-toggle .pswp__icn-sound-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Selection Toggle */
|
||||
|
||||
.p-lightbox__container > .p-lightbox__content.is-selected .pswp__button--select-toggle .pswp__icn-select-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-lightbox__container > .p-lightbox__content:not(.is-selected) .pswp__button--select-toggle .pswp__icn-select-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Navigation Arrow Buttons */
|
||||
|
||||
.pswp__button--arrow {
|
||||
height: 220px;
|
||||
margin-top: -110px;
|
||||
height: 50vh;
|
||||
margin-top: -25vh;
|
||||
width: 150px;
|
||||
max-width: 16vw;
|
||||
}
|
||||
|
||||
.pswp__button--arrow .pswp__icn {
|
||||
@@ -304,6 +319,17 @@
|
||||
margin-top: -25px;
|
||||
top: 50%;
|
||||
width: 50px;
|
||||
max-width: 16vw;
|
||||
}
|
||||
|
||||
/* Allow navigation buttons on touch devices to be used without being visible. */
|
||||
|
||||
.p-lightbox__container > .p-lightbox__content > .pswp--touch .pswp__button--arrow {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.p-lightbox__container > .p-lightbox__content > .pswp--touch .pswp__button--arrow .pswp__icn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Other / Unused Styles */
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Event Logs */
|
||||
/* Library Event Logs */
|
||||
|
||||
.p-logs {
|
||||
color: white;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
font-size: 0.825rem;
|
||||
font-family: monospace;
|
||||
white-space: normal;
|
||||
color: white;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -26,28 +26,24 @@
|
||||
}
|
||||
|
||||
.p-log-message span {
|
||||
color: #FEFEFE;
|
||||
color: rgba(var(--v-theme-on-surface), .95);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.p-log-fatal,
|
||||
.p-log-critical,
|
||||
.p-log-error {
|
||||
color: #F48FB1;
|
||||
color: rgba(var(--v-theme-error), .95);
|
||||
}
|
||||
|
||||
.p-log-warning {
|
||||
color: #FFD600;
|
||||
}
|
||||
|
||||
.p-log-fatal {
|
||||
color: #FFECB3;
|
||||
color: rgba(var(--v-theme-warning), .95);
|
||||
}
|
||||
|
||||
.p-log-info {
|
||||
color: #82B1FF;
|
||||
color: rgba(var(--v-theme-info), .95);
|
||||
}
|
||||
|
||||
.p-log-debug {
|
||||
color: #DDDDDD;
|
||||
color: rgba(var(--v-theme-on-surface), .9);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#p-navigation {
|
||||
z-index: 10;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
nav .v-list__item__title.title {
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
/* Global Font Presets */
|
||||
/* Inline <HTML>, <Body>, and Splash Screen Styles */
|
||||
|
||||
html.loading {
|
||||
overflow-y: hidden !important;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
html.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
[data-color-mode=light][data-light-theme*=dark],
|
||||
[data-color-mode=dark][data-dark-theme*=dark] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root,
|
||||
[data-color-mode=light][data-light-theme*=light],
|
||||
[data-color-mode=dark][data-dark-theme*=light] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #333333;
|
||||
@@ -28,14 +48,30 @@ body {
|
||||
font-feature-settings: 'kern', 'liga';
|
||||
}
|
||||
|
||||
body.nojs::-webkit-scrollbar,
|
||||
body.hide-scrollbar::-webkit-scrollbar,
|
||||
html.hide-scrollbar ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
z-index: -1000;
|
||||
}
|
||||
|
||||
body.nojs,
|
||||
body.hide-scrollbar,
|
||||
html.hide-scrollbar body {
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Loading Animation Styles */
|
||||
|
||||
#busy-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
touch-action: none;
|
||||
transition: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
@@ -73,10 +73,22 @@ body.dark-theme {
|
||||
|
||||
/* Dialogs & Overlays */
|
||||
|
||||
.v-overlay-container {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.v-overlay {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v-dialog,
|
||||
.v-dialog > *,
|
||||
.v-overlay,
|
||||
.v-overlay > *,
|
||||
.v-overlay .v-overlay__scrim {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.v-overlay.v-dialog.v-dialog--sidepanel:not(.v-dialog--fullscreen) {
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
@@ -111,8 +123,6 @@ div.v-dialog.v-dialog--fullscreen > div.v-card {
|
||||
|
||||
.v-overlay.v-dialog.v-dialog--sidepanel:not(.v-dialog--fullscreen) > .v-overlay__content > .v-card {
|
||||
border-radius: 8px 0 0 8px;
|
||||
/* border-radius: 8px 0 0 8px; */
|
||||
/* border-radius: 0; */
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-left-style: solid;
|
||||
border-left-width: thin;
|
||||
@@ -234,12 +244,18 @@ div.v-dialog.v-dialog--fullscreen > div.v-card {
|
||||
}
|
||||
|
||||
.v-input--density-compact {
|
||||
--v-input-control-height: 32x;
|
||||
--v-input-control-height: 32px;
|
||||
--v-input-padding-top: 8px;
|
||||
}
|
||||
|
||||
.v-input--density-comfortable {
|
||||
--v-input-control-height: 40px;
|
||||
--v-input-control-height: 48px;
|
||||
--v-input-padding-top: 12px;
|
||||
}
|
||||
|
||||
.v-input--density-comfortable .v-field {
|
||||
--v-field-padding-start: 12px;
|
||||
--v-field-padding-end: 12px;
|
||||
}
|
||||
|
||||
.v-selection-control--density-compact .v-label {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user