Frontend: Limit clipboard size to 500 items #481

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-08-28 11:04:53 +02:00
parent b39d660686
commit a845ebd4ce
8 changed files with 1744 additions and 1731 deletions

View file

@ -46,15 +46,15 @@
}
},
"@babel/core": {
"version": "7.11.1",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz",
"integrity": "sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ==",
"version": "7.11.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.4.tgz",
"integrity": "sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg==",
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.0",
"@babel/generator": "^7.11.4",
"@babel/helper-module-transforms": "^7.11.0",
"@babel/helpers": "^7.10.4",
"@babel/parser": "^7.11.1",
"@babel/parser": "^7.11.4",
"@babel/template": "^7.10.4",
"@babel/traverse": "^7.11.0",
"@babel/types": "^7.11.0",
@ -68,18 +68,20 @@
"source-map": "^0.5.0"
},
"dependencies": {
"@babel/helper-split-export-declaration": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz",
"integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==",
"@babel/generator": {
"version": "7.11.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz",
"integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==",
"requires": {
"@babel/types": "^7.11.0"
"@babel/types": "^7.11.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/parser": {
"version": "7.11.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz",
"integrity": "sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA=="
"version": "7.11.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz",
"integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA=="
},
"@babel/template": {
"version": "7.10.4",
@ -91,32 +93,6 @@
"@babel/types": "^7.10.4"
}
},
"@babel/traverse": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz",
"integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==",
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/generator": "^7.11.0",
"@babel/helper-function-name": "^7.10.4",
"@babel/helper-split-export-declaration": "^7.11.0",
"@babel/parser": "^7.11.0",
"@babel/types": "^7.11.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
}
},
"@babel/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz",
"integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==",
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -7130,9 +7106,9 @@
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"hls.js": {
"version": "0.14.9",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.9.tgz",
"integrity": "sha512-5j1ONTvIzcIxCtg2eafikFbZ3b/9fDhR6u871LmK7jZ44/Qdc2G4xaSBCwcVK61gz7kTyiobaAhB++2M4J58rQ==",
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.10.tgz",
"integrity": "sha512-muBt5Gxhapgk2uV8aEKRYe9GJ6AI5niFfDuOd8ZXHga519RNJ0+QAeRPdEpl1QKMqj1lT/6r/WKVnLL+ePB6ow==",
"requires": {
"eventemitter3": "^4.0.3",
"url-toolkit": "^2.1.6"
@ -8589,9 +8565,9 @@
}
},
"luxon": {
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.24.1.tgz",
"integrity": "sha512-CgnIMKAWT0ghcuWFfCWBnWGOddM0zu6c4wZAWmD0NN7MZTnro0+833DF6tJep+xlxRPg4KtsYEHYLfTMBQKwYg=="
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz",
"integrity": "sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ=="
},
"make-dir": {
"version": "2.1.0",
@ -8953,22 +8929,22 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"mocha": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.1.tgz",
"integrity": "sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.2.tgz",
"integrity": "sha512-I8FRAcuACNMLQn3lS4qeWLxXqLvGf6r2CaLstDpZmMUUSmvW6Cnm1AuHxgbc7ctZVRcfwspCRbDHymPsi3dkJw==",
"requires": {
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.3.1",
"debug": "3.2.6",
"chokidar": "3.4.2",
"debug": "4.1.1",
"diff": "4.0.2",
"escape-string-regexp": "1.0.5",
"find-up": "4.1.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.1.6",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "3.13.1",
"log-symbols": "3.0.0",
"js-yaml": "3.14.0",
"log-symbols": "4.0.0",
"minimatch": "3.0.4",
"ms": "2.1.2",
"object.assign": "4.1.0",
@ -9011,30 +8987,10 @@
"fill-range": "^7.0.1"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"chokidar": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
"integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
"integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
@ -9043,17 +8999,22 @@
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.3.0"
"readdirp": "~3.4.0"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -9063,11 +9024,11 @@
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"requires": {
"locate-path": "^5.0.0",
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
@ -9085,6 +9046,11 @@
"is-glob": "^4.0.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -9098,29 +9064,20 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"requires": {
"p-locate": "^4.1.0"
"p-locate": "^5.0.0"
}
},
"log-symbols": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
"integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
"requires": {
"chalk": "^2.4.2"
"chalk": "^4.0.0"
}
},
"ms": {
@ -9128,12 +9085,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"p-limit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
"integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
"requires": {
"p-limit": "^2.2.0"
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"requires": {
"p-limit": "^3.0.2"
}
},
"path-exists": {
@ -9142,19 +9107,11 @@
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"readdirp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
"integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"requires": {
"picomatch": "^2.0.7"
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
"picomatch": "^2.2.1"
}
},
"strip-json-comments": {
@ -9168,13 +9125,6 @@
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"requires": {
"has-flag": "^4.0.0"
},
"dependencies": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
}
}
},
"to-regex-range": {
@ -13629,9 +13579,9 @@
}
},
"vue": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"vue-fullscreen": {
"version": "2.1.6",
@ -13783,9 +13733,9 @@
}
},
"vue-template-compiler": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz",
"integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==",
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz",
"integrity": "sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.1.0"

View file

@ -20,7 +20,7 @@
},
"dependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/core": "^7.11.4",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.11.0",
@ -62,7 +62,7 @@
"eventsource-polyfill": "^0.9.6",
"file-loader": "^3.0.1",
"friendly-errors-webpack-plugin": "^1.7.0",
"hls.js": "^0.14.9",
"hls.js": "^0.14.10",
"html-webpack-plugin": "^4.3.0",
"http-proxy-middleware": "^1.0.5",
"i18n-iso-countries": "^6.0.0",
@ -74,13 +74,13 @@
"karma-mocha": "^2.0.1",
"karma-verbose-reporter": "^0.0.6",
"karma-webpack": "^4.0.2",
"luxon": "^1.24.1",
"luxon": "^1.25.0",
"mapbox-gl": "^1.12.0",
"material-design-icons-iconfont": "^5.0.1",
"mediaelement": "^4.2.16",
"mini-css-extract-plugin": "^0.7.0",
"minimist": "^1.2.5",
"mocha": "^8.1.1",
"mocha": "^8.1.2",
"moment-timezone": "^0.5.31",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"ora": "^4.1.1",
@ -107,7 +107,7 @@
"tar": "^6.0.5",
"truncate": "^2.1.0",
"url-loader": "^1.1.2",
"vue": "^2.6.11",
"vue": "^2.6.12",
"vue-fullscreen": "^2.1.6",
"vue-gettext": "^2.1.10",
"vue-infinite-scroll": "^2.0.2",
@ -116,7 +116,7 @@
"vue-luxon": "^0.7.3",
"vue-router": "^3.4.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"vue-template-compiler": "^2.6.12",
"vue2-filters": "^0.11.0",
"vuelidate": "^0.7.5",
"vuetify": "^1.5.24",

View file

@ -29,8 +29,12 @@ https://docs.photoprism.org/developer-guide/
*/
import RestModel from "model/rest";
import Notify from "common/notify";
import {$gettext} from "./vm";
class Clipboard {
export const MaxItems = 500;
export default class Clipboard {
/**
* @param {Storage} storage
* @param {string} key
@ -42,6 +46,7 @@ class Clipboard {
this.selectionMap = {};
this.selection = [];
this.lastId = "";
this.maxItems = MaxItems;
this.loadFromStorage();
}
@ -59,7 +64,7 @@ class Clipboard {
}
toggle(model) {
if(!model || !(model instanceof RestModel)) {
if (!model || !(model instanceof RestModel)) {
console.log("Clipboard::toggle() - not a model:", model);
return;
}
@ -72,6 +77,11 @@ class Clipboard {
const index = this.selection.indexOf(id);
if (index === -1) {
if (this.selection.length >= this.maxItems) {
Notify.warn($gettext("Can't select more items"));
return;
}
this.selection.push(id);
this.selectionMap["id:" + id] = true;
this.lastId = id;
@ -85,7 +95,7 @@ class Clipboard {
}
add(model) {
if(!model || !(model instanceof RestModel)) {
if (!model || !(model instanceof RestModel)) {
console.log("Clipboard::add() - not a model:", model);
return;
}
@ -96,7 +106,14 @@ class Clipboard {
}
addId(id) {
if (this.hasId(id)) return;
if (this.hasId(id)) {
return;
}
if (this.selection.length >= this.maxItems) {
Notify.warn($gettext("Can't select more items"));
return;
}
this.selection.push(id);
this.selectionMap["id:" + id] = true;
@ -106,14 +123,14 @@ class Clipboard {
}
addRange(rangeEnd, models) {
if(!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("Clipboard::addRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((photo) => photo.UID === this.lastId);
if(rangeStart === -1) {
if (rangeStart === -1) {
this.toggle(models[rangeEnd]);
return 1;
}
@ -132,7 +149,7 @@ class Clipboard {
}
has(model) {
if(!model || !(model instanceof RestModel)) {
if (!model || !(model instanceof RestModel)) {
console.log("Clipboard::has() - not a model:", model);
return;
}
@ -145,7 +162,7 @@ class Clipboard {
}
remove(model) {
if(!model || !(model instanceof RestModel)) {
if (!model || !(model instanceof RestModel)) {
console.log("Clipboard::remove() - not a model:", model);
return;
}
@ -188,5 +205,3 @@ class Clipboard {
this.storage.removeItem(this.storageKey);
}
}
export default Clipboard;

View file

@ -6,4 +6,5 @@
#photoprism main .p-inline-edit a,
#photoprism main .p-inline-edit a span {
cursor: text;
color: inherit;
}

View file

@ -78,11 +78,11 @@
</v-card>
<v-layout row wrap class="p-album-results">
<v-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 lg3 xl2 d-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 lg3 xl2 d-flex
>
<v-hover>
<v-card tile class="accent lighten-3"
@ -93,18 +93,18 @@
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
>
<v-img
:src="album.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
:src="album.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
@ -154,7 +154,8 @@
</v-btn>
</v-card-actions>
<v-card-text primary-title class="pb-2 pt-0 p-photo-desc" style="user-select: none;" @click.stop.prevent="">
<v-card-text primary-title class="pb-2 pt-0 p-photo-desc" style="user-select: none;"
@click.stop.prevent="">
<div class="caption mb-2" v-if="album.Description">
<button @click.exact="edit(album)">
{{ album.Description | truncate(100) }}
@ -195,426 +196,438 @@
</template>
<script>
import Album from "model/album";
import {DateTime} from "luxon";
import Event from "pubsub-js";
import RestModel from "model/rest";
import Album from "model/album";
import {DateTime} from "luxon";
import Event from "pubsub-js";
import RestModel from "model/rest";
import {MaxItems} from "../common/clipboard";
import Notify from "../common/notify";
export default {
name: 'p-page-albums',
props: {
staticFilter: Object,
view: String,
},
watch: {
'$route'() {
const query = this.$route.query;
export default {
name: 'p-page-albums',
props: {
staticFilter: Object,
view: String,
},
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query["q"] ? query["q"] : "";
this.filter.category = query["category"] ? query["category"] : "";
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query["q"] ? query["q"] : "";
const category = query["category"] ? query["category"] : "";
const filter = {q, category};
const settings = {};
this.filter.q = query["q"] ? query["q"] : "";
this.filter.category = query["category"] ? query["category"] : "";
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query["q"] ? query["q"] : "";
const category = query["category"] ? query["category"] : "";
const filter = {q, category};
const settings = {};
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
return {
featureShare: this.$config.feature('share'),
categories: categories,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
labels: {
search: this.$gettext("Search"),
title: this.$gettext("Album Name"),
category: this.$gettext("Category"),
},
titles: {
reload: this.$gettext("Reload"),
upload: this.$gettext("Upload"),
add: this.$gettext("Add Album"),
},
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
dialog: {
share: false,
upload: false,
edit: false,
},
album: new Album(),
};
},
methods: {
share(album) {
this.album = album;
this.dialog.share = true;
},
edit(album) {
this.album = album;
this.dialog.edit = true;
},
webdavUpload() {
this.dialog.share = false;
this.dialog.upload = true;
},
showUpload() {
Event.publish("dialog.upload");
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
clearQuery() {
this.filter.q = '';
this.search();
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Album.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} albums loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Album.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
if (!this.results.length) {
this.$notify.warn(this.$gettext("No albums found"));
} else if (this.results.length === 1) {
this.$notify.info(this.$gettext("One album found"));
} else {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} albums found"), {n: this.results.length}));
}
} else {
this.$notify.info(this.$gettext('More than 20 albums found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
create() {
let title = DateTime.local().toFormat("LLLL yyyy");
if (this.results.findIndex(a => a.Title.startsWith(title)) !== -1) {
const existing = this.results.filter(a => a.Title.startsWith(title));
title = `${title} (${existing.length + 1})`
}
const album = new Album({"Title": title, "Favorite": true});
album.save();
},
onSave(album) {
album.update();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
}
}
}
}
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
this.categories = categories;
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const index = this.results.findIndex((m) => m.UID === values.UID);
if (index === -1 && this.staticFilter.type === values.Type) {
this.results.unshift(new Album(values));
}
}
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.search();
this.subscriptions.push(Event.subscribe("albums", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
return {
featureShare: this.$config.feature('share'),
categories: categories,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
labels: {
search: this.$gettext("Search"),
title: this.$gettext("Album Name"),
category: this.$gettext("Category"),
},
titles: {
reload: this.$gettext("Reload"),
upload: this.$gettext("Upload"),
add: this.$gettext("Add Album"),
},
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
dialog: {
share: false,
upload: false,
edit: false,
},
album: new Album(),
};
},
methods: {
share(album) {
this.album = album;
this.dialog.share = true;
},
edit(album) {
this.album = album;
this.dialog.edit = true;
},
webdavUpload() {
this.dialog.share = false;
this.dialog.upload = true;
},
showUpload() {
Event.publish("dialog.upload");
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
clearQuery() {
this.filter.q = '';
this.search();
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Album.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} albums loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Album.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
if (!this.results.length) {
this.$notify.warn(this.$gettext("No albums found"));
} else if (this.results.length === 1) {
this.$notify.info(this.$gettext("One album found"));
} else {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} albums found"), {n: this.results.length}));
}
} else {
this.$notify.info(this.$gettext('More than 20 albums found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
create() {
let title = DateTime.local().toFormat("LLLL yyyy");
if (this.results.findIndex(a => a.Title.startsWith(title)) !== -1) {
const existing = this.results.filter(a => a.Title.startsWith(title));
title = `${title} (${existing.length + 1})`
}
const album = new Album({"Title": title, "Favorite": true});
album.save();
},
onSave(album) {
album.update();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
}
}
}
}
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.albumCategories().length > 0) {
categories = categories.concat(this.$config.albumCategories().map(cat => {
return {"value": cat, "text": cat};
}));
}
this.categories = categories;
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const index = this.results.findIndex((m) => m.UID === values.UID);
if (index === -1 && this.staticFilter.type === values.Type) {
this.results.unshift(new Album(values));
}
}
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.search();
this.subscriptions.push(Event.subscribe("albums", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
};
</script>

View file

@ -56,11 +56,11 @@
</v-card>
<v-layout row wrap class="p-label-results">
<v-flex
v-for="(label, index) in results"
:key="index"
class="p-label"
:data-uid="label.UID"
xs6 sm4 md3 lg2 d-flex
v-for="(label, index) in results"
:key="index"
class="p-label"
:data-uid="label.UID"
xs6 sm4 md3 lg2 d-flex
>
<v-hover>
<v-card tile class="accent lighten-3"
@ -70,18 +70,18 @@
:class="selection.includes(label.UID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
:to="{name: 'browse', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.Slug)}}">
<v-img
:src="label.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
:src="label.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
@ -109,10 +109,10 @@
<v-card-actions @click.stop.prevent="">
<v-edit-dialog
:return-value.sync="label.Name"
lazy
@save="onSave(label)"
class="p-inline-edit"
:return-value.sync="label.Name"
lazy
@save="onSave(label)"
class="p-inline-edit"
>
<span v-if="label.Name" class="body-2 ma-0">
{{ label.Name | capitalize }}
@ -122,12 +122,12 @@
</span>
<template v-slot:input>
<v-text-field
v-model="label.Name"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
single-line
autofocus
v-model="label.Name"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
single-line
autofocus
></v-text-field>
</template>
</v-edit-dialog>
@ -148,360 +148,372 @@
</template>
<script>
import Label from "model/label";
import Event from "pubsub-js";
import RestModel from "model/rest";
import Label from "model/label";
import Event from "pubsub-js";
import RestModel from "model/rest";
import {MaxItems} from "../common/clipboard";
import Notify from "../common/notify";
export default {
name: 'p-page-labels',
props: {
staticFilter: Object
},
watch: {
'$route'() {
const query = this.$route.query;
export default {
name: 'p-page-labels',
props: {
staticFilter: Object
},
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query['q'] ? query['q'] : '';
this.filter.all = query['all'] ? query['all'] : '';
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : '';
const filter = {q: q, all: all};
const settings = {};
this.filter.q = query['q'] ? query['q'] : '';
this.filter.all = query['all'] ? query['all'] : '';
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : '';
const filter = {q: q, all: all};
const settings = {};
return {
config: this.$config.values,
subscriptions: [],
listen: false,
dirty: false,
results: [],
scrollDisabled: true,
loading: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
},
titles: {
reload: this.$gettext("Reload"),
more: this.$gettext("Show more"),
less: this.$gettext("Show less"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
};
},
methods: {
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if(this.results[index]) {
this.selectRange(index, this.results);
}
}
},
onSave(label) {
label.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
},
showImportant() {
this.filter.all = "";
this.updateQuery();
},
clearQuery() {
this.filter.q = '';
this.updateQuery();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Label.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} labels loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Label.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} labels found"), {n: this.results.length}));
} else {
this.$notify.info(this.$gettext('More than 20 labels found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.search();
this.subscriptions.push(Event.subscribe("labels", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
return {
config: this.$config.values,
subscriptions: [],
listen: false,
dirty: false,
results: [],
scrollDisabled: true,
loading: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
},
titles: {
reload: this.$gettext("Reload"),
more: this.$gettext("Show more"),
less: this.$gettext("Show less"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
};
},
methods: {
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
onSave(label) {
label.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
},
showImportant() {
this.filter.all = "";
this.updateQuery();
},
clearQuery() {
this.filter.q = '';
this.updateQuery();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Label.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} labels loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Label.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} labels found"), {n: this.results.length}));
} else {
this.$notify.info(this.$gettext('More than 20 labels found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.search();
this.subscriptions.push(Event.subscribe("labels", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
};
</script>

View file

@ -9,7 +9,7 @@
<router-link v-for="(item, index) in breadcrumbs" :key="index" :to="item.path">
<v-icon>navigate_next</v-icon>
{{item.name}}
{{ item.name }}
</router-link>
</v-toolbar-title>
@ -46,11 +46,11 @@
</v-card>
<v-layout row wrap class="p-files-results">
<v-flex
v-for="(model, index) in results"
:key="index"
:data-uid="model.UID"
class="p-file"
xs6 sm4 md3 lg2 d-flex
v-for="(model, index) in results"
:key="index"
:data-uid="model.UID"
class="p-file"
xs6 sm4 md3 lg2 d-flex
>
<v-hover>
<v-card tile class="accent lighten-3 clickable"
@ -59,18 +59,18 @@
:dark="selection.includes(model.UID)"
:class="selection.includes(model.UID) ? 'elevation-10 ma-0 darken-1 white--text' : 'elevation-0 ma-1 lighten-3'">
<v-img
:src="model.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
:src="model.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
@ -121,362 +121,372 @@
</template>
<script>
import Event from "pubsub-js";
import RestModel from "model/rest";
import Thumb from "model/thumb";
import {Folder} from "model/folder";
import {Photo, TypeJpeg} from "model/photo";
import Event from "pubsub-js";
import RestModel from "model/rest";
import {Folder} from "model/folder";
import Notify from "common/notify";
import {MaxItems} from "common/clipboard";
export default {
name: 'p-page-files',
props: {
staticFilter: Object
},
watch: {
'$route'() {
const query = this.$route.query;
export default {
name: 'p-page-files',
props: {
staticFilter: Object
},
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query['q'] ? query['q'] : '';
this.filter.all = query['all'] ? query['all'] : '';
this.lastFilter = {};
this.routeName = this.$route.name;
this.path = this.$route.params.pathMatch;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : '';
const filter = {q: q, all: all};
const settings = {};
this.filter.q = query['q'] ? query['q'] : '';
this.filter.all = query['all'] ? query['all'] : '';
this.lastFilter = {};
this.routeName = this.$route.name;
this.path = this.$route.params.pathMatch;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query['q'] ? query['q'] : '';
const all = query['all'] ? query['all'] : '';
const filter = {q: q, all: all};
const settings = {};
return {
config: this.$config.values,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
path: "",
page: 0,
files: {
limit: Folder.limit(),
offset: 0,
},
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Folder Name"),
},
titles: {
reload: this.$gettext("Reload"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
breadcrumbs: [],
};
},
methods: {
getBreadcrumbs() {
let result = [];
let path = "/library/files";
const crumbs = this.path.split("/");
crumbs.forEach(dir => {
if (dir) {
path += "/" + dir
result.push({path: path, name: dir})
}
})
return result;
},
openFile(index) {
const model = this.results[index];
if (model.isFile()) {
// Open Edit Dialog
Event.publish("dialog.edit", {selection: [model.PhotoUID], album: null, index: 0});
} else {
this.$router.push({path: '/library/files/' + model.Path});
}
},
downloadFile(index) {
const model = this.results[index];
const link = document.createElement('a')
link.href = `/api/v1/dl/${model.Hash}?t=${this.$config.downloadToken()}`;
link.download = model.Name;
link.click()
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
} else {
this.openFile(index);
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
onSave(model) {
model.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
},
showImportant() {
this.filter.all = "";
this.updateQuery();
},
clearQuery() {
this.filter.q = '';
this.updateQuery();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
files: true,
uncached: true,
count: this.files.limit,
offset: this.files.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.search();
},
search() {
// Don't query the same data more than once
if (!this.dirty && (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter))) {
this.loading = false;
this.listen = true;
return;
}
Object.assign(this.lastFilter, this.filter);
this.files.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Folder.originals(this.path, params).then(response => {
this.files.offset = this.files.limit;
this.results = response.models;
this.breadcrumbs = this.getBreadcrumbs();
if (response.count === 0) {
this.$notify.warn(this.$gettext('Folder is empty'));
} else if (response.files === 1) {
this.$notify.info(this.$gettext('One file found'));
} else if (response.files === 0 && response.folders === 1) {
this.$notify.info(this.$gettext('One folder found'));
} else if (response.files === 0 && response.folders > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} folders found"), {n: response.folders}));
} else if (response.files < this.files.limit) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("Folder contains %{n} files"), {n: response.files}));
} else {
this.$notify.warn(this.$gettextInterpolate(this.$gettext("Limit reached, showing first %{n} files"), {n: response.files}));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const ppid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === ppid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(ppid)
}
break;
case 'created':
this.dirty = true;
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.path = this.$route.params.pathMatch;
this.search();
this.subscriptions.push(Event.subscribe("folders", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
return {
config: this.$config.values,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
path: "",
page: 0,
files: {
limit: Folder.limit(),
offset: 0,
},
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Folder Name"),
},
titles: {
reload: this.$gettext("Reload"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
breadcrumbs: [],
};
},
methods: {
getBreadcrumbs() {
let result = [];
let path = "/library/files";
const crumbs = this.path.split("/");
crumbs.forEach(dir => {
if (dir) {
path += "/" + dir
result.push({path: path, name: dir})
}
})
return result;
},
openFile(index) {
const model = this.results[index];
if (model.isFile()) {
// Open Edit Dialog
Event.publish("dialog.edit", {selection: [model.PhotoUID], album: null, index: 0});
} else {
this.$router.push({path: '/library/files/' + model.Path});
}
},
downloadFile(index) {
const model = this.results[index];
const link = document.createElement('a')
link.href = `/api/v1/dl/${model.Hash}?t=${this.$config.downloadToken()}`;
link.download = model.Name;
link.click()
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
} else {
this.openFile(index);
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
onSave(model) {
model.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
},
showImportant() {
this.filter.all = "";
this.updateQuery();
},
clearQuery() {
this.filter.q = '';
this.updateQuery();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
files: true,
uncached: true,
count: this.files.limit,
offset: this.files.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.search();
},
search() {
// Don't query the same data more than once
if (!this.dirty && (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter))) {
this.loading = false;
this.listen = true;
return;
}
Object.assign(this.lastFilter, this.filter);
this.files.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Folder.originals(this.path, params).then(response => {
this.files.offset = this.files.limit;
this.results = response.models;
this.breadcrumbs = this.getBreadcrumbs();
if (response.count === 0) {
this.$notify.warn(this.$gettext('Folder is empty'));
} else if (response.files === 1) {
this.$notify.info(this.$gettext('One file found'));
} else if (response.files === 0 && response.folders === 1) {
this.$notify.info(this.$gettext('One folder found'));
} else if (response.files === 0 && response.folders > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} folders found"), {n: response.folders}));
} else if (response.files < this.files.limit) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("Folder contains %{n} files"), {n: response.files}));
} else {
this.$notify.warn(this.$gettextInterpolate(this.$gettext("Limit reached, showing first %{n} files"), {n: response.files}));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const ppid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === ppid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(ppid)
}
break;
case 'created':
this.dirty = true;
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
this.path = this.$route.params.pathMatch;
this.search();
this.subscriptions.push(Event.subscribe("folders", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
};
</script>

View file

@ -26,11 +26,11 @@
</v-card>
<v-layout row wrap class="p-album-results">
<v-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 lg3 xl2 d-flex
v-for="(album, index) in results"
:key="index"
:data-uid="album.UID"
class="p-album"
xs6 sm4 lg3 xl2 d-flex
>
<v-hover>
<v-card tile class="accent lighten-3"
@ -41,18 +41,18 @@
:to="{name: view, params: {uid: album.UID, slug: album.Slug, year: album.Year, month: album.Month}}"
>
<v-img
:src="album.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
:src="album.thumbnailUrl('tile_500')"
@mousedown="onMouseDown($event, index)"
@click="onClick($event, index)"
aspect-ratio="1"
class="accent lighten-2"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
@ -94,399 +94,411 @@
</template>
<script>
import Album from "model/album";
import {DateTime} from "luxon";
import Event from "pubsub-js";
import RestModel from "model/rest";
import Album from "model/album";
import {DateTime} from "luxon";
import Event from "pubsub-js";
import RestModel from "model/rest";
import {MaxItems} from "../common/clipboard";
import Notify from "../common/notify";
export default {
name: 'p-page-albums',
props: {
staticFilter: Object,
view: String,
},
watch: {
'$route'() {
const query = this.$route.query;
export default {
name: 'p-page-albums',
props: {
staticFilter: Object,
view: String,
},
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query["q"] ? query["q"] : "";
this.filter.category = query["category"] ? query["category"] : "";
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query["q"] ? query["q"] : "";
const category = query["category"] ? query["category"] : "";
const filter = {q, category};
const settings = {};
this.filter.q = query["q"] ? query["q"] : "";
this.filter.category = query["category"] ? query["category"] : "";
this.lastFilter = {};
this.routeName = this.$route.name;
this.search();
}
},
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const q = query["q"] ? query["q"] : "";
const category = query["category"] ? query["category"] : "";
const filter = {q, category};
const settings = {};
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
let categories = [{"value": "", "text": this.$gettext("All Categories")}];
if (this.$config.values.albumCategories) {
categories = categories.concat(this.$config.values.albumCategories.map(cat => {
return {"value": cat, "text": cat};
}));
}
if (this.$config.values.albumCategories) {
categories = categories.concat(this.$config.values.albumCategories.map(cat => {
return {"value": cat, "text": cat};
}));
}
return {
categories: categories,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
labels: {
search: this.$gettext("Search"),
title: this.$gettext("Album Name"),
category: this.$gettext("Category"),
},
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
};
},
methods: {
showUpload() {
Event.publish("dialog.upload");
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
clearQuery() {
this.filter.q = '';
this.search();
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Album.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} albums loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Album.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
if (!this.results.length) {
this.$notify.warn(this.$gettext("No albums found"));
} else if (this.results.length === 1) {
this.$notify.info(this.$gettext("One album found"));
} else {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} albums found"), {n: this.results.length}));
}
} else {
this.$notify.info(this.$gettext('More than 20 albums found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
create() {
let title = DateTime.local().toFormat("LLLL yyyy");
if (this.results.findIndex(a => a.Title.startsWith(title)) !== -1) {
const existing = this.results.filter(a => a.Title.startsWith(title));
title = `${title} (${existing.length + 1})`
}
const album = new Album({"Title": title, "Favorite": true});
album.save();
},
onSave(album) {
album.update();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
}
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const index = this.results.findIndex((m) => m.UID === values.UID);
if (index === -1 && this.staticFilter.type === values.Type) {
this.results.unshift(new Album(values));
}
}
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
const token = this.$route.params.token;
if (this.$session.hasToken(token)) {
this.search();
} else {
this.$session.redeemToken(token).then(() => {
this.search();
});
}
this.subscriptions.push(Event.subscribe("albums", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
return {
categories: categories,
subscriptions: [],
listen: false,
dirty: false,
results: [],
loading: true,
scrollDisabled: true,
pageSize: 24,
offset: 0,
page: 0,
selection: [],
settings: settings,
filter: filter,
lastFilter: {},
routeName: routeName,
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
labels: {
search: this.$gettext("Search"),
title: this.$gettext("Album Name"),
category: this.$gettext("Category"),
},
mouseDown: {
index: -1,
timeStamp: -1,
},
lastId: "",
};
},
methods: {
showUpload() {
Event.publish("dialog.upload");
},
selectRange(rangeEnd, models) {
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
return;
}
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
if (rangeStart === -1) {
this.toggleSelection(models[rangeEnd].getId());
return 1;
}
if (rangeStart > rangeEnd) {
const newEnd = rangeStart;
rangeStart = rangeEnd;
rangeEnd = newEnd;
}
for (let i = rangeStart; i <= rangeEnd; i++) {
this.addSelection(models[i].getId());
}
return (rangeEnd - rangeStart) + 1;
},
onSelect(ev, index) {
if (ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
},
onMouseDown(ev, index) {
this.mouseDown.index = index;
this.mouseDown.timeStamp = ev.timeStamp;
},
onClick(ev, index) {
let longClick = (this.mouseDown.index === index && ev.timeStamp - this.mouseDown.timeStamp > 400);
if (longClick || this.selection.length > 0) {
ev.preventDefault();
ev.stopPropagation();
if (longClick || ev.shiftKey) {
this.selectRange(index, this.results);
} else {
this.toggleSelection(this.results[index].getId());
}
}
},
onContextMenu(ev, index) {
if (this.$isMobile) {
ev.preventDefault();
ev.stopPropagation();
if (this.results[index]) {
this.selectRange(index, this.results);
}
}
},
clearQuery() {
this.filter.q = '';
this.search();
},
loadMore() {
if (this.scrollDisabled) return;
this.scrollDisabled = true;
this.listen = false;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Object.assign(params, this.lastFilter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
Album.search(params).then(response => {
this.results = this.dirty ? response.models : this.results.concat(response.models);
this.scrollDisabled = (response.models.length < count);
if (this.scrollDisabled) {
this.offset = offset;
if (this.results.length > 1) {
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} albums loaded"), {n: this.results.length}));
}
} else {
this.offset = offset + count;
this.page++;
}
}).catch(() => {
this.scrollDisabled = false;
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
updateQuery() {
this.filter.q = this.filter.q.trim();
const len = this.filter.q.length;
if (len > 1 && len < 3) {
this.$notify.error(this.$gettext("Search term too short"));
return;
}
const query = {
view: this.settings.view
};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query: query});
},
searchParams() {
const params = {
count: this.pageSize,
offset: this.offset,
};
Object.assign(params, this.filter);
if (this.staticFilter) {
Object.assign(params, this.staticFilter);
}
return params;
},
search() {
this.scrollDisabled = true;
// Don't query the same data more than once
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
this.$nextTick(() => this.$emit("scrollRefresh"));
return;
}
Object.assign(this.lastFilter, this.filter);
this.offset = 0;
this.page = 0;
this.loading = true;
this.listen = false;
const params = this.searchParams();
Album.search(params).then(response => {
this.offset = this.pageSize;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
if (this.scrollDisabled) {
if (!this.results.length) {
this.$notify.warn(this.$gettext("No albums found"));
} else if (this.results.length === 1) {
this.$notify.info(this.$gettext("One album found"));
} else {
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} albums found"), {n: this.results.length}));
}
} else {
this.$notify.info(this.$gettext('More than 20 albums found'));
this.$nextTick(() => this.$emit("scrollRefresh"));
}
}).finally(() => {
this.dirty = false;
this.loading = false;
this.listen = true;
});
},
refresh() {
if (this.loading) return;
this.loading = true;
this.page = 0;
this.dirty = true;
this.scrollDisabled = false;
this.loadMore();
},
create() {
let title = DateTime.local().toFormat("LLLL yyyy");
if (this.results.findIndex(a => a.Title.startsWith(title)) !== -1) {
const existing = this.results.filter(a => a.Title.startsWith(title));
title = `${title} (${existing.length + 1})`
}
const album = new Album({"Title": title, "Favorite": true});
album.save();
},
onSave(album) {
album.update();
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos === -1) {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid)
this.lastId = uid;
}
},
toggleSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
} else {
if (this.selection.length >= MaxItems) {
Notify.warn(this.$gettext("Can't select more items"));
return;
}
this.selection.push(uid);
this.lastId = uid;
}
},
removeSelection(uid) {
const pos = this.selection.indexOf(uid);
if (pos !== -1) {
this.selection.splice(pos, 1);
this.lastId = "";
}
},
clearSelection() {
this.selection.splice(0, this.selection.length);
this.lastId = "";
},
onUpdate(ev, data) {
if (!this.listen) return;
if (!data || !data.entities) {
return
}
const type = ev.split('.')[1];
switch (type) {
case 'updated':
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.UID === values.UID);
if (model) {
for (let key in values) {
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
model[key] = values[key];
}
}
}
}
break;
case 'deleted':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const uid = data.entities[i];
const index = this.results.findIndex((m) => m.UID === uid);
if (index >= 0) {
this.results.splice(index, 1);
}
this.removeSelection(uid)
}
break;
case 'created':
this.dirty = true;
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const index = this.results.findIndex((m) => m.UID === values.UID);
if (index === -1 && this.staticFilter.type === values.Type) {
this.results.unshift(new Album(values));
}
}
break;
default:
console.warn("unexpected event type", ev);
}
}
},
created() {
const token = this.$route.params.token;
if (this.$session.hasToken(token)) {
this.search();
} else {
this.$session.redeemToken(token).then(() => {
this.search();
});
}
this.subscriptions.push(Event.subscribe("albums", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
},
destroyed() {
for (let i = 0; i < this.subscriptions.length; i++) {
Event.unsubscribe(this.subscriptions[i]);
}
},
};
</script>