Compare commits
123 Commits
aa68748817
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d745b10d24 | |||
| 2d4786a4fd | |||
| 2d0983f4d1 | |||
| dd344ecc3f | |||
| 93c31888b2 | |||
| 6779caccef | |||
| bf69b5acd1 | |||
| 0424f27264 | |||
| 5ae102071b | |||
| 8b716aeb97 | |||
| 1f5bbb6ce5 | |||
| a94c24581f | |||
| 5383f9bfc4 | |||
| beddc9c7ef | |||
| 0e81334c5f | |||
| dbd269c43b | |||
| 5c3d7d77fc | |||
| 0c48b8f7bf | |||
| 8c09fb4cda | |||
| 0a84598fbe | |||
| ee1e347405 | |||
| 6f8d3b1ad2 | |||
| a91350bcab | |||
| b00425e9c0 | |||
| c82a3477bd | |||
| 040dca3829 | |||
| 8674c1d361 | |||
| 6c9cb4201a | |||
| 7c32a5e824 | |||
| 6c2f9cb62e | |||
| 3cece877f7 | |||
| 8dd700f62e | |||
| 804cdcbf59 | |||
| 885fe7c9e9 | |||
| 930cc52373 | |||
| 4636a213b3 | |||
| 469954f613 | |||
| 5bdcd65d42 | |||
| cb91cd4cc7 | |||
| e04ddaed9b | |||
| 605da5e46c | |||
| 360e8ece85 | |||
| 1543bf9d03 | |||
| 287932d9d7 | |||
| 7c4de31c12 | |||
| 497c6150f7 | |||
| 514f51f111 | |||
| 244dc4fff6 | |||
| 4b39f787c9 | |||
| fb92b9c973 | |||
| 181358db7d | |||
| 3790567d44 | |||
| b3af993236 | |||
| 7ae2bb7635 | |||
| a75f0662ad | |||
| ffd68d5037 | |||
| adf9e2334c | |||
| 4125811017 | |||
| 4e8356b072 | |||
| bad116cdc0 | |||
| 9eac6871a4 | |||
| 3b694689f3 | |||
| 1604ddaa78 | |||
| 16181923b6 | |||
| 62dcaba875 | |||
| f67c05f034 | |||
| 8fd792b85c | |||
| 2595fcc365 | |||
| 0af5b8d84c | |||
| d1e8ff48c5 | |||
| d7964f6fc2 | |||
| 0394093407 | |||
| adcd6abfc7 | |||
| 721eaa5d07 | |||
| 994908ef6a | |||
| dda76ac7b0 | |||
| e7eac26e66 | |||
| 15b61ed56f | |||
| f9a51ffdce | |||
| a47b9a86dc | |||
| 89ca64dd88 | |||
| e5e1c5f82b | |||
| ab1e642c36 | |||
| 1cae3ac092 | |||
| afe5876162 | |||
| 72129f69ee | |||
| a2e2704a05 | |||
| 65af850d68 | |||
| f7425cdc03 | |||
| d9f432ef73 | |||
| 3a86651891 | |||
| 20f304c9cd | |||
| 1f58f989f7 | |||
| 96b1a99e04 | |||
| 1bc7dd3a43 | |||
| a048970be6 | |||
| e3171e5693 | |||
| d463b3df5c | |||
| 1ae7befa4b | |||
| dc80e1e199 | |||
| 35ffc2b0f7 | |||
| 383844dda8 | |||
| 77fb6595a2 | |||
| aa3adbe634 | |||
| 59a700c2f3 | |||
| 18f5a6f476 | |||
| 63a178ca57 | |||
| 0ae949a616 | |||
| edc9720623 | |||
| bbc3fbb0a5 | |||
| 1fc468efac | |||
| 143c415797 | |||
| e79a4d48e1 | |||
| 343325440e | |||
| 5c018892d3 | |||
| be61293cbe | |||
| e493194ce1 | |||
| b61631c33c | |||
| fa65da75bc | |||
| a4468f18dc | |||
| fea28b71ba | |||
| 884ad891f3 | |||
| 058161f798 |
3
.babelrc
@@ -5,7 +5,6 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@babel/plugin-transform-optional-chaining",
|
"@babel/plugin-transform-optional-chaining"
|
||||||
"@babel/plugin-syntax-import-attributes"
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
52
.eslintrc
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"extends": [
|
|
||||||
"airbnb-base",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:vue/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"ecmaVersion": 2022,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"default-param-last": 0,
|
|
||||||
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
|
|
||||||
"import/prefer-default-export": 0,
|
|
||||||
"no-underscore-dangle": 0,
|
|
||||||
"import/extensions": [2, "always"],
|
|
||||||
"indent": ["error", "tab"],
|
|
||||||
"max-len": 0,
|
|
||||||
"no-console": 0,
|
|
||||||
"no-param-reassign": ["error", {
|
|
||||||
"props": true,
|
|
||||||
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
|
||||||
}],
|
|
||||||
"vue/multi-word-component-names": 0,
|
|
||||||
"vue/no-reserved-component-names": 0,
|
|
||||||
"no-tabs": "off",
|
|
||||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
|
|
||||||
"prefer-destructuring": 0,
|
|
||||||
"template-curly-spacing": "off",
|
|
||||||
"vue/html-indent": ["error", "tab"],
|
|
||||||
"vue/multiline-html-element-content-newline": 0,
|
|
||||||
"vue/no-v-html": 0,
|
|
||||||
"vue/singleline-html-element-content-newline": 0,
|
|
||||||
"vue/comment-directive": 0,
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"alias": {
|
|
||||||
"extensions": [".js"],
|
|
||||||
"map": [
|
|
||||||
["#", "."],
|
|
||||||
["#root", "."]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"CONFIG": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
|
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
|
||||||
|
|
||||||
|
<!-- RTA restricted to adults label -->
|
||||||
|
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
|
||||||
|
|
||||||
<title>traxxx - Consent</title>
|
<title>traxxx - Consent</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -156,6 +159,12 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rta {
|
||||||
|
position: fixed;
|
||||||
|
bottom: .5rem;
|
||||||
|
right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media(max-width: 800px) {
|
@media(max-width: 800px) {
|
||||||
.heading {
|
.heading {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
@@ -219,5 +228,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/rta.gif"
|
||||||
|
alt="RTA Restricted To Adults"
|
||||||
|
title="RTA Restricted To Adults"
|
||||||
|
class="rta"
|
||||||
|
>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
.button {
|
.button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: stretch;
|
/* align-items: stretch; */
|
||||||
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
fill: var(--glass);
|
fill: var(--glass);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
@@ -55,6 +56,10 @@
|
|||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--glass-weak-30);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-label {
|
.button-label {
|
||||||
@@ -305,3 +310,23 @@
|
|||||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-cancel {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
fill: var(--glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
assets/img/icons/at-sign.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.657 2.343c-1.511-1.511-3.52-2.343-5.657-2.343s-4.146 0.832-5.657 2.343c-1.511 1.511-2.343 3.52-2.343 5.657s0.832 4.146 2.343 5.657c1.511 1.511 3.52 2.343 5.657 2.343 1.199 0 2.353-0.259 3.429-0.77 0.499-0.237 0.711-0.834 0.474-1.332s-0.834-0.711-1.332-0.474c-0.806 0.383-1.671 0.577-2.571 0.577-3.308 0-6-2.692-6-6s2.692-6 6-6 6 2.692 6 6v1c0 0.551-0.449 1-1 1s-1-0.449-1-1v-4c0-0.552-0.448-1-1-1-0.406 0-0.755 0.242-0.912 0.59-0.608-0.374-1.323-0.59-2.088-0.59-2.206 0-4 1.794-4 4s1.794 4 4 4c1.045 0 1.998-0.403 2.712-1.062 0.551 0.649 1.372 1.062 2.288 1.062 1.654 0 3-1.346 3-3v-1c0-2.137-0.832-4.146-2.343-5.657zM8 10c-1.103 0-2-0.897-2-2s0.897-2 2-2c1.103 0 2 0.897 2 2s-0.897 2-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 850 B |
4
assets/img/icons/cancel.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.5 0h-7l-4.5 4.5v7l4.5 4.5h7l4.5-4.5v-7l-4.5-4.5zM12.5 11l-1.5 1.5-3-3-3 3-1.5-1.5 3-3-3-3 1.5-1.5 3 3 3-3 1.5 1.5-3 3 3 3z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 283 B |
5
assets/img/icons/checkbox-checked.svg
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14z"></path>
|
||||||
|
<path d="M2.5 8l1.5-1.5 2.5 2.5 5.5-5.5 1.5 1.5-7 7z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 259 B |
4
assets/img/icons/checkbox-checked2.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 0h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2zM7 12.414l-3.707-3.707 1.414-1.414 2.293 2.293 4.793-4.793 1.414 1.414-6.207 6.207z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
4
assets/img/icons/checkbox-partial.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14zM4 4h8v8h-8z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
4
assets/img/icons/checkbox-partial2.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M14.5 0h-13c-0.825 0-1.5 0.675-1.5 1.5v13c0 0.825 0.675 1.5 1.5 1.5h13c0.825 0 1.5-0.675 1.5-1.5v-13c0-0.825-0.675-1.5-1.5-1.5zM14 14h-12v-12h12v12zM5 12h6c0.55 0 1-0.45 1-1v-6c0-0.55-0.45-1-1-1h-6c-0.55 0-1 0.45-1 1v6c0 0.55 0.45 1 1 1zM5 10.875h6v0.123c-0.001 0.001-0.001 0.001-0.002 0.002h-5.996c-0.001-0.001-0.001-0.001-0.002-0.002v-0.123zM5.002 5h5.996c0.001 0.001 0.001 0.001 0.002 0.002v0.123h-6v-0.123c0.001-0.001 0.001-0.001 0.002-0.002zM11 5.375v0.25h-6v-0.25h6zM11 5.875v0.25h-6v-0.25h6zM11 6.375v0.25h-6v-0.25h6zM11 6.875v0.25h-6v-0.25h6zM11 7.375v0.25h-6v-0.25h6zM11 7.875v0.25h-6v-0.25h6zM11 8.375v0.25h-6v-0.25h6zM11 8.875v0.25h-6v-0.25h6zM11 9.375v0.25h-6v-0.25h6zM11 9.875v0.25h-6v-0.25h6zM11 10.375v0.25h-6v-0.25h6z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 890 B |
4
assets/img/icons/checkbox-unchecked.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 197 B |
4
assets/img/icons/checkbox-unchecked2.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 0h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2zM14 14h-12v-12h12v12z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 263 B |
12
assets/img/icons/collaboration.svg
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 3.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||||
|
<path d="M2.5 5c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||||
|
<path d="M14 3.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||||
|
<path d="M12.5 5c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||||
|
<path d="M9 11.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||||
|
<path d="M7.5 13c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||||
|
<path d="M9.5 4h-4c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h4c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
|
||||||
|
<path d="M10.5 12c-0.095 0-0.192-0.027-0.277-0.084-0.23-0.153-0.292-0.464-0.139-0.693l2-3c0.153-0.23 0.464-0.292 0.693-0.139s0.292 0.464 0.139 0.693l-2 3c-0.096 0.145-0.255 0.223-0.416 0.223z"></path>
|
||||||
|
<path d="M4.5 12c-0.162 0-0.32-0.078-0.417-0.223l-2-3c-0.153-0.23-0.091-0.54 0.139-0.693s0.54-0.091 0.693 0.139l2 3c0.153 0.23 0.091 0.54-0.139 0.693-0.085 0.057-0.182 0.084-0.277 0.084z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
4
assets/img/icons/exclude.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM14 6.125h-3v-0.125h3v0.125zM6 14v-0.125h8v0.125h-8zM6 13.625v-0.25h8v0.25h-8zM6 13.125v-0.25h8v0.25h-8zM6 12.625v-0.25h8v0.25h-8zM6 12.125v-0.25h8v0.25h-8zM6 11.625v-0.25h8v0.25h-8zM6 11.125v-0.125h5v-0.125h3v0.25h-8zM11 10.625v-0.25h3v0.25h-3zM11 10.125v-0.25h3v0.25h-3zM11 9.625v-0.25h3v0.25h-3zM11 9.125v-0.25h3v0.25h-3zM11 8.625v-0.25h3v0.25h-3zM11 8.125v-0.25h3v0.25h-3zM11 7.625v-0.25h3v0.25h-3zM11 7.125v-0.25h3v0.25h-3zM11 6.625v-0.25h3v0.25h-3zM9 4h-5v0.125h-2v-0.25h7v0.125zM2 9v-0.125h2v0.125h-2zM2 8.625v-0.25h2v0.25h-2zM2 8.125v-0.25h2v0.25h-2zM2 7.625v-0.25h2v0.25h-2zM2 7.125v-0.25h2v0.25h-2zM2 6.625v-0.25h2v0.25h-2zM2 6.125v-0.25h2v0.25h-2zM2 5.625v-0.25h2v0.25h-2zM2 5.125v-0.25h2v0.25h-2zM2 4.625v-0.25h2v0.25h-2zM9 3.625h-7v-0.25h7v0.25zM9 3.125h-7v-0.25h7v0.25zM9 2.625h-7v-0.25h7v0.25zM9 2.125h-7v-0.125h7v0.125zM5 5h5v5h-5v-5z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
4
assets/img/icons/interset.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM10 6.125h-4v-0.125h4v0.125zM10 10h-4v-0.125h4v0.125zM10 9.625h-4v-0.25h4v0.25zM10 9.125h-4v-0.25h4v0.25zM10 8.625h-4v-0.25h4v0.25zM10 8.125h-4v-0.25h4v0.25zM10 7.625h-4v-0.25h4v0.25zM10 7.125h-4v-0.25h4v0.25zM10 6.625h-4v-0.25h4v0.25zM4 10h-3v-9h9v3h-6v6zM15 15h-10v-3h7v-7h3v10z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
5
assets/img/icons/merge.svg
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M9 5h3.5l-4.5-4.5-4.5 4.5h3.5v3.586l-5.957 5.957 1.414 1.414 6.543-6.543z"></path>
|
||||||
|
<path d="M8.543 11.957l1.414-1.414 4 4-1.414 1.414-4-4z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 295 B |
4
assets/img/icons/popout.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M14.5 2h-9c-0.825 0-1.5 0.675-1.5 1.5v1.5h-2.5c-0.825 0-1.5 0.675-1.5 1.5v6c0 0.825 0.675 1.5 1.5 1.5h9c0.825 0 1.5-0.675 1.5-1.5v-1.5h2.5c0.825 0 1.5-0.675 1.5-1.5v-6c0-0.825-0.675-1.5-1.5-1.5zM11 12.5c0 0.271-0.229 0.5-0.5 0.5h-9c-0.271 0-0.5-0.229-0.5-0.5v-6c0-0.271 0.229-0.5 0.5-0.5h2.5v3.5c0 0.825 0.675 1.5 1.5 1.5h5.5v1.5zM15 9.5c0 0.271-0.229 0.5-0.5 0.5h-9c-0.271 0-0.5-0.229-0.5-0.5v-6c0-0.271 0.229-0.5 0.5-0.5h9c0.271 0 0.5 0.229 0.5 0.5v6z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
4
assets/img/icons/stack-plus.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M5 1v1.155l-2.619 0.368 0.17 1.211-2.551 0.732 3.308 11.535 10.189-2.921 0.558-0.079h1.945v-12h-11zM3.929 14.879l-2.808-9.793 1.558-0.447 1.373 9.766 2.997-0.421-3.119 0.894zM4.822 13.382l-1.418-10.088 1.595-0.224v9.93h2.543l-2.721 0.382zM15 12h-9v-10h9v10zM13 8h-2v2h-1v-2h-2v-1h2v-2h1v2h2z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 448 B |
4
assets/img/icons/stack.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.75 4h-0.75v-0.75c0-0.689-0.561-1.25-1.25-1.25h-0.75v-0.75c0-0.689-0.561-1.25-1.25-1.25h-7.5c-0.689 0-1.25 0.561-1.25 1.25v9.5c0 0.689 0.561 1.25 1.25 1.25h0.75v0.75c0 0.689 0.561 1.25 1.25 1.25h0.75v0.75c0 0.689 0.561 1.25 1.25 1.25h7.5c0.689 0 1.25-0.561 1.25-1.25v-9.5c0-0.689-0.561-1.25-1.25-1.25zM2.25 11c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.137 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.113 0.25 0.25v0.75h-5.75c-0.689 0-1.25 0.561-1.25 1.25v7.75h-0.75zM4.25 13c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.138 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.112 0.25 0.25v0.75h-5.75c-0.689 0-1.25 0.561-1.25 1.25v7.75h-0.75zM14 14.75c0 0.137-0.113 0.25-0.25 0.25h-7.5c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.138 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.112 0.25 0.25v9.5z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 922 B |
4
assets/img/icons/stack2.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 5l-8-4-8 4 8 4 8-4zM8 2.328l5.345 2.672-5.345 2.672-5.345-2.672 5.345-2.672zM14.398 7.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199zM14.398 10.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 354 B |
4
assets/img/icons/stack3.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 9l-8-4 8-4 8 4zM14.398 7.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199zM14.398 10.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 292 B |
4
assets/img/icons/unite.svg
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM10 5.125h-5v-0.125h5v0.125zM14 6.125h-3v-0.125h3v0.125zM5 9.875h5v0.125h-5v-0.125zM2 8.875h2v0.125h-2v-0.125zM4 5.875v0.25h-2v-0.25h2zM2 5.625v-0.25h2v0.25h-2zM4 6.375v0.25h-2v-0.25h2zM5 8.125v-0.25h5v0.25h-5zM10 8.375v0.25h-5v-0.25h5zM5 7.625v-0.25h5v0.25h-5zM5 7.125v-0.25h5v0.25h-5zM5 6.625v-0.25h5v0.25h-5zM4 6.875v0.25h-2v-0.25h2zM4 7.375v0.25h-2v-0.25h2zM4 7.875v0.25h-2v-0.25h2zM4 8.375v0.25h-2v-0.25h2zM5 8.875h5v0.25h-5v-0.25zM10 9.375v0.25h-5v-0.25h5zM6 14v-0.125h8v0.125h-8zM6 13.625v-0.25h8v0.25h-8zM6 13.125v-0.25h8v0.25h-8zM6 12.625v-0.25h8v0.25h-8zM6 12.125v-0.25h8v0.25h-8zM6 11.625v-0.25h8v0.25h-8zM6 11.125v-0.125h5v-0.125h3v0.25h-8zM11 10.625v-0.25h3v0.25h-3zM11 10.125v-0.25h3v0.25h-3zM11 9.625v-0.25h3v0.25h-3zM11 9.125v-0.25h3v0.25h-3zM11 8.625v-0.25h3v0.25h-3zM11 8.125v-0.25h3v0.25h-3zM11 7.625v-0.25h3v0.25h-3zM11 7.125v-0.25h3v0.25h-3zM11 6.625v-0.25h3v0.25h-3zM10 6.125h-5v-0.25h5v0.25zM5 5.625v-0.25h5v0.25h-5zM4 5.125h-2v-0.25h2v0.25zM9 2v0.125h-7v-0.125h7zM9 2.375v0.25h-7v-0.25h7zM9 2.875v0.25h-7v-0.25h7zM9 3.375v0.25h-7v-0.25h7zM9 3.875v0.125h-5v0.125h-2v-0.25h7zM4 4.375v0.25h-2v-0.25h2z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
11
assets/img/icons/unlink5.svg
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Generated by IcoMoon.io -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16">
|
||||||
|
<path d="M7 12h-3c-2.206 0-4-1.794-4-4s1.794-4 4-4h3c0.552 0 1 0.448 1 1s-0.448 1-1 1h-3c-1.103 0-2 0.897-2 2s0.897 2 2 2h3c0.552 0 1 0.448 1 1s-0.448 1-1 1z"></path>
|
||||||
|
<path d="M13 12h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h3c1.103 0 2-0.897 2-2s-0.897-2-2-2h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h3c2.206 0 4 1.794 4 4s-1.794 4-4 4z"></path>
|
||||||
|
<path d="M8.5 3c-0.276 0-0.5-0.224-0.5-0.5v-2c0-0.276 0.224-0.5 0.5-0.5s0.5 0.224 0.5 0.5v2c0 0.276-0.224 0.5-0.5 0.5z"></path>
|
||||||
|
<path d="M11.5 3c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l2-2c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-2 2c-0.098 0.098-0.226 0.146-0.354 0.146z"></path>
|
||||||
|
<path d="M5.5 3c-0.128 0-0.256-0.049-0.354-0.146l-2-2c-0.195-0.195-0.195-0.512 0-0.707s0.512-0.195 0.707 0l2 2c0.195 0.195 0.195 0.512 0 0.707-0.098 0.098-0.226 0.146-0.354 0.146z"></path>
|
||||||
|
<path d="M8.5 13c0.276 0 0.5 0.224 0.5 0.5v2c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-2c0-0.276 0.224-0.5 0.5-0.5z"></path>
|
||||||
|
<path d="M5.5 13c0.128 0 0.256 0.049 0.354 0.146 0.195 0.195 0.195 0.512 0 0.707l-2 2c-0.195 0.195-0.512 0.195-0.707 0s-0.195-0.512 0-0.707l2-2c0.098-0.098 0.226-0.146 0.354-0.146z"></path>
|
||||||
|
<path d="M11.5 13c0.128 0 0.256 0.049 0.354 0.146l2 2c0.195 0.195 0.195 0.512 0 0.707s-0.512 0.195-0.707 0l-2-2c-0.195-0.195-0.195-0.512 0-0.707 0.098-0.098 0.226-0.146 0.354-0.146z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
2
common
@@ -1,3 +1,132 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format, subYears } from 'date-fns';
|
||||||
|
import { parse } from 'path-to-regexp';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import ActorTile from '#/components/actors/tile.vue';
|
||||||
|
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||||
|
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||||
|
|
||||||
|
import CountryFilter from '#/components/filters/country.vue';
|
||||||
|
import Filters from '#/components/filters/filters.vue';
|
||||||
|
import GenderFilter from '#/components/filters/gender.vue';
|
||||||
|
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
import Pagination from '#/components/pagination/pagination.vue';
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||||
|
|
||||||
|
const q = ref(urlParsed.search.q);
|
||||||
|
const actors = ref([]);
|
||||||
|
const pageStash = pageProps.stash;
|
||||||
|
const countries = ref(pageProps.countries);
|
||||||
|
const cupRange = ref(pageProps.cupRange);
|
||||||
|
|
||||||
|
actors.value = pageProps.actors;
|
||||||
|
|
||||||
|
const currentPage = ref(Number(routeParams.page));
|
||||||
|
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
||||||
|
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
gender: urlParsed.search.gender,
|
||||||
|
ageRequired: !!urlParsed.search.age,
|
||||||
|
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||||
|
dobRequired: !!urlParsed.search.dob,
|
||||||
|
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||||
|
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||||
|
country: urlParsed.search.c,
|
||||||
|
braSizeRequired: !!urlParsed.search.cup,
|
||||||
|
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||||
|
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||||
|
heightRequired: !!urlParsed.search.height,
|
||||||
|
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||||
|
weightRequired: !!urlParsed.search.weight,
|
||||||
|
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||||
|
avatarRequired: !!urlParsed.search.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
function getPath(preserveQuery) {
|
||||||
|
const path = parse(routeParams.path).map((segment) => {
|
||||||
|
if (segment.name === 'page') {
|
||||||
|
return `${segment.prefix}${currentPage.value || 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (preserveQuery && urlParsed.searchOriginal) {
|
||||||
|
return `${path}${urlParsed.searchOriginal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(options = {}) {
|
||||||
|
if (options.resetPage !== false) {
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.autoScope !== false) {
|
||||||
|
if (q.value) {
|
||||||
|
order.value = 'results.desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!q.value && order.value.includes('results')) {
|
||||||
|
order.value = 'likes.desc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
q: q.value || undefined,
|
||||||
|
order: order.value,
|
||||||
|
gender: filters.value.gender || undefined,
|
||||||
|
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||||
|
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||||
|
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||||
|
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||||
|
c: filters.value.country || undefined,
|
||||||
|
nb: filters.value.naturalBoobs,
|
||||||
|
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||||
|
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||||
|
avatar: filters.value.avatarRequired || undefined,
|
||||||
|
stashId: pageStash?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
navigate(getPath(false), query, { redirect: false });
|
||||||
|
|
||||||
|
const res = await get('/actors', {
|
||||||
|
...query,
|
||||||
|
page: currentPage.value, // client uses param rather than query pagination
|
||||||
|
});
|
||||||
|
|
||||||
|
actors.value = res.actors;
|
||||||
|
total.value = res.total;
|
||||||
|
|
||||||
|
countries.value = res.countries;
|
||||||
|
|
||||||
|
events.emit('scrollUp');
|
||||||
|
}
|
||||||
|
|
||||||
|
function paginate({ page }) {
|
||||||
|
currentPage.value = page;
|
||||||
|
|
||||||
|
search({ resetPage: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilter(prop, value, reload = true) {
|
||||||
|
filters.value[prop] = value;
|
||||||
|
|
||||||
|
if (reload) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<Filters :results="total">
|
<Filters :results="total">
|
||||||
@@ -5,12 +134,19 @@
|
|||||||
<!-- onsearch not compatible with FF and Safari -->
|
<!-- onsearch not compatible with FF and Safari -->
|
||||||
<input
|
<input
|
||||||
v-model="q"
|
v-model="q"
|
||||||
type="search"
|
type="input"
|
||||||
placeholder="Search actors"
|
placeholder="Search actors"
|
||||||
class="input search"
|
class="input search"
|
||||||
@search="search"
|
@keydown.enter="search"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="q"
|
||||||
|
icon="cross2"
|
||||||
|
class="search-button"
|
||||||
|
@click="q = ''; search()"
|
||||||
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
icon="search"
|
icon="search"
|
||||||
class="search-button"
|
class="search-button"
|
||||||
@@ -104,135 +240,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
import { format, subYears } from 'date-fns';
|
|
||||||
import { parse } from 'path-to-regexp';
|
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
|
|
||||||
import ActorTile from '#/components/actors/tile.vue';
|
|
||||||
import Pagination from '#/components/pagination/pagination.vue';
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
import Filters from '#/components/filters/filters.vue';
|
|
||||||
import GenderFilter from '#/components/filters/gender.vue';
|
|
||||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
|
||||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
|
||||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
|
||||||
import CountryFilter from '#/components/filters/country.vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
|
||||||
|
|
||||||
const q = ref(urlParsed.search.q);
|
|
||||||
const actors = ref([]);
|
|
||||||
const pageStash = pageProps.stash;
|
|
||||||
const countries = ref(pageProps.countries);
|
|
||||||
const cupRange = ref(pageProps.cupRange);
|
|
||||||
|
|
||||||
actors.value = pageProps.actors;
|
|
||||||
|
|
||||||
const currentPage = ref(Number(routeParams.page));
|
|
||||||
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
|
||||||
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
|
||||||
|
|
||||||
const filters = ref({
|
|
||||||
gender: urlParsed.search.gender,
|
|
||||||
ageRequired: !!urlParsed.search.age,
|
|
||||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
|
||||||
dobRequired: !!urlParsed.search.dob,
|
|
||||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
|
||||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
|
||||||
country: urlParsed.search.c,
|
|
||||||
braSizeRequired: !!urlParsed.search.cup,
|
|
||||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
|
||||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
|
||||||
heightRequired: !!urlParsed.search.height,
|
|
||||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
|
||||||
weightRequired: !!urlParsed.search.weight,
|
|
||||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
|
||||||
avatarRequired: !!urlParsed.search.avatar,
|
|
||||||
});
|
|
||||||
|
|
||||||
function getPath(preserveQuery) {
|
|
||||||
const path = parse(routeParams.path).map((segment) => {
|
|
||||||
if (segment.name === 'page') {
|
|
||||||
return `${segment.prefix}${1}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
if (preserveQuery && urlParsed.searchOriginal) {
|
|
||||||
return `${path}${urlParsed.searchOriginal}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function search(options = {}) {
|
|
||||||
if (options.resetPage !== false) {
|
|
||||||
currentPage.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.autoScope !== false) {
|
|
||||||
if (q.value) {
|
|
||||||
order.value = 'results.desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!q.value && order.value.includes('results')) {
|
|
||||||
order.value = 'likes.desc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
q: q.value || undefined,
|
|
||||||
order: order.value,
|
|
||||||
gender: filters.value.gender || undefined,
|
|
||||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
|
||||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
|
||||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
|
||||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
|
||||||
c: filters.value.country || undefined,
|
|
||||||
nb: filters.value.naturalBoobs,
|
|
||||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
|
||||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
|
||||||
avatar: filters.value.avatarRequired || undefined,
|
|
||||||
stashId: pageStash?.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
navigate(getPath(false), query, { redirect: false });
|
|
||||||
|
|
||||||
const res = await get('/actors', {
|
|
||||||
...query,
|
|
||||||
page: currentPage.value, // client uses param rather than query pagination
|
|
||||||
});
|
|
||||||
|
|
||||||
actors.value = res.actors;
|
|
||||||
total.value = res.total;
|
|
||||||
|
|
||||||
countries.value = res.countries;
|
|
||||||
|
|
||||||
events.emit('scrollUp');
|
|
||||||
}
|
|
||||||
|
|
||||||
function paginate({ page }) {
|
|
||||||
currentPage.value = page;
|
|
||||||
|
|
||||||
search({ resetPage: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilter(prop, value, reload = true) {
|
|
||||||
filters.value[prop] = value;
|
|
||||||
|
|
||||||
if (reload) {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.gender-button {
|
.gender-button {
|
||||||
&.selected .gender .icon {
|
&.selected .gender .icon {
|
||||||
|
|||||||
176
components/actors/add.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||||
|
|
||||||
|
import { post } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const nameInput = ref(null);
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
const isSubmitted = ref(false);
|
||||||
|
|
||||||
|
const name = ref(null);
|
||||||
|
const gender = ref('female');
|
||||||
|
const allowGlobalMatch = ref(true);
|
||||||
|
|
||||||
|
const newActor = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nameInput.value.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addActor() {
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { actor } = await post('/actors', {
|
||||||
|
actor: {
|
||||||
|
name: name.value,
|
||||||
|
gender: gender.value,
|
||||||
|
allowGlobalMatch: allowGlobalMatch.value,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
successFeedback: 'Actor has been added.',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
name.value = null;
|
||||||
|
gender.value = null;
|
||||||
|
allowGlobalMatch.value = true;
|
||||||
|
|
||||||
|
newActor.value = actor;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = false;
|
||||||
|
isSubmitted.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
title="Add actor"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isSubmitted && newActor"
|
||||||
|
class="dialog-body"
|
||||||
|
>
|
||||||
|
<strong>Added '{{ newActor.name }}' (#{{ newActor.id }})</strong>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
v-if="isSubmitted && newActor"
|
||||||
|
class="dialog-body options"
|
||||||
|
>
|
||||||
|
<li class="option">
|
||||||
|
<a
|
||||||
|
:href="`/actor/edit/${newActor.id}/${newActor.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Edit bio</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="option">
|
||||||
|
<a
|
||||||
|
:href="`/actor/${newActor.id}/${newActor.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Go to actor</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="option">
|
||||||
|
<span
|
||||||
|
class="link"
|
||||||
|
@click="newActor = null; isSubmitted = false;"
|
||||||
|
>Add another actor</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
v-else
|
||||||
|
class="dialog-body"
|
||||||
|
@submit.prevent="addActor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="nameInput"
|
||||||
|
v-model="name"
|
||||||
|
class="input"
|
||||||
|
placeholder="Name"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="gender"
|
||||||
|
placeholder="Gender"
|
||||||
|
class="input"
|
||||||
|
>
|
||||||
|
<option value="female">Female</option>
|
||||||
|
<option value="male">Male</option>
|
||||||
|
<option value="transsexual">Transsexual</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Allow global match"
|
||||||
|
:checked="allowGlobalMatch"
|
||||||
|
@change="(isChecked) => allowGlobalMatch = isChecked"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<Ellipsis v-if="isSubmitting" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="button button-primary"
|
||||||
|
>Add actor</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-body {
|
||||||
|
width: 20rem;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-container {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,99 @@
|
|||||||
|
<script setup>
|
||||||
|
import formatTemplate from 'template-format';
|
||||||
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
import { formatDate } from '#/utils/format.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expanded = ref(false);
|
||||||
|
const enhancedTooltipId = useId();
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { user, env } = pageContext;
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
twitter: 'twitter-x',
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the profile is empty, the expand button overlaps the header
|
||||||
|
const showExpand = [
|
||||||
|
'age',
|
||||||
|
'bust',
|
||||||
|
'cup',
|
||||||
|
'eyes',
|
||||||
|
'ethnicity',
|
||||||
|
'hairColor',
|
||||||
|
'hasPiercings',
|
||||||
|
'hasTattoos',
|
||||||
|
'height',
|
||||||
|
'hip',
|
||||||
|
'naturalBoobs',
|
||||||
|
'origin',
|
||||||
|
'residence',
|
||||||
|
'waist',
|
||||||
|
'weight',
|
||||||
|
].some((attribute) => !!props.actor[attribute]);
|
||||||
|
|
||||||
|
const augmentationMap = {
|
||||||
|
bbl: 'BBL',
|
||||||
|
fat: 'fat transfer',
|
||||||
|
lift: 'direct lift',
|
||||||
|
lipo: 'lipo without BBL',
|
||||||
|
filler: 'filler',
|
||||||
|
mms: 'MMS',
|
||||||
|
over: 'over-muscle',
|
||||||
|
under: 'under-muscle',
|
||||||
|
mammary: 'under breast',
|
||||||
|
areolar: 'areolar',
|
||||||
|
crescent: 'crescent areolar',
|
||||||
|
lollipop: 'lollipop',
|
||||||
|
axillary: 'armpit',
|
||||||
|
umbilical: 'navel',
|
||||||
|
};
|
||||||
|
|
||||||
|
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||||
|
.filter((profile) => !!profile.description)
|
||||||
|
.map((profile) => [profile.descriptionHash, {
|
||||||
|
text: profile.description,
|
||||||
|
entity: profile.entity,
|
||||||
|
}])));
|
||||||
|
|
||||||
|
function getSocialUrl(social) {
|
||||||
|
if (social.url) {
|
||||||
|
return social.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageContext.env.socials.urls[social.platform]) {
|
||||||
|
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socials = props.actor.socials.slice(0, 10).map((social) => ({
|
||||||
|
...social,
|
||||||
|
handle: social.url
|
||||||
|
? new URL(social.url).hostname
|
||||||
|
: social.handle,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const aliases = Object
|
||||||
|
.entries(props.actor.aliases.reduce((acc, alias) => {
|
||||||
|
acc[alias.name] = (props.actor[alias.name] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {}))
|
||||||
|
.toSorted(([, countA], [, countB]) => countB - countA)
|
||||||
|
.map(([alias]) => alias)
|
||||||
|
.filter((alias) => alias !== props.actor.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="profile"
|
class="profile"
|
||||||
@@ -20,6 +116,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="bio nolist">
|
<ul class="bio nolist">
|
||||||
|
<li
|
||||||
|
v-if="aliases.length > 0"
|
||||||
|
class="bio-item"
|
||||||
|
>
|
||||||
|
<dfn class="bio-label"><Icon icon="at-sign" />Aliases</dfn>
|
||||||
|
|
||||||
|
<span class="orientation">{{ aliases.join(', ') }}</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-if="actor.dateOfBirth"
|
v-if="actor.dateOfBirth"
|
||||||
class="bio-item"
|
class="bio-item"
|
||||||
@@ -44,7 +149,7 @@
|
|||||||
<dfn class="bio-label"><Icon icon="cake" />Age</dfn>
|
<dfn class="bio-label"><Icon icon="cake" />Age</dfn>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
:title="'Exact date of birth or age unknown'"
|
title="Exact date of birth or age unknown"
|
||||||
class="birthdate"
|
class="birthdate"
|
||||||
>> {{ actor.age }}</span>
|
>> {{ actor.age }}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -80,10 +185,17 @@
|
|||||||
<span
|
<span
|
||||||
v-if="actor.origin.city"
|
v-if="actor.origin.city"
|
||||||
class="city"
|
class="city"
|
||||||
>{{ actor.origin.city }}</span><span
|
>{{ actor.origin.city }}</span>
|
||||||
|
|
||||||
|
<span
|
||||||
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
|
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
|
||||||
class="state"
|
class="state"
|
||||||
>{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span>
|
>
|
||||||
|
{{ actor.origin.city
|
||||||
|
? [',', actor.origin.state].join(' ')
|
||||||
|
: actor.origin.state
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="actor.origin.country"
|
v-if="actor.origin.country"
|
||||||
@@ -105,7 +217,7 @@
|
|||||||
class="bio-item residence"
|
class="bio-item residence"
|
||||||
:class="{ hideable: !!actor.origin }"
|
:class="{ hideable: !!actor.origin }"
|
||||||
>
|
>
|
||||||
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
|
<dfn class="bio-label"><Icon icon="location" />{{ actor.dateOfDeath ? 'Lived' : 'Lives' }} in</dfn>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span
|
<span
|
||||||
@@ -146,7 +258,7 @@
|
|||||||
{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
||||||
<Icon
|
<Icon
|
||||||
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
||||||
v-tooltip="[actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', ')"
|
v-tooltip="{ content: [actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', '), ariaId: enhancedTooltipId }"
|
||||||
icon="magic-wand2"
|
icon="magic-wand2"
|
||||||
class="enhanced"
|
class="enhanced"
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +280,7 @@
|
|||||||
actor.boobsVolume && `${actor.boobsVolume}cc`,
|
actor.boobsVolume && `${actor.boobsVolume}cc`,
|
||||||
augmentationMap[actor.boobsPlacement] || actor.boobsPlacement,
|
augmentationMap[actor.boobsPlacement] || actor.boobsPlacement,
|
||||||
augmentationMap[actor.boobsIncision] || actor.boobsIncision,
|
augmentationMap[actor.boobsIncision] || actor.boobsIncision,
|
||||||
augmentationMap[actor.boobsImplant] || actor.boobsImplant
|
augmentationMap[actor.boobsImplant] || actor.boobsImplant,
|
||||||
].filter(Boolean).join(' ')"
|
].filter(Boolean).join(' ')"
|
||||||
class="augmentations-section"
|
class="augmentations-section"
|
||||||
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
||||||
@@ -208,7 +320,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="actor.isCircumcised"
|
v-if="actor.isCircumcised"
|
||||||
:title="'Circumcised'"
|
title="Circumcised"
|
||||||
icon="pagebreak"
|
icon="pagebreak"
|
||||||
class="circumcised"
|
class="circumcised"
|
||||||
/>
|
/>
|
||||||
@@ -312,10 +424,10 @@
|
|||||||
<a
|
<a
|
||||||
v-for="social in socials"
|
v-for="social in socials"
|
||||||
:key="`social-${social.id}`"
|
:key="`social-${social.id}`"
|
||||||
|
v-tooltip="{ content: social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url, ariaId: `social-${social.id}` }"
|
||||||
:href="getSocialUrl(social)"
|
:href="getSocialUrl(social)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
:title="social.platform || social.url"
|
|
||||||
class="social ellipsis"
|
class="social ellipsis"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -339,7 +451,9 @@
|
|||||||
:class="`icon-social icon-${social.platform} icon-generic`"
|
:class="`icon-social icon-${social.platform} icon-generic`"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!--
|
||||||
<template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }}
|
<template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }}
|
||||||
|
-->
|
||||||
</a>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -356,7 +470,7 @@
|
|||||||
:href="`/actor/edit/${actor.id}/${actor.slug}`"
|
:href="`/actor/edit/${actor.id}/${actor.slug}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="link"
|
class="link"
|
||||||
>Edit bio</a>
|
>Edit</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="`/actor/revs/${actor.id}/${actor.slug}`"
|
:href="`/actor/revs/${actor.id}/${actor.slug}`"
|
||||||
@@ -420,92 +534,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
import formatTemplate from 'template-format';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
import { formatDate } from '#/utils/format.js';
|
|
||||||
|
|
||||||
const expanded = ref(false);
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { user, env } = pageContext;
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
actor: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
twitter: 'twitter-x',
|
|
||||||
};
|
|
||||||
|
|
||||||
// if the profile is empty, the expand button overlaps the header
|
|
||||||
const showExpand = [
|
|
||||||
'age',
|
|
||||||
'bust',
|
|
||||||
'cup',
|
|
||||||
'eyes',
|
|
||||||
'ethnicity',
|
|
||||||
'hairColor',
|
|
||||||
'hasPiercings',
|
|
||||||
'hasTattoos',
|
|
||||||
'height',
|
|
||||||
'hip',
|
|
||||||
'naturalBoobs',
|
|
||||||
'origin',
|
|
||||||
'residence',
|
|
||||||
'waist',
|
|
||||||
'weight',
|
|
||||||
].some((attribute) => !!props.actor[attribute]);
|
|
||||||
|
|
||||||
const augmentationMap = {
|
|
||||||
bbl: 'BBL',
|
|
||||||
fat: 'fat transfer',
|
|
||||||
lift: 'direct lift',
|
|
||||||
lipo: 'lipo without BBL',
|
|
||||||
filler: 'filler',
|
|
||||||
mms: 'MMS',
|
|
||||||
over: 'over-muscle',
|
|
||||||
under: 'under-muscle',
|
|
||||||
mammary: 'under breast',
|
|
||||||
areolar: 'areolar',
|
|
||||||
crescent: 'crescent areolar',
|
|
||||||
lollipop: 'lollipop',
|
|
||||||
axillary: 'armpit',
|
|
||||||
umbilical: 'navel',
|
|
||||||
};
|
|
||||||
|
|
||||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
|
||||||
.filter((profile) => !!profile.description)
|
|
||||||
.map((profile) => [profile.descriptionHash, {
|
|
||||||
text: profile.description,
|
|
||||||
entity: profile.entity,
|
|
||||||
}])));
|
|
||||||
|
|
||||||
function getSocialUrl(social) {
|
|
||||||
if (social.url) {
|
|
||||||
return social.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageContext.env.socials.urls[social.platform]) {
|
|
||||||
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const socials = props.actor.socials.map((social) => ({
|
|
||||||
...social,
|
|
||||||
handle: social.url
|
|
||||||
? new URL(social.url).hostname
|
|
||||||
: social.handle,
|
|
||||||
}));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.header-gender {
|
.header-gender {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -735,19 +763,23 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.socials {
|
.socials {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/*
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
grid-gap: 0 0;
|
grid-gap: 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
*/
|
||||||
gap: .25rem;
|
gap: .25rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social {
|
.social {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
height: 2rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: .1rem .5rem;
|
justify-content: center;
|
||||||
|
padding: .75rem .75rem;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -755,10 +787,6 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
background: var(--highlight-weak-40);
|
background: var(--highlight-weak-40);
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-generic {
|
.icon-generic {
|
||||||
fill: var(--highlight);
|
fill: var(--highlight);
|
||||||
}
|
}
|
||||||
@@ -783,6 +811,7 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
.link {
|
.link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +883,7 @@ const socials = props.actor.socials.map((social) => ({
|
|||||||
display: none;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 1;
|
||||||
bottom: -.25rem;
|
bottom: -.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
gender: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-if="gender"
|
v-if="gender"
|
||||||
@@ -8,15 +17,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
gender: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.gender {
|
.gender {
|
||||||
&.female .icon {
|
&.female .icon {
|
||||||
|
|||||||
288
components/actors/merge.vue
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
|
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||||
|
|
||||||
|
import { get, post } from '#/src/api.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'merged']);
|
||||||
|
|
||||||
|
const targetActor = ref(null);
|
||||||
|
const actorInput = ref(null);
|
||||||
|
const actorQuery = ref('');
|
||||||
|
const actorResults = ref([]);
|
||||||
|
const submitted = ref(false);
|
||||||
|
const merged = ref(false);
|
||||||
|
const searchDropdownId = useId();
|
||||||
|
const conflictTooltipId = useId();
|
||||||
|
|
||||||
|
const sourceTargetConflict = computed(() => props.actors.some((actor) => actor.id === targetActor.value?.id));
|
||||||
|
|
||||||
|
async function searchActors() {
|
||||||
|
const res = await get('/actors', {
|
||||||
|
q: actorQuery.value, // return partial matches
|
||||||
|
limit: 10,
|
||||||
|
global: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
actorResults.value = res.actors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActorNames() {
|
||||||
|
if (props.actors.length > 1) {
|
||||||
|
return `${props.actors.length} actors`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.actors[0].entity) {
|
||||||
|
return `${props.actors[0].name} (${props.actors[0].entity.name})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.actors[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function merge() {
|
||||||
|
submitted.value = true;
|
||||||
|
|
||||||
|
await post(`/actors/${targetActor.value.id}/merge/${props.actors.map((actor) => actor.id).join(',')}`, null, {
|
||||||
|
successFeedback: `Merged ${getActorNames()} into ${targetActor.value.name}`,
|
||||||
|
errorFeedback: `Failed to merge ${getActorNames()} into ${targetActor.value.name}`,
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
submitted.value = false;
|
||||||
|
merged.value = true;
|
||||||
|
|
||||||
|
emit('merged');
|
||||||
|
|
||||||
|
// emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectActor(actor) {
|
||||||
|
targetActor.value = actor;
|
||||||
|
actorQuery.value = '';
|
||||||
|
actorResults.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
actorInput.value.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:title="`Merge ${actors.length === 1 ? `'${actors[0].name}'` : `${actors.length} actors`}`"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="merged"
|
||||||
|
class="dialog-body"
|
||||||
|
>
|
||||||
|
Merge successful
|
||||||
|
|
||||||
|
<ul class="options">
|
||||||
|
<li class="option">
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
class="link"
|
||||||
|
>Reload page</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="option">
|
||||||
|
<a
|
||||||
|
:href="`/actor/${targetActor?.id}/${targetActor?.slug}`"
|
||||||
|
class="link"
|
||||||
|
>Go to #{{ targetActor?.id }} {{ targetActor?.name }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="dialog-body"
|
||||||
|
>
|
||||||
|
<ul class="actors nolist">
|
||||||
|
<li
|
||||||
|
v-for="actor in actors"
|
||||||
|
:key="`actor-${actor.id}`"
|
||||||
|
class="actor"
|
||||||
|
>
|
||||||
|
<span class="source">
|
||||||
|
<strong class="source-name">{{ actor.name }}<template v-if="actor.entity"> ({{ actor.entity.name }})</template></strong>
|
||||||
|
<span class="source-id">#{{ actor.id }}</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<span class="path">merging into</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="targetActor"
|
||||||
|
class="target"
|
||||||
|
>
|
||||||
|
<strong class="target-name">
|
||||||
|
<span class="target-id">#{{ targetActor.id }}</span>
|
||||||
|
{{ targetActor.name }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
@click="targetActor = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<VDropdown
|
||||||
|
:aria-id="searchDropdownId"
|
||||||
|
:triggers="[]"
|
||||||
|
:shown="actorResults.length > 0"
|
||||||
|
:auto-hide="false"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="actorInput"
|
||||||
|
v-model="actorQuery"
|
||||||
|
class="input"
|
||||||
|
placeholder="Search target actor"
|
||||||
|
@input="searchActors"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<ul
|
||||||
|
class="results nolist"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="actorResult in actorResults"
|
||||||
|
:key="`actor-result-${actorResult.id}`"
|
||||||
|
v-close-popper
|
||||||
|
class="result-item"
|
||||||
|
@click="selectActor(actorResult)"
|
||||||
|
>
|
||||||
|
<div class="result-label">
|
||||||
|
<span class="result-id">#{{ actorResult.id }}</span> {{ actorResult.name }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<Ellipsis v-if="submitted" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
v-tooltip="sourceTargetConflict && { content: 'Cannot merge actor profile into itself', ariaId: conflictTooltipId }"
|
||||||
|
type="submit"
|
||||||
|
class="button button-primary"
|
||||||
|
:disabled="!targetActor || sourceTargetConflict"
|
||||||
|
@click="merge"
|
||||||
|
>Merge</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-body {
|
||||||
|
width: 20rem;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actors {
|
||||||
|
max-height: 15rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
color: var(--glass-strong-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: .25rem .75rem;
|
||||||
|
fill: var(--glass);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--error);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-id,
|
||||||
|
.target-id {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
padding: .25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-id {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-container {
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['actor']);
|
||||||
|
const dropdownId = useId();
|
||||||
|
const actors = ref([]);
|
||||||
|
const query = ref(null);
|
||||||
|
const queryInput = ref(null);
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
const data = await get('/actors', { q: query.value });
|
||||||
|
|
||||||
|
actors.value = data.actors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryInput.value?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDropdown
|
<VDropdown
|
||||||
|
:aria-id="dropdownId"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
@show="focus"
|
@show="focus"
|
||||||
@@ -36,38 +68,6 @@
|
|||||||
</VDropdown>
|
</VDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
const actors = ref([]);
|
|
||||||
const query = ref(null);
|
|
||||||
const queryInput = ref(null);
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['actor']);
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
const data = await get('/actors', { q: query.value });
|
|
||||||
|
|
||||||
actors.value = data.actors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
setTimeout(() => {
|
|
||||||
queryInput.value?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.trigger {
|
.trigger {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
import { formatDate } from '#/utils/format.js';
|
||||||
|
import Gender from './gender.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { user, restriction } = pageContext;
|
||||||
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
|
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
||||||
|
const entityTooltipId = useId();
|
||||||
|
const aliasTooltipId = useId();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="tile"
|
class="tile"
|
||||||
@@ -41,7 +67,7 @@
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="actor.ageAtDeath"
|
v-if="actor.ageAtDeath"
|
||||||
:title="`Passed ${formatDate(actor.ageAtDeath, 'MMMM d, yyyy')}`"
|
:title="`Passed ${formatDate(actor.dateOfDeath, 'MMMM d, yyyy')}`"
|
||||||
class="age age-death"
|
class="age age-death"
|
||||||
>{{ actor.ageAtDeath }}</span>
|
>{{ actor.ageAtDeath }}</span>
|
||||||
|
|
||||||
@@ -74,42 +100,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="label">
|
<span class="label">
|
||||||
<span class="name ellipsis">{{ actor.name }}</span>
|
<span
|
||||||
|
class="name ellipsis"
|
||||||
|
:style="{ 'font-size': `${Math.max(0.9 + Math.min((17 - actor.name.length), 0) * 0.06, 0.65)}rem` }"
|
||||||
|
>{{ actor.name }}</span>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="actor.entity"
|
v-if="actor.entity"
|
||||||
v-tooltip="actor.entity.name"
|
v-tooltip="{ content: actor.entity.name, ariaId: entityTooltipId }"
|
||||||
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
||||||
class="favicon"
|
class="favicon"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="actor.alias && actor.alias.name !== actor.name"
|
||||||
|
v-tooltip="{ content: `Credited as '${actor.alias.name}'`, ariaId: aliasTooltipId }"
|
||||||
|
icon="at-sign"
|
||||||
|
class="alias"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
import { formatDate } from '#/utils/format.js';
|
|
||||||
|
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
|
||||||
import Gender from './gender.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
actor: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { user, restriction } = pageContext;
|
|
||||||
const pageStash = pageContext.pageProps.stash;
|
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
|
||||||
|
|
||||||
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tile {
|
.tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -128,6 +140,10 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
:deep(.bookmarks) .icon:not(.favorited):not(:hover) {
|
:deep(.bookmarks) .icon:not(.favorited):not(:hover) {
|
||||||
fill: var(--text-light);
|
fill: var(--text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unstashed {
|
&.unstashed {
|
||||||
@@ -135,7 +151,16 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
fill: var(--highlight-strong-20);
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
height: 1.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -149,7 +174,7 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
padding: .35rem .25rem .35rem .5rem;
|
padding: 0 .25rem 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favicon {
|
.favicon {
|
||||||
@@ -246,4 +271,11 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
height: .75rem;
|
height: .75rem;
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alias {
|
||||||
|
height: 100%;
|
||||||
|
fill: var(--glass-weak-20);
|
||||||
|
padding: 0 .25rem;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,31 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import { post } from '#/src/api.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
async function initCaches() {
|
||||||
|
await post('/caches', null, {
|
||||||
|
successFeedback: 'Reloaded caches',
|
||||||
|
errorFeedback: 'Failed to reload caches',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<ul class="nav-items nolist">
|
<ul class="nav-items nolist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
href="/admin/actors"
|
||||||
|
class="nav-link nolink"
|
||||||
|
:class="{ active: pageContext.routeParams.section === 'actors' }"
|
||||||
|
>Actors</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
href="/admin/revisions/scenes"
|
href="/admin/revisions/scenes"
|
||||||
@@ -39,22 +63,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
import { post } from '#/src/api.js';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
async function initCaches() {
|
|
||||||
await post('/caches', null, {
|
|
||||||
successFeedback: 'Reloaded caches',
|
|
||||||
errorFeedback: 'Failed to reload caches',
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,121 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import AlertDialog from '#/components/alerts/create.vue';
|
||||||
|
|
||||||
|
import { del, get } from '#/src/api.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const alerts = ref(pageContext.pageProps.alerts);
|
||||||
|
const showAlertDialog = ref(false);
|
||||||
|
const done = ref(true);
|
||||||
|
|
||||||
|
const query = ref('');
|
||||||
|
const filterActors = ref(false);
|
||||||
|
const filterEntities = ref(false);
|
||||||
|
const filterTags = ref(false);
|
||||||
|
const filterExpressions = ref(false);
|
||||||
|
const filterCombined = ref(null);
|
||||||
|
|
||||||
|
const filteredAlerts = computed(() => {
|
||||||
|
const queryRegex = new RegExp(query.value, 'i');
|
||||||
|
|
||||||
|
return alerts.value.filter((alert) => {
|
||||||
|
if (filterActors.value && alert.actors.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterEntities.value && alert.entities.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterTags.value && alert.tags.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterExpressions.value && alert.matches.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterCombined.value === false && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterCombined.value === true && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryRegex.test(alert.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.actors.some((actor) => queryRegex.test(actor.name))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.tags.some((tag) => queryRegex.test(tag.name))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.entities.some((entity) => queryRegex.test(entity.name))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.matches.some((match) => queryRegex.test(match.expression))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function reloadAlerts() {
|
||||||
|
alerts.value = await get('/alerts');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAlert(alert) {
|
||||||
|
if (done.value === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to remove alert #${alert.id}?`)) { // eslint-disable-line no-alert
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
const alertLabel = [
|
||||||
|
...alert.actors.map((actor) => actor.name),
|
||||||
|
...alert.tags.map((tag) => tag.name),
|
||||||
|
...alert.entities.map((entity) => entity.name),
|
||||||
|
...alert.matches.map((match) => match.expression),
|
||||||
|
].filter(Boolean).join(', ');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del(`/alerts/${alert.id}`, {
|
||||||
|
undoFeedback: `Removed alert for '${alertLabel}'`,
|
||||||
|
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await reloadAlerts();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterCombinedStates = [null, true, false];
|
||||||
|
|
||||||
|
function toggleFilterCombined() {
|
||||||
|
const index = filterCombinedStates.indexOf(filterCombined.value);
|
||||||
|
|
||||||
|
filterCombined.value = filterCombinedStates[(index + 1) % filterCombinedStates.length];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="profile-section">
|
<section class="profile-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -169,28 +287,28 @@
|
|||||||
<div class="alert-triggers">
|
<div class="alert-triggers">
|
||||||
<Icon
|
<Icon
|
||||||
v-if="alert.notify && alert.isFromPreset"
|
v-if="alert.notify && alert.isFromPreset"
|
||||||
v-tooltip="'Notify in traxxx, added as quick alert'"
|
v-tooltip="{ content: 'Notify in traxxx, added as quick alert', ariaId: `alert-notify-preset-${alert.id}` }"
|
||||||
icon="bell-plus"
|
icon="bell-plus"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-else-if="alert.notify"
|
v-else-if="alert.notify"
|
||||||
v-tooltip="'Notify in traxxx'"
|
v-tooltip="{ content: 'Notify in traxxx', ariaId: `alert-notify-${alert.id}` }"
|
||||||
icon="bell2"
|
icon="bell2"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-if="alert.stashes.some((stash) => !stash.isPrimary)"
|
v-if="alert.stashes.some((stash) => !stash.isPrimary)"
|
||||||
v-tooltip="`Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`"
|
v-tooltip="{ content: `Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`, ariaId: `alert-stashes-${alert.id}` }"
|
||||||
icon="folder-heart"
|
icon="folder-heart"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-else-if="alert.stashes.length > 0"
|
v-else-if="alert.stashes.length > 0"
|
||||||
v-tooltip="alert.stashes.length > 0 ? 'Add to Favorites' : undefined"
|
v-tooltip="alert.stashes.length > 0 ? { content: 'Add to Favorites', ariaId: `alert-favorites-${alert.id}` } : undefined"
|
||||||
icon="heart7"
|
icon="heart7"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
/>
|
/>
|
||||||
@@ -206,7 +324,7 @@
|
|||||||
|
|
||||||
<div class="alert-actions">
|
<div class="alert-actions">
|
||||||
<span
|
<span
|
||||||
v-tooltip="format(alert.createdAt, 'yyyy-MM-dd hh:mm')"
|
v-tooltip="{ content: format(alert.createdAt, 'yyyy-MM-dd hh:mm'), ariaId: `alert-created-${alert.id}` }"
|
||||||
class="alert-id"
|
class="alert-id"
|
||||||
title="Alert ID"
|
title="Alert ID"
|
||||||
>#{{ alert.id }}</span>
|
>#{{ alert.id }}</span>
|
||||||
@@ -227,123 +345,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
import AlertDialog from '#/components/alerts/create.vue';
|
|
||||||
|
|
||||||
import { get, del } from '#/src/api.js';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const alerts = ref(pageContext.pageProps.alerts);
|
|
||||||
const showAlertDialog = ref(false);
|
|
||||||
const done = ref(true);
|
|
||||||
|
|
||||||
const query = ref('');
|
|
||||||
const filterActors = ref(false);
|
|
||||||
const filterEntities = ref(false);
|
|
||||||
const filterTags = ref(false);
|
|
||||||
const filterExpressions = ref(false);
|
|
||||||
const filterCombined = ref(null);
|
|
||||||
|
|
||||||
const filteredAlerts = computed(() => {
|
|
||||||
const queryRegex = new RegExp(query.value, 'i');
|
|
||||||
|
|
||||||
return alerts.value.filter((alert) => {
|
|
||||||
if (filterActors.value && alert.actors.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterEntities.value && alert.entities.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterTags.value && alert.tags.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterExpressions.value && alert.matches.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterCombined.value === false && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterCombined.value === true && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length === 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryRegex.test(alert.id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alert.actors.some((actor) => queryRegex.test(actor.name))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alert.tags.some((tag) => queryRegex.test(tag.name))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alert.entities.some((entity) => queryRegex.test(entity.name))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alert.matches.some((match) => queryRegex.test(match.expression))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function reloadAlerts() {
|
|
||||||
alerts.value = await get('/alerts');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAlert(alert) {
|
|
||||||
if (done.value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to remove alert #${alert.id}?`)) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
const alertLabel = [
|
|
||||||
...alert.actors.map((actor) => actor.name),
|
|
||||||
...alert.tags.map((tag) => tag.name),
|
|
||||||
...alert.entities.map((entity) => entity.name),
|
|
||||||
...alert.matches.map((match) => match.expression),
|
|
||||||
].filter(Boolean).join(', ');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await del(`/alerts/${alert.id}`, {
|
|
||||||
undoFeedback: `Removed alert for '${alertLabel}'`,
|
|
||||||
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await reloadAlerts();
|
|
||||||
} finally {
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterCombinedStates = [null, true, false];
|
|
||||||
|
|
||||||
function toggleFilterCombined() {
|
|
||||||
const index = filterCombinedStates.indexOf(filterCombined.value);
|
|
||||||
|
|
||||||
filterCombined.value = filterCombinedStates[(index + 1) % filterCombinedStates.length];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.alerts {
|
.alerts {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,390 +1,11 @@
|
|||||||
<template>
|
|
||||||
<Dialog
|
|
||||||
title="New alert"
|
|
||||||
@close="emit('close')"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
class="dialog-body"
|
|
||||||
@submit.prevent="createAlert"
|
|
||||||
>
|
|
||||||
<div class="dialog-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h3 class="heading">IF</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field actors">
|
|
||||||
<span
|
|
||||||
v-tooltip="fieldsAnd ? 'The alert is triggered if all fields are matched.' : 'The alert is triggered if any of the fields are matched.'"
|
|
||||||
class="field-logic fields-logic noselect"
|
|
||||||
@click="fieldsAnd = !fieldsAnd"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
v-show="fieldsAnd"
|
|
||||||
icon="link3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
v-show="!fieldsAnd"
|
|
||||||
icon="unlink3"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="field-items nolist noselect"
|
|
||||||
:class="{ and: actorAnd, or: !actorAnd }"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(actor, index) in actors"
|
|
||||||
:key="`actor-${actor.id}`"
|
|
||||||
class="field-item actor"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="index > 0"
|
|
||||||
class="field-logic item-logic"
|
|
||||||
@click="actorAnd = !actorAnd"
|
|
||||||
>{{ actorAnd ? 'AND' : 'OR' }}</div>
|
|
||||||
|
|
||||||
<div class="field-tile">
|
|
||||||
<div class="field-label">{{ actor.name }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon="cross2"
|
|
||||||
class="field-remove"
|
|
||||||
@click="actors = actors.filter((selectedActor) => selectedActor.id !== actor.id)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="field-add">
|
|
||||||
<VDropdown @show="focusActorInput">
|
|
||||||
<button
|
|
||||||
class="button"
|
|
||||||
type="button"
|
|
||||||
><Icon icon="plus3" />Add actor</button>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<input
|
|
||||||
ref="actorInput"
|
|
||||||
v-model="actorQuery"
|
|
||||||
class="input"
|
|
||||||
@input="searchActors"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ul class="nolist">
|
|
||||||
<li
|
|
||||||
v-for="actor in actorResults"
|
|
||||||
:key="`actor-result-${actor.id}`"
|
|
||||||
v-close-popper
|
|
||||||
class="result-item"
|
|
||||||
@click="selectActor(actor)"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="actor.avatar"
|
|
||||||
class="field-avatar"
|
|
||||||
:src="getPath(actor.avatar, 'lazy')"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="field-avatar"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="result-label">
|
|
||||||
{{ actor.name }}
|
|
||||||
<template v-if="actor.ageFromBirth || actor.origin?.country">({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].filter(Boolean).join(', ') }})</template>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field tags">
|
|
||||||
<span
|
|
||||||
class="field-logic noselect"
|
|
||||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="field-items nolist noselect"
|
|
||||||
:class="{ and: actorAnd, or: !actorAnd }"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(tag, index) in tags"
|
|
||||||
:key="`tag-${tag.id}`"
|
|
||||||
class="field-item tag"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="index > 0"
|
|
||||||
class="field-logic item-logic"
|
|
||||||
@click="tagAnd = !tagAnd"
|
|
||||||
>{{ tagAnd ? 'AND' : 'OR' }}</div>
|
|
||||||
|
|
||||||
<div class="field-tile field-label">{{ tag.name }}</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon="cross2"
|
|
||||||
class="field-remove"
|
|
||||||
@click="tags = tags.filter((selectedTag) => selectedTag.id !== tag.id)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="field-add">
|
|
||||||
<VDropdown>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button"
|
|
||||||
><Icon icon="plus3" />Add tag</button>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<input
|
|
||||||
ref="tagInput"
|
|
||||||
v-model="tagQuery"
|
|
||||||
class="input"
|
|
||||||
@input="searchTags"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ul class="nolist">
|
|
||||||
<li
|
|
||||||
v-for="tag in tagResults"
|
|
||||||
:key="`tag-result-${tag.id}`"
|
|
||||||
v-close-popper
|
|
||||||
class="result-item result-label"
|
|
||||||
@click="selectTag(tag)"
|
|
||||||
>{{ tag.name }}</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field entities">
|
|
||||||
<span
|
|
||||||
class="field-logic noselect"
|
|
||||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
|
||||||
|
|
||||||
<ul class="field-items nolist noselect">
|
|
||||||
<li
|
|
||||||
v-for="(entity, index) in entities"
|
|
||||||
:key="`entity-${entity.id}`"
|
|
||||||
class="field-item entity"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="index > 0"
|
|
||||||
v-tooltip.click="{
|
|
||||||
content: 'Scenes are only associated to one channel, \'AND\' would never match.',
|
|
||||||
triggers: ['click'],
|
|
||||||
autoHide: true,
|
|
||||||
}"
|
|
||||||
class="field-logic"
|
|
||||||
>OR</div>
|
|
||||||
|
|
||||||
<div class="field-tile field-label">
|
|
||||||
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
|
||||||
{{ entity.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon="cross2"
|
|
||||||
class="field-remove"
|
|
||||||
@click="entities = entities.filter((selectedEntity) => selectedEntity.id !== entity.id)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="field-add">
|
|
||||||
<VDropdown>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button"
|
|
||||||
><Icon icon="plus3" />Add channel</button>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<input
|
|
||||||
ref="entityInput"
|
|
||||||
v-model="entityQuery"
|
|
||||||
class="input"
|
|
||||||
@input="searchEntities"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ul class="nolist">
|
|
||||||
<li
|
|
||||||
v-for="entity in entityResults"
|
|
||||||
:key="`entity-result-${entity.id}`"
|
|
||||||
v-close-popper
|
|
||||||
class="result-item result-label"
|
|
||||||
@click="selectEntity(entity)"
|
|
||||||
>
|
|
||||||
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
|
||||||
|
|
||||||
{{ entity.name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field matches">
|
|
||||||
<span
|
|
||||||
class="field-logic noselect"
|
|
||||||
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
|
||||||
|
|
||||||
<ul class="field-items nolist noselect">
|
|
||||||
<li
|
|
||||||
v-for="(match, index) in matches"
|
|
||||||
:key="`match-${match.property}-${match.expression}`"
|
|
||||||
class="field-item match"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="index > 0"
|
|
||||||
class="field-logic item-logic"
|
|
||||||
@click="matchAnd = !matchAnd"
|
|
||||||
>{{ matchAnd ? 'AND' : 'OR' }}</div>
|
|
||||||
|
|
||||||
<div class="field-tile field-label">
|
|
||||||
<strong>{{ match.property }}:</strong> {{ match.expression }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon="cross2"
|
|
||||||
class="field-remove"
|
|
||||||
@click="matches = matches.filter((selectedEntity, selectedIndex) => selectedIndex !== index)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="field-add">
|
|
||||||
<VDropdown>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button"
|
|
||||||
><Icon icon="plus3" />Add expression</button>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<form @submit.prevent="addMatch">
|
|
||||||
<select
|
|
||||||
v-model="matchProperty"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
<option value="title">Title</option>
|
|
||||||
<option value="description">Description</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input
|
|
||||||
v-model="matchExpression"
|
|
||||||
placeholder="Expression, // for regex"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog-section then">
|
|
||||||
<h3 class="heading">THEN</h3>
|
|
||||||
|
|
||||||
<label class="field notify">
|
|
||||||
<span>Notify me in traxxx</span>
|
|
||||||
<Checkbox
|
|
||||||
:checked="notify"
|
|
||||||
@change="(checked) => notify = checked"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<label class="field email">
|
|
||||||
<span>E-mail me</span>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
:checked="email"
|
|
||||||
@change="(checked) => email = checked"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="stash">
|
|
||||||
<ul class="field-items nolist noselect">
|
|
||||||
<li
|
|
||||||
v-for="stash in stashes"
|
|
||||||
:key="`stash-${stash.id}`"
|
|
||||||
class="field-item tag"
|
|
||||||
>
|
|
||||||
<div class="field-tile field-label stash">
|
|
||||||
<Icon
|
|
||||||
v-if="stash.isPrimary"
|
|
||||||
class="favorites"
|
|
||||||
icon="heart7"
|
|
||||||
/>{{ stash.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon="cross2"
|
|
||||||
class="field-remove"
|
|
||||||
@click="stashes = stashes.filter((selectedStash) => selectedStash.id !== stash.id)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<template v-if="stashes.length < assets.stashes.length">
|
|
||||||
<li class="field-add">
|
|
||||||
<button
|
|
||||||
v-if="stashes.length === 0"
|
|
||||||
type="button"
|
|
||||||
class="button favorites"
|
|
||||||
@click="selectStash(assets.primaryStash)"
|
|
||||||
><Icon icon="heart7" />Add to favorites</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="field-add">
|
|
||||||
<VDropdown>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button field-add"
|
|
||||||
><Icon icon="folder-heart" />Add to stash</button>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<ul class="nolist">
|
|
||||||
<li
|
|
||||||
v-for="stash in assets.stashes.filter((stash) => !stashes.some((selectedStash) => selectedStash.id === stash.id))"
|
|
||||||
:key="`stash-result-${stash.id}`"
|
|
||||||
v-close-popper
|
|
||||||
class="result-item result-stash result-label"
|
|
||||||
@click="selectStash(stash)"
|
|
||||||
>
|
|
||||||
{{ stash.name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dialog-section dialog-actions">
|
|
||||||
<button
|
|
||||||
class="button button-submit"
|
|
||||||
>Set alert</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject } from 'vue';
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
import { get, post } from '#/src/api.js';
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
import Dialog from '#/components/dialog/dialog.vue';
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
|
||||||
const { assets } = inject('pageContext');
|
import { get, post } from '#/src/api.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
presetActors: {
|
presetActors: {
|
||||||
@@ -403,6 +24,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['alert', 'close']);
|
const emit = defineEmits(['alert', 'close']);
|
||||||
|
|
||||||
|
const { assets } = inject('pageContext');
|
||||||
|
|
||||||
const actors = ref(props.presetActors);
|
const actors = ref(props.presetActors);
|
||||||
const actorResults = ref([]);
|
const actorResults = ref([]);
|
||||||
const actorInput = ref(null);
|
const actorInput = ref(null);
|
||||||
@@ -427,6 +50,13 @@ const actorAnd = ref(true);
|
|||||||
const tagAnd = ref(true);
|
const tagAnd = ref(true);
|
||||||
const matchAnd = ref(true);
|
const matchAnd = ref(true);
|
||||||
|
|
||||||
|
const fieldsLogicTooltipId = useId();
|
||||||
|
const addActorDropdownId = useId();
|
||||||
|
const addTagDropdownId = useId();
|
||||||
|
const addEntityDropdownId = useId();
|
||||||
|
const addMatchDropdownId = useId();
|
||||||
|
const addStashDropdownId = useId();
|
||||||
|
|
||||||
const notify = ref(true);
|
const notify = ref(true);
|
||||||
const email = ref(false);
|
const email = ref(false);
|
||||||
|
|
||||||
@@ -530,6 +160,387 @@ function selectStash(selectedStash) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
title="New alert"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
class="dialog-body"
|
||||||
|
@submit.prevent="createAlert"
|
||||||
|
>
|
||||||
|
<div class="dialog-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="heading">IF</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field actors">
|
||||||
|
<span
|
||||||
|
v-tooltip="{ content: fieldsAnd ? 'The alert is triggered if all fields are matched.' : 'The alert is triggered if any of the fields are matched.', ariaId: fieldsLogicTooltipId }"
|
||||||
|
class="field-logic fields-logic noselect"
|
||||||
|
@click="fieldsAnd = !fieldsAnd"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-show="fieldsAnd"
|
||||||
|
icon="link3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-show="!fieldsAnd"
|
||||||
|
icon="unlink3"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="field-items nolist noselect"
|
||||||
|
:class="{ and: actorAnd, or: !actorAnd }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(actor, index) in actors"
|
||||||
|
:key="`actor-${actor.id}`"
|
||||||
|
class="field-item actor"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="index > 0"
|
||||||
|
class="field-logic item-logic"
|
||||||
|
@click="actorAnd = !actorAnd"
|
||||||
|
>{{ actorAnd ? 'AND' : 'OR' }}</div>
|
||||||
|
|
||||||
|
<div class="field-tile">
|
||||||
|
<div class="field-label">{{ actor.name }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
class="field-remove"
|
||||||
|
@click="actors = actors.filter((selectedActor) => selectedActor.id !== actor.id)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="field-add">
|
||||||
|
<VDropdown
|
||||||
|
:aria-id="addActorDropdownId"
|
||||||
|
@show="focusActorInput"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
type="button"
|
||||||
|
><Icon icon="plus3" />Add actor</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<input
|
||||||
|
ref="actorInput"
|
||||||
|
v-model="actorQuery"
|
||||||
|
class="input"
|
||||||
|
@input="searchActors"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ul class="nolist">
|
||||||
|
<li
|
||||||
|
v-for="actor in actorResults"
|
||||||
|
:key="`actor-result-${actor.id}`"
|
||||||
|
v-close-popper
|
||||||
|
class="result-item"
|
||||||
|
@click="selectActor(actor)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="actor.avatar"
|
||||||
|
class="field-avatar"
|
||||||
|
:src="getPath(actor.avatar, 'lazy')"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="field-avatar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="result-label">
|
||||||
|
{{ actor.name }}
|
||||||
|
<template v-if="actor.ageFromBirth || actor.origin?.country">({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].filter(Boolean).join(', ') }})</template>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field tags">
|
||||||
|
<span
|
||||||
|
class="field-logic noselect"
|
||||||
|
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="field-items nolist noselect"
|
||||||
|
:class="{ and: actorAnd, or: !actorAnd }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(tag, index) in tags"
|
||||||
|
:key="`tag-${tag.id}`"
|
||||||
|
class="field-item tag"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="index > 0"
|
||||||
|
class="field-logic item-logic"
|
||||||
|
@click="tagAnd = !tagAnd"
|
||||||
|
>{{ tagAnd ? 'AND' : 'OR' }}</div>
|
||||||
|
|
||||||
|
<div class="field-tile field-label">{{ tag.name }}</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
class="field-remove"
|
||||||
|
@click="tags = tags.filter((selectedTag) => selectedTag.id !== tag.id)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="field-add">
|
||||||
|
<VDropdown :aria-id="addTagDropdownId">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
><Icon icon="plus3" />Add tag</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<input
|
||||||
|
ref="tagInput"
|
||||||
|
v-model="tagQuery"
|
||||||
|
class="input"
|
||||||
|
@input="searchTags"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ul class="nolist">
|
||||||
|
<li
|
||||||
|
v-for="tag in tagResults"
|
||||||
|
:key="`tag-result-${tag.id}`"
|
||||||
|
v-close-popper
|
||||||
|
class="result-item result-label"
|
||||||
|
@click="selectTag(tag)"
|
||||||
|
>{{ tag.name }}</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field entities">
|
||||||
|
<span
|
||||||
|
class="field-logic noselect"
|
||||||
|
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||||
|
|
||||||
|
<ul class="field-items nolist noselect">
|
||||||
|
<li
|
||||||
|
v-for="(entity, index) in entities"
|
||||||
|
:key="`entity-${entity.id}`"
|
||||||
|
class="field-item entity"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="index > 0"
|
||||||
|
v-tooltip.click="{
|
||||||
|
content: 'Scenes are only associated to one channel, \'AND\' would never match.',
|
||||||
|
triggers: ['click'],
|
||||||
|
autoHide: true,
|
||||||
|
ariaId: `entity-and-tooltip-${entity.id}`,
|
||||||
|
}"
|
||||||
|
class="field-logic"
|
||||||
|
>OR</div>
|
||||||
|
|
||||||
|
<div class="field-tile field-label">
|
||||||
|
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
||||||
|
{{ entity.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
class="field-remove"
|
||||||
|
@click="entities = entities.filter((selectedEntity) => selectedEntity.id !== entity.id)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="field-add">
|
||||||
|
<VDropdown :aria-id="addEntityDropdownId">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
><Icon icon="plus3" />Add channel</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<input
|
||||||
|
ref="entityInput"
|
||||||
|
v-model="entityQuery"
|
||||||
|
class="input"
|
||||||
|
@input="searchEntities"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ul class="nolist">
|
||||||
|
<li
|
||||||
|
v-for="entity in entityResults"
|
||||||
|
:key="`entity-result-${entity.id}`"
|
||||||
|
v-close-popper
|
||||||
|
class="result-item result-label"
|
||||||
|
@click="selectEntity(entity)"
|
||||||
|
>
|
||||||
|
<Icon :icon="entity.type === 'network' ? 'device_hub' : 'tv'" />
|
||||||
|
|
||||||
|
{{ entity.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field matches">
|
||||||
|
<span
|
||||||
|
class="field-logic noselect"
|
||||||
|
>{{ fieldsAnd ? 'AND' : 'OR' }}</span>
|
||||||
|
|
||||||
|
<ul class="field-items nolist noselect">
|
||||||
|
<li
|
||||||
|
v-for="(match, index) in matches"
|
||||||
|
:key="`match-${match.property}-${match.expression}`"
|
||||||
|
class="field-item match"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="index > 0"
|
||||||
|
class="field-logic item-logic"
|
||||||
|
@click="matchAnd = !matchAnd"
|
||||||
|
>{{ matchAnd ? 'AND' : 'OR' }}</div>
|
||||||
|
|
||||||
|
<div class="field-tile field-label">
|
||||||
|
<strong>{{ match.property }}:</strong> {{ match.expression }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
class="field-remove"
|
||||||
|
@click="matches = matches.filter((selectedEntity, selectedIndex) => selectedIndex !== index)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="field-add">
|
||||||
|
<VDropdown :aria-id="addMatchDropdownId">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
><Icon icon="plus3" />Add expression</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<form @submit.prevent="addMatch">
|
||||||
|
<select
|
||||||
|
v-model="matchProperty"
|
||||||
|
class="input"
|
||||||
|
>
|
||||||
|
<option value="title">Title</option>
|
||||||
|
<option value="description">Description</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="matchExpression"
|
||||||
|
placeholder="Expression, // for regex"
|
||||||
|
class="input"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-section then">
|
||||||
|
<h3 class="heading">THEN</h3>
|
||||||
|
|
||||||
|
<label class="field notify">
|
||||||
|
<span>Notify me in traxxx</span>
|
||||||
|
<Checkbox
|
||||||
|
:checked="notify"
|
||||||
|
@change="(checked) => notify = checked"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<label class="field email">
|
||||||
|
<span>E-mail me</span>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
:checked="email"
|
||||||
|
@change="(checked) => email = checked"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="stash">
|
||||||
|
<ul class="field-items nolist noselect">
|
||||||
|
<li
|
||||||
|
v-for="stash in stashes"
|
||||||
|
:key="`stash-${stash.id}`"
|
||||||
|
class="field-item tag"
|
||||||
|
>
|
||||||
|
<div class="field-tile field-label stash">
|
||||||
|
<Icon
|
||||||
|
v-if="stash.isPrimary"
|
||||||
|
class="favorites"
|
||||||
|
icon="heart7"
|
||||||
|
/>{{ stash.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="cross2"
|
||||||
|
class="field-remove"
|
||||||
|
@click="stashes = stashes.filter((selectedStash) => selectedStash.id !== stash.id)"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<template v-if="stashes.length < assets.stashes.length">
|
||||||
|
<li class="field-add">
|
||||||
|
<button
|
||||||
|
v-if="stashes.length === 0"
|
||||||
|
type="button"
|
||||||
|
class="button favorites"
|
||||||
|
@click="selectStash(assets.primaryStash)"
|
||||||
|
><Icon icon="heart7" />Add to favorites</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="field-add">
|
||||||
|
<VDropdown :aria-id="addStashDropdownId">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button field-add"
|
||||||
|
><Icon icon="folder-heart" />Add to stash</button>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<ul class="nolist">
|
||||||
|
<li
|
||||||
|
v-for="stash in assets.stashes.filter((stash) => !stashes.some((selectedStash) => selectedStash.id === stash.id))"
|
||||||
|
:key="`stash-result-${stash.id}`"
|
||||||
|
v-close-popper
|
||||||
|
class="result-item result-stash result-label"
|
||||||
|
@click="selectStash(stash)"
|
||||||
|
>
|
||||||
|
{{ stash.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-section dialog-actions">
|
||||||
|
<button
|
||||||
|
class="button button-submit"
|
||||||
|
>Set alert</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
campaign: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { restriction } = pageContext;
|
||||||
|
|
||||||
|
// console.log(props.campaign);
|
||||||
|
|
||||||
|
const bannerSrc = (() => {
|
||||||
|
if (props.campaign.banner) {
|
||||||
|
// if (props.campaign.banner.entity.type === 'network' || props.campaign.banner.entity.isIndependent || !props.campaign.banner.entity.parent) {
|
||||||
|
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
||||||
|
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.campaign.banner.entity.type === 'channel' && props.campaign.banner.entity.parent?.type === 'network') {
|
||||||
|
return `/banners/${props.campaign.banner.entity.parent.slug}/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="restriction"
|
v-if="restriction"
|
||||||
@@ -12,7 +46,6 @@
|
|||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
v-else-if="campaign?.banner?.type === 'html'"
|
v-else-if="campaign?.banner?.type === 'html'"
|
||||||
ref="iframe"
|
|
||||||
:width="campaign.banner.width"
|
:width="campaign.banner.width"
|
||||||
:height="campaign.banner.height"
|
:height="campaign.banner.height"
|
||||||
:src="bannerSrc"
|
:src="bannerSrc"
|
||||||
@@ -45,41 +78,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { restriction } = pageContext;
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
campaign: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
backdrop: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(props.campaign);
|
|
||||||
|
|
||||||
const bannerSrc = (() => {
|
|
||||||
if (props.campaign.banner) {
|
|
||||||
// if (props.campaign.banner.entity.type === 'network' || props.campaign.banner.entity.isIndependent || !props.campaign.banner.entity.parent) {
|
|
||||||
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
|
||||||
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.campaign.banner.entity.type === 'channel' && props.campaign.banner.entity.parent?.type === 'network') {
|
|
||||||
return `/banners/${props.campaign.banner.entity.parent.slug}/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.campaign {
|
.campaign {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
confirmClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['open', 'close', 'event']);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (!props.confirmClose || confirm('You have unchanged changes, are you sure you want to close the dialog?')) { // eslint-disable-line no-alert
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => emit('open'));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="#container">
|
<Teleport to="#container">
|
||||||
<div
|
<div
|
||||||
@@ -24,31 +49,6 @@
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
confirmClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'close', 'event']);
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
if (!props.confirmClose || confirm('You have unchanged changes, are you sure you want to close the dialog?')) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => emit('open'));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
stash: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['scenes', 'actors', 'movies'],
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
type: String,
|
||||||
|
default: 'scenes',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="domains">
|
<nav class="domains">
|
||||||
<Link
|
<Link
|
||||||
@@ -32,27 +53,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
stash: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
domains: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ['scenes', 'actors', 'movies'],
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
type: String,
|
|
||||||
default: 'scenes',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.domains {
|
.domains {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import ActorSearch from '#/components/actors/search.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['actors']);
|
||||||
|
|
||||||
|
const newActors = ref([]);
|
||||||
|
|
||||||
|
function addActor(actor) {
|
||||||
|
newActors.value = newActors.value.concat(actor);
|
||||||
|
|
||||||
|
emit('actors', props.edits.actors.concat(actor.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.scene, () => {
|
||||||
|
newActors.value = [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
class="actors nolist"
|
class="actors nolist"
|
||||||
@@ -40,43 +79,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import ActorSearch from '#/components/actors/search.vue';
|
|
||||||
|
|
||||||
const newActors = ref([]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
scene: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['actors']);
|
|
||||||
|
|
||||||
function addActor(actor) {
|
|
||||||
newActors.value = newActors.value.concat(actor);
|
|
||||||
|
|
||||||
emit('actors', props.edits.actors.concat(actor.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.scene, () => { newActors.value = []; });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.actors {
|
.actors {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['augmentation']);
|
||||||
|
const augmentation = reactive(props.edits.augmentation);
|
||||||
|
|
||||||
|
watch(augmentation, () => emit('augmentation', augmentation));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="figure augmentation"
|
class="figure augmentation"
|
||||||
@@ -208,23 +228,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, watch } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['augmentation']);
|
|
||||||
const augmentation = reactive(props.edits.augmentation);
|
|
||||||
|
|
||||||
watch(augmentation, () => emit('augmentation', augmentation));
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
avatar: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="avatar noshrink">
|
<div class="avatar noshrink">
|
||||||
<img
|
<img
|
||||||
@@ -19,10 +31,16 @@
|
|||||||
>{{ avatar.sharpness.toFixed(2) }}</span>
|
>{{ avatar.sharpness.toFixed(2) }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
:title="`Added ${format(avatar.createdAt, 'yyyy-MM-dd')}, may not reflect photo age`"
|
||||||
|
class="avatar-date"
|
||||||
|
>{{ format(avatar.createdAt, '\'\'yy-MM') }}</span>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="getPath(avatar)"
|
:href="getPath(avatar)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="avatar-zoom"
|
class="avatar-zoom"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="search"
|
icon="search"
|
||||||
@@ -31,17 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
avatar: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.avatar {
|
.avatar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -89,7 +96,8 @@ defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar-meta,
|
.avatar-meta,
|
||||||
.avatar-credit {
|
.avatar-credit,
|
||||||
|
.avatar-date {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -112,4 +120,10 @@ defineProps({
|
|||||||
bottom: .75rem;
|
bottom: .75rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-date {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,104 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="figure">
|
|
||||||
<div class="value-section">
|
|
||||||
<span class="value-label">Units</span>
|
|
||||||
|
|
||||||
<select
|
|
||||||
:value="units"
|
|
||||||
class="input"
|
|
||||||
:disabled="!editing.has('figure')"
|
|
||||||
@change="emit('units', $event.target.value)"
|
|
||||||
>
|
|
||||||
<option value="us">USA</option>
|
|
||||||
<option value="uk">UK</option>
|
|
||||||
<option value="eu">Europe/Asia</option>
|
|
||||||
<option value="jp">Japan</option>
|
|
||||||
<option value="au">Australia</option>
|
|
||||||
<option value="it">Italy</option>
|
|
||||||
<option value="fr">France</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="figure-bust">
|
|
||||||
<div class="value-section">
|
|
||||||
<span class="value-label">Bust</span>
|
|
||||||
|
|
||||||
<select
|
|
||||||
v-model="figure.bust"
|
|
||||||
class="select input"
|
|
||||||
placeholder="Bust"
|
|
||||||
:disabled="!editing.has('figure')"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
:key="`figure-bust-unknown`"
|
|
||||||
:value="null"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<option
|
|
||||||
v-for="bust in bustSizes[units]"
|
|
||||||
:key="`figure-bust-${bust}`"
|
|
||||||
:value="Array.isArray(bust) ? bust[0] : bust"
|
|
||||||
>{{ Array.isArray(bust) ? bust.join('/') : bust }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="value-section">
|
|
||||||
<span class="value-label">Cup</span>
|
|
||||||
|
|
||||||
<select
|
|
||||||
v-model="figure.cup"
|
|
||||||
class="select input"
|
|
||||||
placeholder="Cup"
|
|
||||||
:disabled="!editing.has('figure')"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
:key="`figure-cup-unknown`"
|
|
||||||
:value="null"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<option
|
|
||||||
v-for="cup in cupSizes[units]"
|
|
||||||
:key="`figure-cup-${cup}`"
|
|
||||||
:value="Array.isArray(cup) ? cup[0] : cup"
|
|
||||||
>{{ Array.isArray(cup) ? cup.join('/') : cup }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="value-section">
|
|
||||||
<span class="value-label">Waist</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<input
|
|
||||||
v-model="figure.waist"
|
|
||||||
type="number"
|
|
||||||
class="input"
|
|
||||||
:disabled="!editing.has('figure')"
|
|
||||||
>
|
|
||||||
|
|
||||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
|
||||||
<template v-else> cm</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="value-section">
|
|
||||||
<span class="value-label">Hip</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<input
|
|
||||||
v-model="figure.hip"
|
|
||||||
type="number"
|
|
||||||
class="input"
|
|
||||||
:disabled="!editing.has('figure')"
|
|
||||||
>
|
|
||||||
|
|
||||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
|
||||||
<template v-else> cm</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from 'vue';
|
import { reactive, watch } from 'vue';
|
||||||
|
|
||||||
@@ -145,3 +44,104 @@ const bustSizes = {
|
|||||||
bustSizes.uk = bustSizes.us;
|
bustSizes.uk = bustSizes.us;
|
||||||
bustSizes.jp = bustSizes.eu;
|
bustSizes.jp = bustSizes.eu;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="figure">
|
||||||
|
<div class="value-section">
|
||||||
|
<span class="value-label">Units</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
:value="units"
|
||||||
|
class="input"
|
||||||
|
:disabled="!editing.has('figure')"
|
||||||
|
@change="emit('units', $event.target.value)"
|
||||||
|
>
|
||||||
|
<option value="us">USA</option>
|
||||||
|
<option value="uk">UK</option>
|
||||||
|
<option value="eu">Europe/Asia</option>
|
||||||
|
<option value="jp">Japan</option>
|
||||||
|
<option value="au">Australia</option>
|
||||||
|
<option value="it">Italy</option>
|
||||||
|
<option value="fr">France</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="figure-bust">
|
||||||
|
<div class="value-section">
|
||||||
|
<span class="value-label">Bust</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="figure.bust"
|
||||||
|
class="select input"
|
||||||
|
placeholder="Bust"
|
||||||
|
:disabled="!editing.has('figure')"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key="figure-bust-unknown"
|
||||||
|
:value="null"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<option
|
||||||
|
v-for="bust in bustSizes[units]"
|
||||||
|
:key="`figure-bust-${bust}`"
|
||||||
|
:value="Array.isArray(bust) ? bust[0] : bust"
|
||||||
|
>{{ Array.isArray(bust) ? bust.join('/') : bust }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-section">
|
||||||
|
<span class="value-label">Cup</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="figure.cup"
|
||||||
|
class="select input"
|
||||||
|
placeholder="Cup"
|
||||||
|
:disabled="!editing.has('figure')"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key="figure-cup-unknown"
|
||||||
|
:value="null"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<option
|
||||||
|
v-for="cup in cupSizes[units]"
|
||||||
|
:key="`figure-cup-${cup}`"
|
||||||
|
:value="Array.isArray(cup) ? cup[0] : cup"
|
||||||
|
>{{ Array.isArray(cup) ? cup.join('/') : cup }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="value-section">
|
||||||
|
<span class="value-label">Waist</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
v-model="figure.waist"
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
:disabled="!editing.has('figure')"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||||
|
<template v-else> cm</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-section">
|
||||||
|
<span class="value-label">Hip</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
v-model="figure.hip"
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
:disabled="!editing.has('figure')"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||||
|
<template v-else> cm</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import MovieSearch from '#/components/movies/search.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['movies']);
|
||||||
|
|
||||||
|
const newMovies = ref([]);
|
||||||
|
|
||||||
|
function addMovie(movie) {
|
||||||
|
newMovies.value = newMovies.value.concat(movie);
|
||||||
|
|
||||||
|
console.log(movie);
|
||||||
|
|
||||||
|
emit('movies', props.edits.movies.concat(movie.id));
|
||||||
|
|
||||||
|
console.log(props.edits);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.scene, () => {
|
||||||
|
newMovies.value = [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
class="movies nolist"
|
class="movies nolist"
|
||||||
@@ -40,47 +83,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import MovieSearch from '#/components/movies/search.vue';
|
|
||||||
|
|
||||||
const newMovies = ref([]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
scene: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['movies']);
|
|
||||||
|
|
||||||
function addMovie(movie) {
|
|
||||||
newMovies.value = newMovies.value.concat(movie);
|
|
||||||
|
|
||||||
console.log(movie);
|
|
||||||
|
|
||||||
emit('movies', props.edits.movies.concat(movie.id));
|
|
||||||
|
|
||||||
console.log(props.edits);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.scene, () => { newMovies.value = []; });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.movies {
|
.movies {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
units: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['penis', 'units']);
|
||||||
|
const penis = reactive(props.edits.penis);
|
||||||
|
|
||||||
|
watch(penis, () => emit('penis', penis));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="figure penis"
|
class="figure penis"
|
||||||
@@ -83,27 +107,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, watch } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
units: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['penis', 'units']);
|
|
||||||
const penis = reactive(props.edits.penis);
|
|
||||||
|
|
||||||
watch(penis, () => emit('penis', penis));
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, reactive, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['place']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const countries = pageContext.pageProps.countries;
|
||||||
|
|
||||||
|
const place = reactive(props.edits[props.item.key]);
|
||||||
|
|
||||||
|
watch(place, () => emit('place', place));
|
||||||
|
|
||||||
|
const topCountries = [
|
||||||
|
'AU',
|
||||||
|
'BR',
|
||||||
|
'CZ',
|
||||||
|
'DE',
|
||||||
|
'JP',
|
||||||
|
'RU',
|
||||||
|
'GB',
|
||||||
|
'US',
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortedCountries = countries.toSorted((countryA, countryB) => topCountries.indexOf(countryB.alpha2) - topCountries.indexOf(countryA.alpha2));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="place"
|
class="place"
|
||||||
@@ -35,43 +75,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, watch, inject } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const countries = pageContext.pageProps.countries;
|
|
||||||
|
|
||||||
const emit = defineEmits(['place']);
|
|
||||||
const place = reactive(props.edits[props.item.key]);
|
|
||||||
|
|
||||||
watch(place, () => emit('place', place));
|
|
||||||
|
|
||||||
const topCountries = [
|
|
||||||
'AU',
|
|
||||||
'BR',
|
|
||||||
'CZ',
|
|
||||||
'DE',
|
|
||||||
'JP',
|
|
||||||
'RU',
|
|
||||||
'GB',
|
|
||||||
'US',
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortedCountries = countries.toSorted((countryA, countryB) => topCountries.indexOf(countryB.alpha2) - topCountries.indexOf(countryA.alpha2));
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import formatTemplate from 'template-format';
|
||||||
|
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
rev: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
socials: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { env } = pageContext;
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
twitter: 'twitter-x',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUrl(social) {
|
||||||
|
if (social.url) {
|
||||||
|
return social.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.socials.urls[social.platform]) {
|
||||||
|
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="socials nolist">
|
<ul class="socials nolist">
|
||||||
<li
|
<li
|
||||||
@@ -38,46 +77,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
import formatTemplate from 'template-format';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { env } = pageContext;
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
rev: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
socials: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
twitter: 'twitter-x',
|
|
||||||
};
|
|
||||||
|
|
||||||
function getUrl(social) {
|
|
||||||
if (social.url) {
|
|
||||||
return social.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.socials.urls[social.platform]) {
|
|
||||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.socials {
|
.socials {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,202 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import Avatar from '#/components/edit/avatar.vue';
|
||||||
|
import Socials from '#/components/edit/revision-socials.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
|
||||||
|
import { get, post } from '#/src/api.js';
|
||||||
|
import { formatDuration } from '#/utils/format.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
context: {
|
||||||
|
type: String,
|
||||||
|
default: 'admin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const revisions = ref(pageContext.pageProps.revisions);
|
||||||
|
const domain = pageContext.routeParams.domain;
|
||||||
|
|
||||||
|
const actors = ref(pageContext.pageProps.actors);
|
||||||
|
const tags = ref(pageContext.pageProps.tags);
|
||||||
|
const movies = ref(pageContext.pageProps.movies);
|
||||||
|
const avatars = ref(pageContext.pageProps.avatars);
|
||||||
|
|
||||||
|
const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor])));
|
||||||
|
const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag])));
|
||||||
|
const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie])));
|
||||||
|
const avatarsById = computed(() => Object.fromEntries(avatars.value.map((avatar) => [avatar.id, avatar])));
|
||||||
|
|
||||||
|
const feedbacks = ref({});
|
||||||
|
const showReviewed = ref(false);
|
||||||
|
const reviewedRevisions = ref(new Set());
|
||||||
|
const expanded = ref(new Set());
|
||||||
|
|
||||||
|
const mappedKeys = {
|
||||||
|
actors: actorsById,
|
||||||
|
// tags: tagsById,
|
||||||
|
movies: moviesById,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateKeys = [
|
||||||
|
'date',
|
||||||
|
'dateOfBirth',
|
||||||
|
'dateOfDeath',
|
||||||
|
'productionDate',
|
||||||
|
'createdAt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const curatedKeys = {
|
||||||
|
duration: (duration) => formatDuration(duration),
|
||||||
|
};
|
||||||
|
|
||||||
|
const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||||
|
const curatedBase = Object.fromEntries(Object.entries(revision.base).map(([key, value]) => {
|
||||||
|
if (Array.isArray(value) && mappedKeys[key]) {
|
||||||
|
return [key, value.map((itemId) => ({
|
||||||
|
id: itemId,
|
||||||
|
name: mappedKeys[key].value[itemId]?.name || mappedKeys[key].value[itemId]?.title,
|
||||||
|
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItemId) => deltaItemId === itemId)),
|
||||||
|
}))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'tags') {
|
||||||
|
return [key, value.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
name: tag.actorId
|
||||||
|
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||||
|
: tagsById.value[tag.id]?.name,
|
||||||
|
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaTag) => deltaTag.id === tag.id && (!Object.hasOwn(tag, 'actorId') || deltaTag.actorId === tag.actorId))),
|
||||||
|
}))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'socials') {
|
||||||
|
// new socials don't have IDs yet, so we need to compare the values
|
||||||
|
return [key, value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItem) => deltaItem.url === item.url || `${deltaItem.platform}:${deltaItem.handle}` === `${item.platform}:${item.handle}`)),
|
||||||
|
}))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateKeys.includes(key) && value) {
|
||||||
|
return [key, new Date(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curatedKeys[key]) {
|
||||||
|
return [key, curatedKeys[key](value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [key, value];
|
||||||
|
}));
|
||||||
|
|
||||||
|
const curatedDeltas = revision.deltas.map((delta) => {
|
||||||
|
if (Array.isArray(delta.value) && mappedKeys[delta.key]) {
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: delta.value.map((itemId) => ({
|
||||||
|
id: itemId,
|
||||||
|
name: mappedKeys[delta.key].value[itemId]?.name || mappedKeys[delta.key].value[itemId]?.title,
|
||||||
|
modified: !revision.base[delta.key].includes(itemId),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'tags') {
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: delta.value.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
name: tag.actorId
|
||||||
|
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||||
|
: tagsById.value[tag.id]?.name,
|
||||||
|
modified: !revision.base[delta.key].some((baseTag) => baseTag.id === tag.id && (!Object.hasOwn(baseTag, 'actorId') || baseTag.actorId === tag.actorId)),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'socials') {
|
||||||
|
// new socials don't have IDs yet, so we need to compare the values
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: delta.value.map((social) => ({
|
||||||
|
...social,
|
||||||
|
modified: !revision.base[delta.key].some((baseItem) => baseItem.url === social.url || `${baseItem.platform}:${baseItem.handle}` === `${social.platform}:${social.handle}`),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateKeys.includes(delta.key) && delta.value) {
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: new Date(delta.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curatedKeys[delta.key]) {
|
||||||
|
return {
|
||||||
|
...delta,
|
||||||
|
value: curatedKeys[delta.key](delta.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...revision,
|
||||||
|
base: curatedBase,
|
||||||
|
deltas: curatedDeltas,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function reloadRevisions() {
|
||||||
|
const updatedRevisions = await get(`/revisions/${domain}`, {
|
||||||
|
isFinalized: showReviewed.value ? undefined : false,
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
actors.value = updatedRevisions.actors;
|
||||||
|
tags.value = updatedRevisions.tags;
|
||||||
|
movies.value = updatedRevisions.movies;
|
||||||
|
avatars.value = updatedRevisions.avatars;
|
||||||
|
revisions.value = updatedRevisions.revisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reviewRevision(revision, isApproved) {
|
||||||
|
reviewedRevisions.value.add(revision.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||||
|
isApproved,
|
||||||
|
feedback: feedbacks.value[revision.id],
|
||||||
|
}, {
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||||
|
revisionId: revision.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
revisions.value = revisions.value.map((rev) => (rev.id === updatedRevision.revision.id ? updatedRevision.revision : rev));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
reviewedRevisions.value.delete(revision.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function banEditor(revision) {
|
||||||
|
await post('/bans', {
|
||||||
|
userId: revision.user.id,
|
||||||
|
banIp: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await reviewRevision(revision, false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div
|
<div
|
||||||
@@ -21,7 +220,7 @@
|
|||||||
class="rev"
|
class="rev"
|
||||||
:class="{
|
:class="{
|
||||||
reviewed: reviewedRevisions.has(rev.id),
|
reviewed: reviewedRevisions.has(rev.id),
|
||||||
expanded: context === 'admin' || expanded.has(rev.id)
|
expanded: context === 'admin' || expanded.has(rev.id),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
||||||
@@ -65,14 +264,14 @@
|
|||||||
|
|
||||||
<template v-else-if="context === 'admin'">
|
<template v-else-if="context === 'admin'">
|
||||||
<Icon
|
<Icon
|
||||||
v-tooltip="`Ban user from submitting revisions`"
|
v-tooltip="{ content: 'Ban user from submitting revisions', ariaId: `rev-ban-${rev.id}` }"
|
||||||
icon="user-block"
|
icon="user-block"
|
||||||
class="review-reject review-ban"
|
class="review-reject review-ban"
|
||||||
@click="banEditor(rev)"
|
@click="banEditor(rev)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-tooltip="`Reject revision`"
|
v-tooltip="{ content: 'Reject revision', ariaId: `rev-reject-${rev.id}` }"
|
||||||
icon="blocked"
|
icon="blocked"
|
||||||
class="review-reject"
|
class="review-reject"
|
||||||
@click="reviewRevision(rev, false)"
|
@click="reviewRevision(rev, false)"
|
||||||
@@ -85,7 +284,7 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-tooltip="`Approve and apply revision`"
|
v-tooltip="{ content: 'Approve and apply revision', ariaId: `rev-approve-${rev.id}` }"
|
||||||
icon="checkmark"
|
icon="checkmark"
|
||||||
class="review-approve"
|
class="review-approve"
|
||||||
@click="reviewRevision(rev, true)"
|
@click="reviewRevision(rev, true)"
|
||||||
@@ -209,7 +408,7 @@
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
v-if="rev.user && context !== 'user'"
|
v-if="rev.user && context !== 'user'"
|
||||||
v-tooltip="rev.user.username"
|
v-tooltip="{ content: rev.user.username, ariaId: `rev-user-${rev.id}` }"
|
||||||
:href="`/user/${rev.user.username}`"
|
:href="`/user/${rev.user.username}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="rev-avatar nolink ellipsis"
|
class="rev-avatar nolink ellipsis"
|
||||||
@@ -227,181 +426,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
import Avatar from '#/components/edit/avatar.vue';
|
|
||||||
import Socials from '#/components/edit/revision-socials.vue';
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
|
|
||||||
import { get, post } from '#/src/api.js';
|
|
||||||
import { formatDuration } from '#/utils/format.js';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
context: {
|
|
||||||
type: String,
|
|
||||||
default: 'admin',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const revisions = ref(pageContext.pageProps.revisions);
|
|
||||||
const domain = pageContext.routeParams.domain;
|
|
||||||
|
|
||||||
const actors = ref(pageContext.pageProps.actors);
|
|
||||||
const tags = ref(pageContext.pageProps.tags);
|
|
||||||
const movies = ref(pageContext.pageProps.movies);
|
|
||||||
const avatars = ref(pageContext.pageProps.avatars);
|
|
||||||
|
|
||||||
const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor])));
|
|
||||||
const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag])));
|
|
||||||
const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie])));
|
|
||||||
const avatarsById = computed(() => Object.fromEntries(avatars.value.map((avatar) => [avatar.id, avatar])));
|
|
||||||
|
|
||||||
const feedbacks = ref({});
|
|
||||||
const showReviewed = ref(false);
|
|
||||||
const reviewedRevisions = ref(new Set());
|
|
||||||
const expanded = ref(new Set());
|
|
||||||
|
|
||||||
const mappedKeys = {
|
|
||||||
actors: actorsById,
|
|
||||||
tags: tagsById,
|
|
||||||
movies: moviesById,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateKeys = [
|
|
||||||
'date',
|
|
||||||
'dateOfBirth',
|
|
||||||
'dateOfDeath',
|
|
||||||
'productionDate',
|
|
||||||
'createdAt',
|
|
||||||
];
|
|
||||||
|
|
||||||
const curatedKeys = {
|
|
||||||
duration: (duration) => formatDuration(duration),
|
|
||||||
};
|
|
||||||
|
|
||||||
const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
|
||||||
const curatedBase = Object.fromEntries(Object.entries(revision.base).map(([key, value]) => {
|
|
||||||
if (Array.isArray(value) && mappedKeys[key]) {
|
|
||||||
return [key, value.map((itemId) => ({
|
|
||||||
id: itemId,
|
|
||||||
name: mappedKeys[key].value[itemId]?.name || mappedKeys[key].value[itemId]?.title,
|
|
||||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItemId) => deltaItemId === itemId)),
|
|
||||||
}))];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'socials') {
|
|
||||||
// new socials don't have IDs yet, so we need to compare the values
|
|
||||||
return [key, value.map((item) => ({
|
|
||||||
...item,
|
|
||||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItem) => deltaItem.url === item.url || `${deltaItem.platform}:${deltaItem.handle}` === `${item.platform}:${item.handle}`)),
|
|
||||||
}))];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateKeys.includes(key) && value) {
|
|
||||||
return [key, new Date(value)];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curatedKeys[key]) {
|
|
||||||
return [key, curatedKeys[key](value)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [key, value];
|
|
||||||
}));
|
|
||||||
|
|
||||||
const curatedDeltas = revision.deltas.map((delta) => {
|
|
||||||
if (Array.isArray(delta.value) && mappedKeys[delta.key]) {
|
|
||||||
return {
|
|
||||||
...delta,
|
|
||||||
value: delta.value.map((itemId) => ({
|
|
||||||
id: itemId,
|
|
||||||
name: mappedKeys[delta.key].value[itemId]?.name || mappedKeys[delta.key].value[itemId]?.title,
|
|
||||||
modified: !revision.base[delta.key].includes(itemId),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta.key === 'socials') {
|
|
||||||
// new socials don't have IDs yet, so we need to compare the values
|
|
||||||
return {
|
|
||||||
...delta,
|
|
||||||
value: delta.value.map((social) => ({
|
|
||||||
...social,
|
|
||||||
modified: !revision.base[delta.key].some((baseItem) => baseItem.url === social.url || `${baseItem.platform}:${baseItem.handle}` === `${social.platform}:${social.handle}`),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateKeys.includes(delta.key) && delta.value) {
|
|
||||||
return {
|
|
||||||
...delta,
|
|
||||||
value: new Date(delta.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curatedKeys[delta.key]) {
|
|
||||||
return {
|
|
||||||
...delta,
|
|
||||||
value: curatedKeys[delta.key](delta.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return delta;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...revision,
|
|
||||||
base: curatedBase,
|
|
||||||
deltas: curatedDeltas,
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
async function reloadRevisions() {
|
|
||||||
const updatedRevisions = await get(`/revisions/${domain}`, {
|
|
||||||
isFinalized: showReviewed.value ? undefined : false,
|
|
||||||
limit: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
actors.value = updatedRevisions.actors;
|
|
||||||
tags.value = updatedRevisions.tags;
|
|
||||||
movies.value = updatedRevisions.movies;
|
|
||||||
avatars.value = updatedRevisions.avatars;
|
|
||||||
revisions.value = updatedRevisions.revisions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reviewRevision(revision, isApproved) {
|
|
||||||
reviewedRevisions.value.add(revision.id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
|
||||||
isApproved,
|
|
||||||
feedback: feedbacks.value[revision.id],
|
|
||||||
}, {
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
|
||||||
revisionId: revision.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
revisions.value = revisions.value.map((rev) => (rev.id === updatedRevision.revision.id ? updatedRevision.revision : rev));
|
|
||||||
} catch (error) {
|
|
||||||
reviewedRevisions.value.delete(revision.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function banEditor(revision) {
|
|
||||||
await post('/bans', {
|
|
||||||
userId: revision.user.id,
|
|
||||||
banIp: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await reviewRevision(revision, false);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import formatTemplate from 'template-format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
useId,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['socials']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { env } = pageContext;
|
||||||
|
|
||||||
|
const socials = ref(props.edits.socials);
|
||||||
|
|
||||||
|
const platform = ref('');
|
||||||
|
const handle = ref('');
|
||||||
|
const url = ref('');
|
||||||
|
const addSocialDropdownId = useId();
|
||||||
|
|
||||||
|
watch(socials, () => emit('socials', socials.value));
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
twitter: 'twitter-x',
|
||||||
|
};
|
||||||
|
|
||||||
|
function addSocial() {
|
||||||
|
if (!handle.value && !url.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.value && !platform.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socials.value = socials.value.concat({
|
||||||
|
platform: platform.value || null,
|
||||||
|
handle: handle.value || null,
|
||||||
|
url: url.value || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('socials', socials.value);
|
||||||
|
|
||||||
|
platform.value = '';
|
||||||
|
handle.value = '';
|
||||||
|
url.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrl(social) {
|
||||||
|
if (social.url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.socials.urls[social.platform]) {
|
||||||
|
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
class="list nolist"
|
class="list nolist"
|
||||||
@@ -54,7 +126,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="list-new">
|
<li class="list-new">
|
||||||
<VDropdown>
|
<VDropdown :aria-id="addSocialDropdownId">
|
||||||
<Icon
|
<Icon
|
||||||
icon="plus2"
|
icon="plus2"
|
||||||
class="add noselect"
|
class="add noselect"
|
||||||
@@ -110,77 +182,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
inject,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import formatTemplate from 'template-format';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { env } = pageContext;
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['socials']);
|
|
||||||
const socials = ref(props.edits.socials);
|
|
||||||
|
|
||||||
const platform = ref('');
|
|
||||||
const handle = ref('');
|
|
||||||
const url = ref('');
|
|
||||||
|
|
||||||
watch(socials, () => emit('socials', socials));
|
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
twitter: 'twitter-x',
|
|
||||||
};
|
|
||||||
|
|
||||||
function addSocial() {
|
|
||||||
if (!handle.value && !url.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handle.value && !platform.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socials.value = socials.value.concat({
|
|
||||||
platform: platform.value || null,
|
|
||||||
handle: handle.value || null,
|
|
||||||
url: url.value || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('socials', socials.value);
|
|
||||||
|
|
||||||
platform.value = '';
|
|
||||||
handle.value = '';
|
|
||||||
url.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrl(social) {
|
|
||||||
if (social.url) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.socials.urls[social.platform]) {
|
|
||||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import TagSearch from '#/components/tags/search.vue';
|
||||||
|
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['tags']);
|
||||||
|
|
||||||
|
const newTags = ref([]);
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
{
|
||||||
|
tags: props.item.value.filter((tag) => tag.actorId === null),
|
||||||
|
actor: null,
|
||||||
|
actorId: null,
|
||||||
|
},
|
||||||
|
...props.scene.actors.map((actor) => ({
|
||||||
|
tags: props.item.value.filter((tag) => tag.actorId === actor.id),
|
||||||
|
actor,
|
||||||
|
actorId: actor?.id || null,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
function addTag(newTag, actor) {
|
||||||
|
const actorId = actor?.id || null;
|
||||||
|
|
||||||
|
if (props.edits.tags.some((sceneTag) => sceneTag.id === newTag.id && sceneTag.actorId === actorId)) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: 'Tag already added',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTagWithActorId = {
|
||||||
|
...newTag,
|
||||||
|
actorId,
|
||||||
|
};
|
||||||
|
|
||||||
|
newTags.value = newTags.value.concat(newTagWithActorId);
|
||||||
|
emit('tags', props.edits.tags.concat(newTagWithActorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.scene, () => {
|
||||||
|
newTags.value = [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tags-sections">
|
<div class="tags-sections">
|
||||||
<div
|
<div
|
||||||
@@ -53,73 +122,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
|
|
||||||
import TagSearch from '#/components/tags/search.vue';
|
|
||||||
|
|
||||||
const newTags = ref([]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
scene: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
edits: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
type: Set,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['tags']);
|
|
||||||
|
|
||||||
const tags = [
|
|
||||||
{
|
|
||||||
tags: props.item.value.filter((tag) => tag.actorId === null),
|
|
||||||
actor: null,
|
|
||||||
actorId: null,
|
|
||||||
},
|
|
||||||
...props.scene.actors.map((actor) => ({
|
|
||||||
tags: props.item.value.filter((tag) => tag.actorId === actor.id),
|
|
||||||
actor,
|
|
||||||
actorId: actor?.id || null,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
function addTag(newTag, actor) {
|
|
||||||
const actorId = actor?.id || null;
|
|
||||||
|
|
||||||
if (props.edits.tags.some((sceneTag) => sceneTag.id === newTag.id && sceneTag.actorId === actorId)) {
|
|
||||||
events.emit('feedback', {
|
|
||||||
type: 'error',
|
|
||||||
message: 'Tag already added',
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTagWithActorId = {
|
|
||||||
...newTag,
|
|
||||||
actorId,
|
|
||||||
};
|
|
||||||
|
|
||||||
newTags.value = newTags.value.concat(newTagWithActorId);
|
|
||||||
emit('tags', props.edits.tags.concat(newTagWithActorId));
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.scene, () => { newTags.value = []; });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
entity: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showNetworkSymbol: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
:href="`/${entity.type}/${entity.slug}`"
|
:href="`/${entity.type}/${entity.slug}`"
|
||||||
@@ -22,19 +35,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
entity: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showNetworkSymbol: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.entity {
|
.entity {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
|
|||||||
@@ -1,3 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
import Gender from '#/components/actors/gender.vue';
|
||||||
|
|
||||||
|
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
actor: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'available',
|
||||||
|
},
|
||||||
|
toggleActor: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['actor']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
:key="`filter-actor-${actor.id}`"
|
:key="`filter-actor-${actor.id}`"
|
||||||
@@ -44,37 +75,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
|
||||||
|
|
||||||
import Gender from '#/components/actors/gender.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
actor: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'available',
|
|
||||||
},
|
|
||||||
toggleActor: {
|
|
||||||
type: Function,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['actor']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-name {
|
.filter-name {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -1,3 +1,79 @@
|
|||||||
|
<script setup>
|
||||||
|
import { UseVirtualList } from '@vueuse/components';
|
||||||
|
import { computed, inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import Gender from '#/components/actors/gender.vue';
|
||||||
|
import Actor from '#/components/filters/actor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||||
|
const selectedGender = ref(null);
|
||||||
|
const order = ref('count');
|
||||||
|
const genderTooltipId = useId();
|
||||||
|
const sortAlphaTooltipId = useId();
|
||||||
|
const sortCountTooltipId = useId();
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const pageProps = pageContext.pageProps;
|
||||||
|
const { actor: pageActor } = pageProps;
|
||||||
|
|
||||||
|
const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean));
|
||||||
|
|
||||||
|
const availableActors = computed(() => props.actors
|
||||||
|
.filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id)
|
||||||
|
&& actor.id !== pageActor?.id
|
||||||
|
&& searchRegexp.value.test(actor.name)
|
||||||
|
&& (!selectedGender.value || actor.gender === selectedGender.value))
|
||||||
|
.sort((actorA, actorB) => {
|
||||||
|
if (order.value === 'count') {
|
||||||
|
return actorB.count - actorA.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actorA.name.localeCompare(actorB.name);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
|
||||||
|
const isAggActorsLimited = computed(() => props.actors.length >= pageContext.env.maxAggregateSize);
|
||||||
|
|
||||||
|
function toggleActor(actor, combine) {
|
||||||
|
if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) {
|
||||||
|
emit('update', 'actors', props.filters.actors.filter((filterActor) => filterActor.id !== actor.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combine) {
|
||||||
|
emit('update', 'actors', props.filters.actors.concat(actor));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit('update', 'actors', [actor]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectGender() {
|
||||||
|
const genderIndex = genders.value.indexOf(selectedGender.value);
|
||||||
|
|
||||||
|
if (genderIndex >= genders.value.length - 1) {
|
||||||
|
selectedGender.value = genders.value[0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGender.value = genders.value[genderIndex + 1];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter actors-container">
|
<div class="filter actors-container">
|
||||||
<div
|
<div
|
||||||
@@ -18,6 +94,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
v-tooltip="{ content: selectedGender ? `${selectedGender.slice(0, 1).toUpperCase()}${selectedGender.slice(1)} performers` : `All genders`, ariaId: genderTooltipId }"
|
||||||
class="filter-sort noselect"
|
class="filter-sort noselect"
|
||||||
@click="selectGender"
|
@click="selectGender"
|
||||||
>
|
>
|
||||||
@@ -35,6 +112,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'name'"
|
v-show="order === 'name'"
|
||||||
|
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'count'"
|
@click="order = 'count'"
|
||||||
>
|
>
|
||||||
@@ -45,6 +123,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'count'"
|
v-show="order === 'count'"
|
||||||
|
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'name'"
|
@click="order = 'name'"
|
||||||
>
|
>
|
||||||
@@ -96,78 +175,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
import { UseVirtualList } from '@vueuse/components';
|
|
||||||
|
|
||||||
import Actor from '#/components/filters/actor.vue';
|
|
||||||
import Gender from '#/components/actors/gender.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
actors: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
|
|
||||||
const search = ref('');
|
|
||||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
|
||||||
const selectedGender = ref(null);
|
|
||||||
const order = ref('count');
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const pageProps = pageContext.pageProps;
|
|
||||||
const { actor: pageActor } = pageProps;
|
|
||||||
|
|
||||||
const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean));
|
|
||||||
|
|
||||||
const availableActors = computed(() => props.actors
|
|
||||||
.filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id)
|
|
||||||
&& actor.id !== pageActor?.id
|
|
||||||
&& searchRegexp.value.test(actor.name)
|
|
||||||
&& (!selectedGender.value || actor.gender === selectedGender.value))
|
|
||||||
.sort((actorA, actorB) => {
|
|
||||||
if (order.value === 'count') {
|
|
||||||
return actorB.count - actorA.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return actorA.name.localeCompare(actorB.name);
|
|
||||||
}));
|
|
||||||
|
|
||||||
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
|
|
||||||
const isAggActorsLimited = computed(() => props.actors.length >= pageContext.env.maxAggregateSize);
|
|
||||||
|
|
||||||
function toggleActor(actor, combine) {
|
|
||||||
if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) {
|
|
||||||
emit('update', 'actors', props.filters.actors.filter((filterActor) => filterActor.id !== actor.id));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combine) {
|
|
||||||
emit('update', 'actors', props.filters.actors.concat(actor));
|
|
||||||
} else {
|
|
||||||
emit('update', 'actors', [actor]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectGender() {
|
|
||||||
const genderIndex = genders.value.indexOf(selectedGender.value);
|
|
||||||
|
|
||||||
if (genderIndex >= genders.value.length - 1) {
|
|
||||||
selectedGender.value = genders.value[0];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedGender.value = genders.value[genderIndex + 1];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter {
|
.filter {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format, subYears } from 'date-fns';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import RangeFilter from '#/components/filters/range.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// <select> doesn't work well with :value for some reason
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const maxDob = format(subYears(new Date(), 18), 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
const dobType = ref(props.filters.dobType);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
@@ -53,26 +75,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { format, subYears } from 'date-fns';
|
|
||||||
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
import RangeFilter from '#/components/filters/range.vue';
|
|
||||||
|
|
||||||
const maxDob = format(subYears(new Date(), 18), 'yyyy-MM-dd');
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const dobType = ref(props.filters.dobType); // <select> doesn't work well with :value for some reason
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import RangeFilter from '#/components/filters/range.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
// cup range should probably not be used, because it becomes impossible to filter for larger values than the current results
|
||||||
|
cupRange: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
function cupRangeToCups(cupA, cupB) {
|
||||||
|
const smallestCode = cupA.charCodeAt(0);
|
||||||
|
const biggestCode = cupB.charCodeAt(0);
|
||||||
|
|
||||||
|
return Array.from({ length: biggestCode - smallestCode + 1 }, (value, index) => String.fromCharCode(smallestCode + index));
|
||||||
|
}
|
||||||
|
|
||||||
|
const naturalBoobsValues = [true, undefined, false];
|
||||||
|
const cups = computed(() => (props.cupRange ? cupRangeToCups(props.cupRange[0], props.cupRange[1]) : 'ABCDEFGHIJKZ'.split(''))); // add Z to allow for 'bigger than K' without wasting fidelity
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<RangeFilter
|
<RangeFilter
|
||||||
@@ -42,33 +72,3 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import RangeFilter from '#/components/filters/range.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
// cup range should probably not be used, because it becomes impossible to filter for larger values than the current results
|
|
||||||
cupRange: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function cupRangeToCups(cupA, cupB) {
|
|
||||||
const smallestCode = cupA.charCodeAt(0);
|
|
||||||
const biggestCode = cupB.charCodeAt(0);
|
|
||||||
|
|
||||||
return Array.from({ length: biggestCode - smallestCode + 1 }, (value, index) => String.fromCharCode(smallestCode + index));
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
|
|
||||||
const naturalBoobsValues = [true, undefined, false];
|
|
||||||
const cups = computed(() => (props.cupRange ? cupRangeToCups(props.cupRange[0], props.cupRange[1]) : 'ABCDEFGHIJKZ'.split(''))); // add Z to allow for 'bigger than K' without wasting fidelity
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||||
|
const order = ref('name');
|
||||||
|
const sortAlphaTooltipId = useId();
|
||||||
|
const sortCountTooltipId = useId();
|
||||||
|
|
||||||
|
const { pageProps } = inject('pageContext');
|
||||||
|
const { channel: pageChannel } = pageProps;
|
||||||
|
|
||||||
|
function sort(channelA, channelB) {
|
||||||
|
if (order.value === 'count') {
|
||||||
|
return channelB.count - channelA.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelA.name.localeCompare(channelB.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = computed(() => {
|
||||||
|
const filteredChannels = props.channels.filter((channel) => channel.id !== pageChannel?.id
|
||||||
|
&& (searchRegexp.value.test(channel.name)
|
||||||
|
|| searchRegexp.value.test(channel.slug)
|
||||||
|
|| (channel.parent && searchRegexp.value.test(channel.parent.name))
|
||||||
|
|| (channel.parent && searchRegexp.value.test(channel.parent.slug))));
|
||||||
|
|
||||||
|
const networks = Object.values(filteredChannels.reduce((acc, channel) => {
|
||||||
|
if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child
|
||||||
|
acc[channel.id] = {
|
||||||
|
...channel,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acc[channel.id] && channel.parent && !acc[channel.parent.id] && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||||
|
acc[channel.parent.id] = {
|
||||||
|
...channel.parent,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acc[channel.id] && channel.parent && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||||
|
acc[channel.parent.id].children.push(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}))
|
||||||
|
.map((network) => ({
|
||||||
|
...network,
|
||||||
|
children: network.children?.sort(sort),
|
||||||
|
count: network.count || network.children?.reduce((acc, channel) => acc + channel.count, 0),
|
||||||
|
}))
|
||||||
|
.sort(sort)
|
||||||
|
.flatMap((network) => [network, ...(network.children || [])]);
|
||||||
|
|
||||||
|
return networks;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter channels-container">
|
<div class="filter channels-container">
|
||||||
<div class="filters-sort">
|
<div class="filters-sort">
|
||||||
@@ -14,6 +91,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'name'"
|
v-show="order === 'name'"
|
||||||
|
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'count'"
|
@click="order = 'count'"
|
||||||
>
|
>
|
||||||
@@ -22,6 +100,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'count'"
|
v-show="order === 'count'"
|
||||||
|
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'name'"
|
@click="order = 'name'"
|
||||||
>
|
>
|
||||||
@@ -75,7 +154,7 @@
|
|||||||
v-if="filters.entity?.id === entity.id"
|
v-if="filters.entity?.id === entity.id"
|
||||||
icon="cross2"
|
icon="cross2"
|
||||||
class="filter-remove"
|
class="filter-remove"
|
||||||
@click.native.stop="emit('update', 'entity', null)"
|
@click.stop="emit('update', 'entity', null)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -85,81 +164,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
channels: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
|
|
||||||
const search = ref('');
|
|
||||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
|
||||||
const order = ref('name');
|
|
||||||
|
|
||||||
const { pageProps } = inject('pageContext');
|
|
||||||
const { channel: pageChannel } = pageProps;
|
|
||||||
|
|
||||||
function sort(channelA, channelB) {
|
|
||||||
if (order.value === 'count') {
|
|
||||||
return channelB.count - channelA.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return channelA.name.localeCompare(channelB.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entities = computed(() => {
|
|
||||||
const filteredChannels = props.channels.filter((channel) => channel.id !== pageChannel?.id
|
|
||||||
&& (searchRegexp.value.test(channel.name)
|
|
||||||
|| searchRegexp.value.test(channel.slug)
|
|
||||||
|| (channel.parent && searchRegexp.value.test(channel.parent.name))
|
|
||||||
|| (channel.parent && searchRegexp.value.test(channel.parent.slug))));
|
|
||||||
|
|
||||||
const networks = Object.values(filteredChannels.reduce((acc, channel) => {
|
|
||||||
if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child
|
|
||||||
acc[channel.id] = {
|
|
||||||
...channel,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acc[channel.id] && channel.parent && !acc[channel.parent.id] && (channel.type === 'channel' || channel.type === 'studio')) {
|
|
||||||
acc[channel.parent.id] = {
|
|
||||||
...channel.parent,
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acc[channel.id] && channel.parent && (channel.type === 'channel' || channel.type === 'studio')) {
|
|
||||||
acc[channel.parent.id].children.push(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {}))
|
|
||||||
.map((network) => ({
|
|
||||||
...network,
|
|
||||||
children: network.children?.sort(sort),
|
|
||||||
count: network.count || network.children?.reduce((acc, channel) => acc + channel.count, 0),
|
|
||||||
}))
|
|
||||||
.sort(sort)
|
|
||||||
.flatMap((network) => [network, ...(network.children || [])]);
|
|
||||||
|
|
||||||
return networks;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-items {
|
.filter-items {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
countries: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
selectedCountry: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['country']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="countries nolist">
|
<ul class="countries nolist">
|
||||||
<li
|
<li
|
||||||
@@ -19,27 +34,12 @@
|
|||||||
v-if="selectedCountry === country.alpha2"
|
v-if="selectedCountry === country.alpha2"
|
||||||
icon="cross2"
|
icon="cross2"
|
||||||
class="filter-remove"
|
class="filter-remove"
|
||||||
@click.native.stop="emit('country', null)"
|
@click.stop="emit('country', null)"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
countries: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
selectedCountry: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['country']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.countries:not(:last-child) {
|
.countries:not(:last-child) {
|
||||||
border-bottom: solid 1px var(--glass-weak-30);
|
border-bottom: solid 1px var(--glass-weak-30);
|
||||||
|
|||||||
@@ -1,3 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import Countries from './countries.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
countries: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { pageProps } = pageContext;
|
||||||
|
|
||||||
|
const allCountries = computed(() => props.countries || pageProps.countries);
|
||||||
|
const countryQuery = ref('');
|
||||||
|
|
||||||
|
const topCountryAlpha2s = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
||||||
|
const topCountries = computed(() => topCountryAlpha2s.map((alpha2) => allCountries.value.find((country) => country.alpha2 === alpha2)).filter(Boolean));
|
||||||
|
const filteredCountries = computed(() => allCountries.value.filter((country) => new RegExp(countryQuery.value, 'i').test(country.name)));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="countries-container"
|
class="countries-container"
|
||||||
@@ -43,35 +72,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import Countries from './countries.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
countries: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { pageProps } = pageContext;
|
|
||||||
|
|
||||||
const allCountries = computed(() => props.countries || pageProps.countries);
|
|
||||||
const countryQuery = ref('');
|
|
||||||
|
|
||||||
const topCountryAlpha2s = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
|
||||||
const topCountries = computed(() => topCountryAlpha2s.map((alpha2) => allCountries.value.find((country) => country.alpha2 === alpha2)).filter(Boolean));
|
|
||||||
const filteredCountries = computed(() => allCountries.value.filter((country) => new RegExp(countryQuery.value, 'i').test(country.name)));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.countries-container {
|
.countries-container {
|
||||||
border-bottom: solid 1px var(--shadow-weak-30);
|
border-bottom: solid 1px var(--shadow-weak-30);
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
results: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// desktop defaults to open, compact defaults to closed
|
||||||
|
// we can't measure viewframe in SSR, so use separate toggles
|
||||||
|
const showFilters = ref(true);
|
||||||
|
const showFiltersCompact = ref(false);
|
||||||
|
|
||||||
|
function toggleFilters(state) {
|
||||||
|
showFilters.value = state ?? !showFilters.value;
|
||||||
|
showFiltersCompact.value = state ?? !showFiltersCompact.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filters-frame">
|
<div class="filters-frame">
|
||||||
<div
|
<div
|
||||||
@@ -47,31 +72,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
results: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// desktop defaults to open, compact defaults to closed
|
|
||||||
// we can't measure viewframe in SSR, so use separate toggles
|
|
||||||
const showFilters = ref(true);
|
|
||||||
const showFiltersCompact = ref(false);
|
|
||||||
|
|
||||||
function toggleFilters(state) {
|
|
||||||
showFilters.value = state ?? !showFilters.value;
|
|
||||||
showFiltersCompact.value = state ?? !showFiltersCompact.value;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.filter {
|
.filter {
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
import Gender from '#/components/actors/gender.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="filter genders nolist">
|
<ul class="filter genders nolist">
|
||||||
<li>
|
<li>
|
||||||
@@ -47,19 +60,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import Gender from '#/components/actors/gender.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.genders {
|
.genders {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
import RangeFilter from '#/components/filters/range.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<RangeFilter
|
<RangeFilter
|
||||||
@@ -31,16 +44,3 @@
|
|||||||
</RangeFilter>
|
</RangeFilter>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import RangeFilter from '#/components/filters/range.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,39 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="filter-section">
|
|
||||||
<label class="filter-label noselect">
|
|
||||||
<span class="label">
|
|
||||||
<Checkbox
|
|
||||||
:checked="!disabled"
|
|
||||||
class="checkbox"
|
|
||||||
@change="(checked) => $emit('enable', checked)"
|
|
||||||
/>{{ label }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="!disabled"
|
|
||||||
class="label-values"
|
|
||||||
>{{ value[0] }} - {{ value[1] }}<template v-if="unit"> {{ unit }}</template></span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<span class="filter-split">
|
|
||||||
<Range
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
:value="value"
|
|
||||||
:values="values"
|
|
||||||
:disabled="disabled"
|
|
||||||
:allow-enable="allowEnable"
|
|
||||||
@enable="emit('enable', true)"
|
|
||||||
@input="(range) => emit('input', range)"
|
|
||||||
@change="(range) => emit('change', range)"
|
|
||||||
>
|
|
||||||
<template #start><slot name="start" /></template>
|
|
||||||
<template #end><slot name="end" /></template>
|
|
||||||
</Range>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Checkbox from '../form/checkbox.vue';
|
import Checkbox from '../form/checkbox.vue';
|
||||||
import Range from '../form/range.vue';
|
import Range from '../form/range.vue';
|
||||||
@@ -75,3 +39,39 @@ defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['change', 'input', 'enable']);
|
const emit = defineEmits(['change', 'input', 'enable']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="filter-section">
|
||||||
|
<label class="filter-label noselect">
|
||||||
|
<span class="label">
|
||||||
|
<Checkbox
|
||||||
|
:checked="!disabled"
|
||||||
|
class="checkbox"
|
||||||
|
@change="(checked) => $emit('enable', checked)"
|
||||||
|
/>{{ label }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!disabled"
|
||||||
|
class="label-values"
|
||||||
|
>{{ value[0] }} - {{ value[1] }}<template v-if="unit"> {{ unit }}</template></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<span class="filter-split">
|
||||||
|
<Range
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:value="value"
|
||||||
|
:values="values"
|
||||||
|
:disabled="disabled"
|
||||||
|
:allow-enable="allowEnable"
|
||||||
|
@enable="emit('enable', true)"
|
||||||
|
@input="(range) => emit('input', range)"
|
||||||
|
@change="(range) => emit('change', range)"
|
||||||
|
>
|
||||||
|
<template #start><slot name="start" /></template>
|
||||||
|
<template #end><slot name="end" /></template>
|
||||||
|
</Range>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,129 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
actorTags: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const { pageProps } = inject('pageContext');
|
||||||
|
|
||||||
|
const {
|
||||||
|
tag: pageTag,
|
||||||
|
actor: pageActor,
|
||||||
|
} = pageProps;
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
|
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||||
|
const order = ref('priority');
|
||||||
|
const showActorTags = ref(!!pageActor);
|
||||||
|
const actorTagsTooltipId = useId();
|
||||||
|
const allTagsTooltipId = useId();
|
||||||
|
const sortPriorityTooltipId = useId();
|
||||||
|
const sortCountTooltipId = useId();
|
||||||
|
const sortAlphaTooltipId = useId();
|
||||||
|
|
||||||
|
const priorityTags = [
|
||||||
|
'anal',
|
||||||
|
'dp',
|
||||||
|
'threesome',
|
||||||
|
'gangbang',
|
||||||
|
'blowbang',
|
||||||
|
'orgy',
|
||||||
|
'airtight',
|
||||||
|
'dap',
|
||||||
|
'dvp',
|
||||||
|
'triple-penetration',
|
||||||
|
'tap',
|
||||||
|
'tvp',
|
||||||
|
'transsexual',
|
||||||
|
'spitroast',
|
||||||
|
'mfm',
|
||||||
|
'fmf',
|
||||||
|
'bdsm',
|
||||||
|
'deepthroat',
|
||||||
|
'blowjob',
|
||||||
|
'lesbian',
|
||||||
|
];
|
||||||
|
|
||||||
|
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor));
|
||||||
|
|
||||||
|
const groupedTags = computed(() => {
|
||||||
|
// can't show actor tags inside stash, because both require a join, and manticore currently only supports one
|
||||||
|
const tags = showActorTags.value && isActorTagsAvailable.value
|
||||||
|
? props.actorTags
|
||||||
|
: props.tags;
|
||||||
|
|
||||||
|
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
||||||
|
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
||||||
|
&& tag.id !== pageTag?.id
|
||||||
|
&& searchRegexp.value.test(tag.name));
|
||||||
|
|
||||||
|
if (order.value === 'count') {
|
||||||
|
const available = filtered.sort((tagA, tagB) => {
|
||||||
|
if (order.value === 'count') {
|
||||||
|
return tagB.count - tagA.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// page: [pageTag],
|
||||||
|
selected,
|
||||||
|
available,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.value === 'priority') {
|
||||||
|
return {
|
||||||
|
// page: [pageTag],
|
||||||
|
selected,
|
||||||
|
available: [
|
||||||
|
...priorityTags.map((priorityTag) => filtered.find((tag) => tag.slug === priorityTag)).filter(Boolean),
|
||||||
|
...filtered.filter((tag) => !priorityTags.includes(tag.slug)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// page: [pageTag],
|
||||||
|
selected,
|
||||||
|
available: filtered,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleTag(tag, combine) {
|
||||||
|
emit('update', 'onlyActorTags', showActorTags.value, false);
|
||||||
|
|
||||||
|
if (props.filters.tags.includes(tag.slug)) {
|
||||||
|
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combine) {
|
||||||
|
emit('update', 'tags', props.filters.tags.concat(tag.slug));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit('update', 'tags', [tag.slug]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter tags-container">
|
<div class="filter tags-container">
|
||||||
<div class="filters-sort">
|
<div class="filters-sort">
|
||||||
@@ -12,28 +138,29 @@
|
|||||||
<Icon icon="search" />
|
<Icon icon="search" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!--
|
<template v-if="isActorTagsAvailable">
|
||||||
<div
|
<div
|
||||||
v-show="showActorTags"
|
v-show="showActorTags"
|
||||||
v-tooltip="'Tags relevant to the selected actors'"
|
v-tooltip="{ content: 'Tags relevant to the selected actors', ariaId: actorTagsTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="showActorTags = false"
|
@click="showActorTags = false"
|
||||||
>
|
>
|
||||||
<Icon icon="user-tags" />
|
<Icon icon="user-tags" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="!showActorTags"
|
v-show="!showActorTags"
|
||||||
v-tooltip="'All tags'"
|
v-tooltip="{ content: 'All tags', ariaId: allTagsTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="showActorTags = true"
|
@click="showActorTags = true"
|
||||||
>
|
>
|
||||||
<Icon icon="price-tags" />
|
<Icon icon="price-tags" />
|
||||||
</div>
|
</div>
|
||||||
-->
|
</template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'priority'"
|
v-show="order === 'priority'"
|
||||||
|
v-tooltip="{ content: 'Sorted by tag priority', ariaId: sortPriorityTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'count'"
|
@click="order = 'count'"
|
||||||
>
|
>
|
||||||
@@ -42,6 +169,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'count'"
|
v-show="order === 'count'"
|
||||||
|
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'name'"
|
@click="order = 'name'"
|
||||||
>
|
>
|
||||||
@@ -52,6 +180,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="order === 'name'"
|
v-show="order === 'name'"
|
||||||
|
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||||
class="filter-sort order noselect"
|
class="filter-sort order noselect"
|
||||||
@click="order = 'priority'"
|
@click="order = 'priority'"
|
||||||
>
|
>
|
||||||
@@ -119,121 +248,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
filters: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
actorTags: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
|
|
||||||
const { pageProps } = inject('pageContext');
|
|
||||||
// const { tag: pageTag, actor: pageActor } = pageProps;
|
|
||||||
const { tag: pageTag } = pageProps;
|
|
||||||
|
|
||||||
const search = ref('');
|
|
||||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
|
||||||
const order = ref('priority');
|
|
||||||
// const showActorTags = ref(true);
|
|
||||||
|
|
||||||
const priorityTags = [
|
|
||||||
'anal',
|
|
||||||
'dp',
|
|
||||||
'threesome',
|
|
||||||
'gangbang',
|
|
||||||
'blowbang',
|
|
||||||
'orgy',
|
|
||||||
'airtight',
|
|
||||||
'dap',
|
|
||||||
'dvp',
|
|
||||||
'triple-penetration',
|
|
||||||
'tap',
|
|
||||||
'tvp',
|
|
||||||
'transsexual',
|
|
||||||
'spitroast',
|
|
||||||
'mfm',
|
|
||||||
'fmf',
|
|
||||||
'bdsm',
|
|
||||||
'deepthroat',
|
|
||||||
'blowjob',
|
|
||||||
'lesbian',
|
|
||||||
];
|
|
||||||
|
|
||||||
const groupedTags = computed(() => {
|
|
||||||
/*
|
|
||||||
const tags = showActorTags.value && props.actorTags && (props.filters.actors.length > 0 || pageActor)
|
|
||||||
? props.actorTags
|
|
||||||
: props.tags;
|
|
||||||
*/
|
|
||||||
const tags = props.tags;
|
|
||||||
|
|
||||||
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
|
||||||
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
|
||||||
&& tag.id !== pageTag?.id
|
|
||||||
&& searchRegexp.value.test(tag.name));
|
|
||||||
|
|
||||||
if (order.value === 'count') {
|
|
||||||
const available = filtered.sort((tagA, tagB) => {
|
|
||||||
if (order.value === 'count') {
|
|
||||||
return tagB.count - tagA.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
// page: [pageTag],
|
|
||||||
selected,
|
|
||||||
available,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order.value === 'priority') {
|
|
||||||
return {
|
|
||||||
// page: [pageTag],
|
|
||||||
selected,
|
|
||||||
available: [
|
|
||||||
...priorityTags.map((priorityTag) => filtered.find((tag) => tag.slug === priorityTag)).filter(Boolean),
|
|
||||||
...filtered.filter((tag) => !priorityTags.includes(tag.slug)),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// page: [pageTag],
|
|
||||||
selected,
|
|
||||||
available: filtered,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleTag(tag, combine) {
|
|
||||||
if (props.filters.tags.includes(tag.slug)) {
|
|
||||||
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combine) {
|
|
||||||
emit('update', 'tags', props.filters.tags.concat(tag.slug));
|
|
||||||
} else {
|
|
||||||
emit('update', 'tags', [tag.slug]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-items {
|
.filter-items {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['toggle']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="filters-toggle open"
|
class="filters-toggle open"
|
||||||
@@ -5,10 +9,6 @@
|
|||||||
><Icon icon="filter" /></div>
|
><Icon icon="filter" /></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const emit = defineEmits(['toggle']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filters-toggle {
|
.filters-toggle {
|
||||||
min-width: 2.75rem;
|
min-width: 2.75rem;
|
||||||
|
|||||||
@@ -1,28 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="years-container">
|
|
||||||
<select
|
|
||||||
v-model="selected"
|
|
||||||
class="years nobar"
|
|
||||||
multiple
|
|
||||||
@change="updateYears"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="year in years"
|
|
||||||
:key="`year-${year.year}`"
|
|
||||||
class="year"
|
|
||||||
:value="year.year"
|
|
||||||
>{{ year.year }}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
v-show="selected.length > 0"
|
|
||||||
icon="cross2"
|
|
||||||
class="clear"
|
|
||||||
@click="clearYears"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
@@ -50,6 +25,31 @@ function clearYears() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="years-container">
|
||||||
|
<select
|
||||||
|
v-model="selected"
|
||||||
|
class="years nobar"
|
||||||
|
multiple
|
||||||
|
@change="updateYears"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="year in years"
|
||||||
|
:key="`year-${year.year}`"
|
||||||
|
class="year"
|
||||||
|
:value="year.year"
|
||||||
|
>{{ year.year }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-show="selected.length > 0"
|
||||||
|
icon="cross2"
|
||||||
|
class="clear"
|
||||||
|
@click="clearYears"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.years-container {
|
.years-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { env } = pageContext;
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<span class="footer-segment">© traxxx</span>
|
<span class="footer-segment">© traxxx</span>
|
||||||
@@ -27,13 +34,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { env } = pageContext;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['sidebar']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="nav-list noselect">
|
<nav class="nav-list noselect">
|
||||||
<a
|
<a
|
||||||
@@ -37,15 +46,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['sidebar']);
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.nav-list {
|
.nav-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
checked: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['change']);
|
||||||
|
|
||||||
|
const uid = String(Math.random()).slice(2);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label class="check-container noselect">
|
<label class="check-container noselect">
|
||||||
<span
|
<span
|
||||||
@@ -22,27 +43,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const uid = String(Math.random()).slice(2);
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
checked: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(['change']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.check-container {
|
.check-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,61 +1,8 @@
|
|||||||
<template>
|
|
||||||
<div class="range-container">
|
|
||||||
<div
|
|
||||||
class="label label-start"
|
|
||||||
:class="{ disabled }"
|
|
||||||
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
|
|
||||||
>
|
|
||||||
<slot name="start" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="range"
|
|
||||||
:class="{ disabled }"
|
|
||||||
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
|
|
||||||
@click="setNearest"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model.number="range.a"
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
:data-value="range.a"
|
|
||||||
:disabled="disabled"
|
|
||||||
type="range"
|
|
||||||
class="slider"
|
|
||||||
@input="set('input')"
|
|
||||||
@change="set('change')"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
|
|
||||||
<input
|
|
||||||
v-model.number="range.b"
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
:data-value="range.b"
|
|
||||||
:disabled="disabled"
|
|
||||||
type="range"
|
|
||||||
class="slider"
|
|
||||||
@input="set('input')"
|
|
||||||
@change="set('change')"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="label label-end"
|
|
||||||
:class="{ disabled }"
|
|
||||||
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
|
|
||||||
>
|
|
||||||
<slot name="end" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ref,
|
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -138,6 +85,59 @@ async function setRange(prop, value) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="range-container">
|
||||||
|
<div
|
||||||
|
class="label label-start"
|
||||||
|
:class="{ disabled }"
|
||||||
|
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
|
||||||
|
>
|
||||||
|
<slot name="start" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="range"
|
||||||
|
:class="{ disabled }"
|
||||||
|
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
|
||||||
|
@click="setNearest"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model.number="range.a"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:data-value="range.a"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="range"
|
||||||
|
class="slider"
|
||||||
|
@input="set('input')"
|
||||||
|
@change="set('change')"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model.number="range.b"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:data-value="range.b"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="range"
|
||||||
|
class="slider"
|
||||||
|
@input="set('input')"
|
||||||
|
@change="set('change')"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="label label-end"
|
||||||
|
:class="{ disabled }"
|
||||||
|
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
|
||||||
|
>
|
||||||
|
<slot name="end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dark .range-container .range {
|
.dark .range-container .range {
|
||||||
--slider-range: var(--lighten-weak-10);
|
--slider-range: var(--lighten-weak-10);
|
||||||
|
|||||||
@@ -1,26 +1,3 @@
|
|||||||
<template>
|
|
||||||
<label
|
|
||||||
class="toggle-container noselect"
|
|
||||||
:class="{ light: $store.state.ui.theme === 'dark' }"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:id="`toggle-${id}`"
|
|
||||||
:checked="checked"
|
|
||||||
:true-value="trueValue"
|
|
||||||
:false-value="falseValue"
|
|
||||||
:disabled="disabled"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle-input"
|
|
||||||
@change="$emit('change', $event.target.checked)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<label
|
|
||||||
:for="`toggle-${id}`"
|
|
||||||
class="toggle"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -44,12 +21,35 @@ export default {
|
|||||||
emits: ['change'],
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
id: Math.floor(new Date().getTime() * Math.random()),
|
id: Math.floor(Date.now() * Math.random()),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label
|
||||||
|
class="toggle-container noselect"
|
||||||
|
:class="{ light: $store.state.ui.theme === 'dark' }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`toggle-${id}`"
|
||||||
|
:checked="checked"
|
||||||
|
:true-value="trueValue"
|
||||||
|
:false-value="falseValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle-input"
|
||||||
|
@change="$emit('change', $event.target.checked)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label
|
||||||
|
:for="`toggle-${id}`"
|
||||||
|
class="toggle"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import 'breakpoints';
|
@import 'breakpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
useId,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import AlertDialog from '#/components/alerts/create.vue';
|
||||||
|
import Notifications from '#/components/header/notifications.vue';
|
||||||
|
import Settings from '#/components/settings/settings.vue';
|
||||||
|
// import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
import { del } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
|
import logo from '../../assets/img/logo.svg?raw';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const theme = ref(pageContext.env.theme);
|
||||||
|
|
||||||
|
const user = pageContext.user;
|
||||||
|
const primaryStash = pageContext.assets?.primaryStash;
|
||||||
|
const unseen = ref(pageContext.assets?.unseenNotifications);
|
||||||
|
const query = ref(pageContext.urlParsed.search.q || '');
|
||||||
|
const allowLogin = pageContext.env.allowLogin;
|
||||||
|
const searchFocused = ref(false);
|
||||||
|
const showSettings = ref(false);
|
||||||
|
const showAlertDialog = ref(false);
|
||||||
|
const notifsDropdownId = useId();
|
||||||
|
const menuDropdownId = useId();
|
||||||
|
|
||||||
|
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||||
|
const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`;
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
navigate('/search', { q: query.value }, { redirect: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(newTheme) {
|
||||||
|
theme.value = newTheme;
|
||||||
|
|
||||||
|
Cookies.set('theme', newTheme);
|
||||||
|
events.emit('theme', newTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await del('/session');
|
||||||
|
navigate('/login?consent', null, { redirect: true }); // pass consent variable to reinstate in new session
|
||||||
|
}
|
||||||
|
|
||||||
|
function blurSearch(event) {
|
||||||
|
if (!event.relatedTarget || !Object.hasOwn(event.relatedTarget?.dataset, 'search')) {
|
||||||
|
searchFocused.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
@@ -92,6 +153,7 @@
|
|||||||
>
|
>
|
||||||
<VDropdown
|
<VDropdown
|
||||||
v-if="user"
|
v-if="user"
|
||||||
|
:aria-id="notifsDropdownId"
|
||||||
:triggers="['click']"
|
:triggers="['click']"
|
||||||
:prevent-overflow="true"
|
:prevent-overflow="true"
|
||||||
class="notifs-trigger"
|
class="notifs-trigger"
|
||||||
@@ -121,6 +183,7 @@
|
|||||||
</VDropdown>
|
</VDropdown>
|
||||||
|
|
||||||
<VDropdown
|
<VDropdown
|
||||||
|
:aria-id="menuDropdownId"
|
||||||
:triggers="['click']"
|
:triggers="['click']"
|
||||||
:prevent-overflow="true"
|
:prevent-overflow="true"
|
||||||
class="menu-trigger"
|
class="menu-trigger"
|
||||||
@@ -187,6 +250,32 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
v-if="user"
|
||||||
|
class="menu-item"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="`/user/${user.username}/stashes`"
|
||||||
|
class="menu-button nolink stashes"
|
||||||
|
>
|
||||||
|
<Icon icon="folder-heart" />
|
||||||
|
Stashes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
v-if="user"
|
||||||
|
class="menu-item"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="`/user/${user.username}/alerts`"
|
||||||
|
class="menu-button nolink alerts"
|
||||||
|
>
|
||||||
|
<Icon icon="alarm" />
|
||||||
|
Alerts
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-close-popper
|
v-close-popper
|
||||||
class="menu-item menu-button"
|
class="menu-item menu-button"
|
||||||
@@ -257,64 +346,6 @@
|
|||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
computed,
|
|
||||||
inject,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
|
||||||
import { del } from '#/src/api.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
// import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
import Notifications from '#/components/header/notifications.vue';
|
|
||||||
import Settings from '#/components/settings/settings.vue';
|
|
||||||
import AlertDialog from '#/components/alerts/create.vue';
|
|
||||||
|
|
||||||
import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const theme = ref(pageContext.env.theme);
|
|
||||||
|
|
||||||
const user = pageContext.user;
|
|
||||||
const primaryStash = pageContext.assets?.primaryStash;
|
|
||||||
const unseen = ref(pageContext.assets?.unseenNotifications);
|
|
||||||
const query = ref(pageContext.urlParsed.search.q || '');
|
|
||||||
const allowLogin = pageContext.env.allowLogin;
|
|
||||||
const searchFocused = ref(false);
|
|
||||||
const showSettings = ref(false);
|
|
||||||
const showAlertDialog = ref(false);
|
|
||||||
|
|
||||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
|
||||||
const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`;
|
|
||||||
|
|
||||||
function search() {
|
|
||||||
navigate('/search', { q: query.value }, { redirect: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTheme(newTheme) {
|
|
||||||
theme.value = newTheme;
|
|
||||||
|
|
||||||
Cookies.set('theme', newTheme);
|
|
||||||
events.emit('theme', newTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await del('/session');
|
|
||||||
navigate('/login?consent', null, { redirect: true }); // pass consent variable to reinstate in new session
|
|
||||||
}
|
|
||||||
|
|
||||||
function blurSearch(event) {
|
|
||||||
if (!event.relatedTarget || !Object.hasOwn(event.relatedTarget?.dataset, 'search')) {
|
|
||||||
searchFocused.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
defineEmits,
|
||||||
|
inject,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||||
|
import { get, patch } from '#/src/api.js';
|
||||||
|
|
||||||
|
import { formatDate } from '#/utils/format.js';
|
||||||
|
|
||||||
|
const emit = defineEmits(['unseen', 'addAlert']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const user = pageContext.user;
|
||||||
|
|
||||||
|
const notifications = ref([]);
|
||||||
|
const done = ref(true);
|
||||||
|
|
||||||
|
async function fetchNotifications() {
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
const res = await get(`/users/${user?.id}/notifications`);
|
||||||
|
notifications.value = res.notifications;
|
||||||
|
|
||||||
|
done.value = true;
|
||||||
|
|
||||||
|
emit('unseen', res.unseen);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markSeen(notif) {
|
||||||
|
if (notif.isSeen || !done.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
await patch(`/users/${user?.id}/notifications/${notif.id}`, {
|
||||||
|
seen: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchNotifications();
|
||||||
|
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAllSeen() {
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
await patch(`/users/${user?.id}/notifications`, {
|
||||||
|
seen: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchNotifications();
|
||||||
|
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchNotifications();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div class="notifs-header">
|
<div class="notifs-header">
|
||||||
@@ -88,71 +152,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
defineEmits,
|
|
||||||
onMounted,
|
|
||||||
inject,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { get, patch } from '#/src/api.js';
|
|
||||||
import { formatDate } from '#/utils/format.js';
|
|
||||||
|
|
||||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const user = pageContext.user;
|
|
||||||
|
|
||||||
const notifications = ref([]);
|
|
||||||
const done = ref(true);
|
|
||||||
|
|
||||||
const emit = defineEmits(['unseen', 'addAlert']);
|
|
||||||
|
|
||||||
async function fetchNotifications() {
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
const res = await get(`/users/${user?.id}/notifications`);
|
|
||||||
notifications.value = res.notifications;
|
|
||||||
|
|
||||||
done.value = true;
|
|
||||||
|
|
||||||
emit('unseen', res.unseen);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markSeen(notif) {
|
|
||||||
if (notif.isSeen || !done.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
await patch(`/users/${user?.id}/notifications/${notif.id}`, {
|
|
||||||
seen: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fetchNotifications();
|
|
||||||
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markAllSeen() {
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
await patch(`/users/${user?.id}/notifications`, {
|
|
||||||
seen: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fetchNotifications();
|
|
||||||
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchNotifications();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notifs {
|
.notifs {
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="{ active }"
|
|
||||||
:title="title"
|
|
||||||
class="icon"
|
|
||||||
v-html="svg"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const svg = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -27,11 +16,22 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const svg = ref(null);
|
||||||
|
|
||||||
import(`../../assets/img/icons/${props.icon}.svg?raw`).then((raw) => {
|
import(`../../assets/img/icons/${props.icon}.svg?raw`).then((raw) => {
|
||||||
svg.value = raw.default;
|
svg.value = raw.default;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ active }"
|
||||||
|
:title="title"
|
||||||
|
class="icon"
|
||||||
|
v-html="svg"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.icon {
|
.icon {
|
||||||
fill: var(--text);
|
fill: var(--text);
|
||||||
|
|||||||
@@ -1,12 +1,3 @@
|
|||||||
<template>
|
|
||||||
<a
|
|
||||||
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
@@ -19,3 +10,12 @@ defineProps({
|
|||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import Player from '#/components/video/player.vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
release: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const playing = ref(false);
|
||||||
|
const paused = ref(false);
|
||||||
|
|
||||||
|
const poster = computed(() => {
|
||||||
|
if (props.release.poster) {
|
||||||
|
return getPath(props.release.poster, 'thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.release.covers?.length > 0) {
|
||||||
|
return getPath(props.release.covers[0], 'thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.release.photos?.length > 0) {
|
||||||
|
return getPath(props.release.photos[0], 'thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
||||||
|
const restrictedTooltipId = useId();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="banner-container"
|
class="banner-container"
|
||||||
@@ -49,7 +86,7 @@
|
|||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-if="(release.trailer || release.teaser).isRestricted"
|
v-if="(release.trailer || release.teaser).isRestricted"
|
||||||
v-tooltip="'Restricted video'"
|
v-tooltip="{ content: 'Restricted video', ariaId: restrictedTooltipId }"
|
||||||
icon="blocked"
|
icon="blocked"
|
||||||
class="restricted"
|
class="restricted"
|
||||||
/>
|
/>
|
||||||
@@ -102,42 +139,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
import Player from '#/components/video/player.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
release: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const playing = ref(false);
|
|
||||||
const paused = ref(false);
|
|
||||||
|
|
||||||
const poster = computed(() => {
|
|
||||||
if (props.release.poster) {
|
|
||||||
return getPath(props.release.poster, 'thumbnail');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.release.covers?.length > 0) {
|
|
||||||
return getPath(props.release.covers[0], 'thumbnail');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.release.photos?.length > 0) {
|
|
||||||
return getPath(props.release.photos[0], 'thumbnail');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.banner-container {
|
.banner-container {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|||||||
@@ -1,115 +1,20 @@
|
|||||||
<template>
|
|
||||||
<div class="movies-page">
|
|
||||||
<Filters
|
|
||||||
v-if="showFilters"
|
|
||||||
:class="{ loading }"
|
|
||||||
>
|
|
||||||
<div class="filter">
|
|
||||||
<input
|
|
||||||
v-model="filters.search"
|
|
||||||
type="search"
|
|
||||||
placeholder="Search movies"
|
|
||||||
class="search input"
|
|
||||||
@search="search"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<YearsFilter
|
|
||||||
:filters="filters"
|
|
||||||
:years="aggYears"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagsFilter
|
|
||||||
:filters="filters"
|
|
||||||
:tags="aggTags"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChannelsFilter
|
|
||||||
:filters="filters"
|
|
||||||
:channels="aggChannels"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActorsFilter
|
|
||||||
:filters="filters"
|
|
||||||
:actors="aggActors"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="filter">
|
|
||||||
<Checkbox
|
|
||||||
:checked="filters.requireCover"
|
|
||||||
label="Require box cover"
|
|
||||||
@change="(checked) => updateFilter('requireCover', checked, true)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Filters>
|
|
||||||
|
|
||||||
<div class="movies-container">
|
|
||||||
<div class="movies-header">
|
|
||||||
<div class="meta">{{ total }} results</div>
|
|
||||||
|
|
||||||
<select
|
|
||||||
v-model="scope"
|
|
||||||
class="input"
|
|
||||||
@change="search({ autoScope: false })"
|
|
||||||
>
|
|
||||||
<!-- not selected in SSR without prop -->
|
|
||||||
<option
|
|
||||||
v-if="pageStash"
|
|
||||||
:selected="scope === 'stashed'"
|
|
||||||
value="stashed"
|
|
||||||
>Added</option>
|
|
||||||
|
|
||||||
<option
|
|
||||||
v-if="filters.search"
|
|
||||||
:selected="scope === 'results'"
|
|
||||||
value="results"
|
|
||||||
>Relevance</option>
|
|
||||||
|
|
||||||
<option value="likes">Popular</option>
|
|
||||||
<option value="latest">Latest</option>
|
|
||||||
<option value="upcoming">Upcoming</option>
|
|
||||||
<option value="new">New</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="movies nolist">
|
|
||||||
<li
|
|
||||||
v-for="movie in movies"
|
|
||||||
:key="`movie-${movie.id}`"
|
|
||||||
>
|
|
||||||
<MovieTile :movie="movie" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
:total="total"
|
|
||||||
:page="currentPage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
import { parse } from 'path-to-regexp';
|
import { parse } from 'path-to-regexp';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
|
|
||||||
import MovieTile from '#/components/movies/tile.vue';
|
|
||||||
import Filters from '#/components/filters/filters.vue';
|
|
||||||
import YearsFilter from '#/components/filters/years.vue';
|
|
||||||
import ActorsFilter from '#/components/filters/actors.vue';
|
import ActorsFilter from '#/components/filters/actors.vue';
|
||||||
import TagsFilter from '#/components/filters/tags.vue';
|
|
||||||
import ChannelsFilter from '#/components/filters/channels.vue';
|
import ChannelsFilter from '#/components/filters/channels.vue';
|
||||||
|
import Filters from '#/components/filters/filters.vue';
|
||||||
|
import TagsFilter from '#/components/filters/tags.vue';
|
||||||
|
|
||||||
|
import YearsFilter from '#/components/filters/years.vue';
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
import MovieTile from '#/components/movies/tile.vue';
|
||||||
import Pagination from '#/components/pagination/pagination.vue';
|
import Pagination from '#/components/pagination/pagination.vue';
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
import navigate from '#/src/navigate.js';
|
||||||
|
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const { pageProps, routeParams, urlParsed } = pageContext;
|
const { pageProps, routeParams, urlParsed } = pageContext;
|
||||||
@@ -236,6 +141,115 @@ function updateFilter(prop, value, reload = true) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="movies-page">
|
||||||
|
<Filters
|
||||||
|
v-if="showFilters"
|
||||||
|
:class="{ loading }"
|
||||||
|
>
|
||||||
|
<div class="filter search-container">
|
||||||
|
<!-- onsearch not compatible with FF and Safari -->
|
||||||
|
<input
|
||||||
|
v-model="filters.search"
|
||||||
|
type="input"
|
||||||
|
placeholder="Search movies"
|
||||||
|
class="search input"
|
||||||
|
@keydown.enter="search"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="filters.search"
|
||||||
|
icon="cross2"
|
||||||
|
class="search-button"
|
||||||
|
@click="filters.search = ''; search()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon="search"
|
||||||
|
class="search-button"
|
||||||
|
@click="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<YearsFilter
|
||||||
|
:filters="filters"
|
||||||
|
:years="aggYears"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagsFilter
|
||||||
|
:filters="filters"
|
||||||
|
:tags="aggTags"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChannelsFilter
|
||||||
|
:filters="filters"
|
||||||
|
:channels="aggChannels"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActorsFilter
|
||||||
|
:filters="filters"
|
||||||
|
:actors="aggActors"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="filter">
|
||||||
|
<Checkbox
|
||||||
|
:checked="filters.requireCover"
|
||||||
|
label="Require box cover"
|
||||||
|
@change="(checked) => updateFilter('requireCover', checked, true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Filters>
|
||||||
|
|
||||||
|
<div class="movies-container">
|
||||||
|
<div class="movies-header">
|
||||||
|
<div class="meta">{{ total }} results</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="scope"
|
||||||
|
class="input"
|
||||||
|
@change="search({ autoScope: false })"
|
||||||
|
>
|
||||||
|
<!-- not selected in SSR without prop -->
|
||||||
|
<option
|
||||||
|
v-if="pageStash"
|
||||||
|
:selected="scope === 'stashed'"
|
||||||
|
value="stashed"
|
||||||
|
>Added</option>
|
||||||
|
|
||||||
|
<option
|
||||||
|
v-if="filters.search"
|
||||||
|
:selected="scope === 'results'"
|
||||||
|
value="results"
|
||||||
|
>Relevance</option>
|
||||||
|
|
||||||
|
<option value="likes">Popular</option>
|
||||||
|
<option value="latest">Latest</option>
|
||||||
|
<option value="upcoming">Upcoming</option>
|
||||||
|
<option value="new">New</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="movies nolist">
|
||||||
|
<li
|
||||||
|
v-for="movie in movies"
|
||||||
|
:key="`movie-${movie.id}`"
|
||||||
|
>
|
||||||
|
<MovieTile :movie="movie" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
:page="currentPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.movies-page {
|
.movies-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['movie']);
|
||||||
|
const dropdownId = useId();
|
||||||
|
const movies = ref([]);
|
||||||
|
const query = ref(null);
|
||||||
|
const queryInput = ref(null);
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
const data = await get('/movies', { q: query.value });
|
||||||
|
|
||||||
|
movies.value = data.movies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryInput.value?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDropdown
|
<VDropdown
|
||||||
|
:aria-id="dropdownId"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
@show="focus"
|
@show="focus"
|
||||||
@@ -36,39 +69,6 @@
|
|||||||
</VDropdown>
|
</VDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
const movies = ref([]);
|
|
||||||
const query = ref(null);
|
|
||||||
const queryInput = ref(null);
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['movie']);
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
const data = await get('/movies', { q: query.value });
|
|
||||||
|
|
||||||
movies.value = data.movies;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
setTimeout(() => {
|
|
||||||
queryInput.value?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.trigger {
|
.trigger {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
movie: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showDetails: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const user = pageContext.user;
|
||||||
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
const favorited = ref(props.movie.stashes?.some((movieStash) => movieStash.id === currentStash?.id));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="movie-tile"
|
class="movie-tile"
|
||||||
@@ -118,35 +147,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
movie: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showDetails: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const user = pageContext.user;
|
|
||||||
const pageStash = pageContext.pageProps.stash;
|
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
|
|
||||||
const favorited = ref(props.movie.stashes?.some((movieStash) => movieStash.id === currentStash?.id));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.movie-tile {
|
.movie-tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import { parse } from 'path-to-regexp';
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
import Campaign from '#/components/campaigns/campaign.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
redirect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
includeQuery: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
useMaxMatches: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['navigation']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const {
|
||||||
|
routeParams,
|
||||||
|
urlParsed,
|
||||||
|
pageProps,
|
||||||
|
env,
|
||||||
|
campaigns,
|
||||||
|
} = pageContext;
|
||||||
|
|
||||||
|
const currentPage = computed(() => props.page || Number(routeParams?.page));
|
||||||
|
|
||||||
|
const limit = computed(() => props.limit || Number(pageProps.limit) || 30);
|
||||||
|
const total = computed(() => props.total || Number(pageProps.total));
|
||||||
|
const pageTotal = computed(() => Math.ceil((props.useMaxMatches ? Math.min(total.value, env.maxMatches) : total.value) / limit.value));
|
||||||
|
|
||||||
|
const hasNextPage = computed(() => currentPage.value + 1 <= pageTotal.value);
|
||||||
|
const hasPrevPage = computed(() => currentPage.value - 1 >= 1);
|
||||||
|
|
||||||
|
const prevPages = computed(() => Array.from({ length: 4 }, (value, index) => {
|
||||||
|
const page = currentPage.value - index - 1;
|
||||||
|
|
||||||
|
if (page < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}).filter(Boolean));
|
||||||
|
|
||||||
|
const nextPages = computed(() => Array.from({ length: 4 }, (value, index) => {
|
||||||
|
const page = currentPage.value + index + 1;
|
||||||
|
|
||||||
|
if (page > pageTotal.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}).filter(Boolean));
|
||||||
|
|
||||||
|
function go(page, event) {
|
||||||
|
if (!props.redirect) {
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState({}, '', event.target.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('navigation', {
|
||||||
|
href: event.target.href,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPath(page) {
|
||||||
|
const query = typeof window === 'undefined'
|
||||||
|
? urlParsed.searchOriginal
|
||||||
|
: window.location.search;
|
||||||
|
|
||||||
|
if (!routeParams.path && props.includeQuery && query) {
|
||||||
|
return `${pageContext.urlParsed.pathname}${page}${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!routeParams.path) {
|
||||||
|
return `${pageContext.urlParsed.pathname}${page}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = parse(routeParams.path)
|
||||||
|
.map((segment) => {
|
||||||
|
if (segment.name === 'page') {
|
||||||
|
return `/${page}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (props.includeQuery && query) {
|
||||||
|
return `${path}${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<Campaign
|
<Campaign
|
||||||
@@ -91,122 +207,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
import { parse } from 'path-to-regexp';
|
|
||||||
|
|
||||||
import Campaign from '#/components/campaigns/campaign.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
page: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
total: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
redirect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
includeQuery: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
useMaxMatches: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigation']);
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const {
|
|
||||||
routeParams,
|
|
||||||
urlParsed,
|
|
||||||
pageProps,
|
|
||||||
env,
|
|
||||||
campaigns,
|
|
||||||
} = pageContext;
|
|
||||||
|
|
||||||
const currentPage = computed(() => props.page || Number(routeParams?.page));
|
|
||||||
|
|
||||||
const limit = computed(() => props.limit || Number(pageProps.limit) || 30);
|
|
||||||
const total = computed(() => props.total || Number(pageProps.total));
|
|
||||||
const pageTotal = computed(() => Math.ceil((props.useMaxMatches ? Math.min(total.value, env.maxMatches) : total.value) / limit.value));
|
|
||||||
|
|
||||||
const hasNextPage = computed(() => currentPage.value + 1 <= pageTotal.value);
|
|
||||||
const hasPrevPage = computed(() => currentPage.value - 1 >= 1);
|
|
||||||
|
|
||||||
const prevPages = computed(() => Array.from({ length: 4 }, (value, index) => {
|
|
||||||
const page = currentPage.value - index - 1;
|
|
||||||
|
|
||||||
if (page < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}).filter(Boolean));
|
|
||||||
|
|
||||||
const nextPages = computed(() => Array.from({ length: 4 }, (value, index) => {
|
|
||||||
const page = currentPage.value + index + 1;
|
|
||||||
|
|
||||||
if (page > pageTotal.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}).filter(Boolean));
|
|
||||||
|
|
||||||
function go(page, event) {
|
|
||||||
if (!props.redirect) {
|
|
||||||
event.preventDefault();
|
|
||||||
history.pushState({}, '', event.target.href); // eslint-disable-line no-restricted-globals
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('navigation', {
|
|
||||||
href: event.target.href,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPath(page) {
|
|
||||||
const query = typeof window === 'undefined'
|
|
||||||
? urlParsed.searchOriginal
|
|
||||||
: window.location.search;
|
|
||||||
|
|
||||||
if (!routeParams.path && props.includeQuery && query) {
|
|
||||||
return `${pageContext.urlParsed.pathname}${page}${query}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!routeParams.path) {
|
|
||||||
return `${pageContext.urlParsed.pathname}${page}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = parse(routeParams.path)
|
|
||||||
.map((segment) => {
|
|
||||||
if (segment.name === 'page') {
|
|
||||||
return `/${page}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
if (props.includeQuery && query) {
|
|
||||||
return `${path}${query}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
import { formatDate, formatDuration } from '#/utils/format.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
chapters: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastChapter = props.chapters.at(-1);
|
||||||
|
const duration = lastChapter.time + lastChapter.duration;
|
||||||
|
|
||||||
|
const timeline = computed(() => {
|
||||||
|
if (props.chapters.every((chapter) => chapter.time)) {
|
||||||
|
return props.chapters.filter((chapter) => chapter.tags?.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="timeline"
|
v-if="timeline"
|
||||||
@@ -43,19 +68,19 @@
|
|||||||
<span class="chapter-details">
|
<span class="chapter-details">
|
||||||
<span
|
<span
|
||||||
v-if="typeof chapter.time === 'number'"
|
v-if="typeof chapter.time === 'number'"
|
||||||
v-tooltip="'Time in video'"
|
v-tooltip="{ content: 'Time in video', ariaId: `chapter-time-${chapter.id}` }"
|
||||||
class="chapter-time"
|
class="chapter-time"
|
||||||
><Icon icon="film3" /> {{ formatDuration(chapter.time) }}</span>
|
><Icon icon="film3" /> {{ formatDuration(chapter.time) }}</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="chapter.duration"
|
v-if="chapter.duration"
|
||||||
v-tooltip="'Duration'"
|
v-tooltip="{ content: 'Duration', ariaId: `chapter-duration-${chapter.id}` }"
|
||||||
class="chapter-duration"
|
class="chapter-duration"
|
||||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||||
|
|
||||||
<time
|
<time
|
||||||
v-if="chapter.date"
|
v-if="chapter.date"
|
||||||
v-tooltip="formatDate(chapter.date, 'yyyy-MM-dd hh:mm')"
|
v-tooltip="{ content: formatDate(chapter.date, 'yyyy-MM-dd hh:mm'), ariaId: `chapter-date-${chapter.id}` }"
|
||||||
:datetime="chapter.date"
|
:datetime="chapter.date"
|
||||||
class="chapter-date"
|
class="chapter-date"
|
||||||
>{{ formatDate(chapter.date, 'MMM d') }}</time>
|
>{{ formatDate(chapter.date, 'MMM d') }}</time>
|
||||||
@@ -90,31 +115,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
import { formatDuration, formatDate } from '#/utils/format.js';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
chapters: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastChapter = props.chapters.at(-1);
|
|
||||||
const duration = lastChapter.time + lastChapter.duration;
|
|
||||||
|
|
||||||
const timeline = computed(() => {
|
|
||||||
if (props.chapters.every((chapter) => chapter.time)) {
|
|
||||||
return props.chapters.filter((chapter) => chapter.tags?.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chapters {
|
.chapters {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class="channel">
|
<div class="channel">
|
||||||
@@ -44,21 +59,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
scene: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,195 @@
|
|||||||
|
<script setup>
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
import { parse } from 'path-to-regexp';
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import Campaign from '#/components/campaigns/campaign.vue';
|
||||||
|
import ActorsFilter from '#/components/filters/actors.vue';
|
||||||
|
import ChannelsFilter from '#/components/filters/channels.vue';
|
||||||
|
import Filters from '#/components/filters/filters.vue';
|
||||||
|
|
||||||
|
import TagsFilter from '#/components/filters/tags.vue';
|
||||||
|
import YearsFilter from '#/components/filters/years.vue';
|
||||||
|
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||||
|
import Pagination from '#/components/pagination/pagination.vue';
|
||||||
|
import SceneTile from '#/components/scenes/tile.vue';
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
// import events from '#/src/events.js';
|
||||||
|
import entityPrefixes from '#/src/entities-prefixes.js';
|
||||||
|
import navigate from '#/src/navigate.js';
|
||||||
|
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
showFilters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showMeta: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showScopeTabs: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
defaultScope: {
|
||||||
|
type: String,
|
||||||
|
default: 'latest',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
pageProps,
|
||||||
|
routeParams,
|
||||||
|
urlParsed,
|
||||||
|
campaigns,
|
||||||
|
env,
|
||||||
|
} = inject('pageContext');
|
||||||
|
|
||||||
|
const {
|
||||||
|
actor: pageActor,
|
||||||
|
tag: pageTag,
|
||||||
|
entity: pageEntity,
|
||||||
|
stash: pageStash,
|
||||||
|
} = pageProps;
|
||||||
|
|
||||||
|
const scenes = ref(pageProps.scenes);
|
||||||
|
const aggYears = ref(pageProps.aggYears || []);
|
||||||
|
const aggActors = ref(pageProps.aggActors || []);
|
||||||
|
const aggTags = ref(pageProps.aggTags || []);
|
||||||
|
const aggActorTags = ref(pageProps.aggActorTags || []);
|
||||||
|
const aggChannels = ref(pageProps.aggChannels || []);
|
||||||
|
|
||||||
|
const currentPage = ref(Number(routeParams.page));
|
||||||
|
const scope = ref(routeParams.scope || props.defaultScope);
|
||||||
|
const total = ref(Number(pageProps.sceneTotal || pageProps.total));
|
||||||
|
const loading = ref(false);
|
||||||
|
const scenesContainer = ref(null);
|
||||||
|
|
||||||
|
const view = ref(env.scenesView || 'grid');
|
||||||
|
|
||||||
|
const actorIds = urlParsed.search.actors?.split(',').map((identifier) => parseActorIdentifier(identifier)?.id).filter(Boolean) || [];
|
||||||
|
const queryActors = actorIds.map((urlActorId) => aggActors.value.find((aggActor) => aggActor.id === urlActorId)).filter(Boolean);
|
||||||
|
|
||||||
|
const networks = Object.fromEntries(aggChannels.value.map((channel) => (channel.type === 'network' ? channel : channel.parent)).filter(Boolean).map((parent) => [`_${parent.slug}`, parent]));
|
||||||
|
const channels = Object.fromEntries(aggChannels.value.filter((channel) => channel.type === 'channel').map((channel) => [channel.slug, channel]));
|
||||||
|
|
||||||
|
const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e];
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
search: urlParsed.search.q,
|
||||||
|
years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
|
||||||
|
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
|
||||||
|
onlyActorTags: Object.hasOwn(urlParsed.search, 'at'),
|
||||||
|
entity: queryEntity,
|
||||||
|
actors: queryActors,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sceneCampaign = campaigns?.scenes;
|
||||||
|
const campaignIndex = campaigns?.index;
|
||||||
|
|
||||||
|
const campaignScenes = computed(() => scenes.value.flatMap((scene, index) => (sceneCampaign && index === campaignIndex ? ['campaign', scene] : scene)));
|
||||||
|
|
||||||
|
function getPath(targetScope, preserveQuery) {
|
||||||
|
const path = parse(routeParams.path).map((segment) => {
|
||||||
|
if (segment.name === 'scope') {
|
||||||
|
return `${segment.prefix}${targetScope}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.name === 'page') {
|
||||||
|
return `${segment.prefix}${1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (preserveQuery && urlParsed.searchOriginal) {
|
||||||
|
return `${path}${urlParsed.searchOriginal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(options = {}) {
|
||||||
|
if (options.resetPage !== false) {
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.autoScope !== false) {
|
||||||
|
if (filters.value.search) {
|
||||||
|
scope.value = 'results';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filters.value.search && scope.value === 'results') {
|
||||||
|
scope.value = 'latest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
q: filters.value.search || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const entity = filters.value.entity || pageEntity;
|
||||||
|
// const entitySlug = entity?.type === 'network' ? `_${entity.slug}` : entity?.slug;
|
||||||
|
const entitySlug = entity && `${entityPrefixes[entity.type]}${entity.slug}`;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
navigate(getPath(scope.value, false), {
|
||||||
|
...query,
|
||||||
|
years: filters.value.years.join(',') || undefined,
|
||||||
|
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
|
||||||
|
tags: filters.value.tags.join(',') || undefined,
|
||||||
|
at: (filters.value.tags.length > 0 && filters.value.onlyActorTags) || undefined,
|
||||||
|
// e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
|
||||||
|
e: filters.value.entity ? `${entityPrefixes[filters.value.entity.type]}${filters.value.entity.slug}` : undefined,
|
||||||
|
}, { redirect: false });
|
||||||
|
|
||||||
|
const res = await get('/scenes', {
|
||||||
|
...query,
|
||||||
|
years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
|
||||||
|
actors: [pageActor, ...filters.value.actors].filter(Boolean).map((filterActor) => getActorIdentifier(filterActor)).join(','), // if we're on an actor page, that actor ID needs to be included
|
||||||
|
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
|
||||||
|
at: !!filters.value.onlyActorTags,
|
||||||
|
stashId: pageStash?.id,
|
||||||
|
e: entitySlug,
|
||||||
|
scope: scope.value,
|
||||||
|
page: currentPage.value, // client uses param rather than query pagination
|
||||||
|
});
|
||||||
|
|
||||||
|
scenes.value = res.scenes;
|
||||||
|
aggYears.value = res.aggYears;
|
||||||
|
aggActors.value = res.aggActors;
|
||||||
|
aggTags.value = res.aggTags;
|
||||||
|
aggActorTags.value = res.aggActorTags;
|
||||||
|
aggChannels.value = res.aggChannels;
|
||||||
|
|
||||||
|
total.value = res.total;
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
// events.emit('scrollUp');
|
||||||
|
scenesContainer.value?.scrollIntoView(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilter(prop, value, reload = true) {
|
||||||
|
filters.value[prop] = value;
|
||||||
|
|
||||||
|
if (reload) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setView(newView) {
|
||||||
|
view.value = newView;
|
||||||
|
Cookies.set('scenesView', newView);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="scenes-page"
|
class="scenes-page"
|
||||||
@@ -12,12 +204,19 @@
|
|||||||
<!-- onsearch not compatible with FF and Safari -->
|
<!-- onsearch not compatible with FF and Safari -->
|
||||||
<input
|
<input
|
||||||
v-model="filters.search"
|
v-model="filters.search"
|
||||||
type="search"
|
type="input"
|
||||||
placeholder="Search scenes"
|
placeholder="Search scenes"
|
||||||
class="search input"
|
class="search input"
|
||||||
@search="search"
|
@keydown.enter="search"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="filters.search"
|
||||||
|
icon="cross2"
|
||||||
|
class="search-button"
|
||||||
|
@click="filters.search = ''; search()"
|
||||||
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
icon="search"
|
icon="search"
|
||||||
class="search-button"
|
class="search-button"
|
||||||
@@ -200,195 +399,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
computed,
|
|
||||||
inject,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { parse } from 'path-to-regexp';
|
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
// import events from '#/src/events.js';
|
|
||||||
import entityPrefixes from '#/src/entities-prefixes.js';
|
|
||||||
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
|
|
||||||
|
|
||||||
import Filters from '#/components/filters/filters.vue';
|
|
||||||
import YearsFilter from '#/components/filters/years.vue';
|
|
||||||
import ActorsFilter from '#/components/filters/actors.vue';
|
|
||||||
import TagsFilter from '#/components/filters/tags.vue';
|
|
||||||
import ChannelsFilter from '#/components/filters/channels.vue';
|
|
||||||
import SceneTile from '#/components/scenes/tile.vue';
|
|
||||||
import Campaign from '#/components/campaigns/campaign.vue';
|
|
||||||
import Pagination from '#/components/pagination/pagination.vue';
|
|
||||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
showFilters: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showMeta: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showScopeTabs: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
defaultScope: {
|
|
||||||
type: String,
|
|
||||||
default: 'latest',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
pageProps,
|
|
||||||
routeParams,
|
|
||||||
urlParsed,
|
|
||||||
campaigns,
|
|
||||||
env,
|
|
||||||
} = inject('pageContext');
|
|
||||||
|
|
||||||
const {
|
|
||||||
actor: pageActor,
|
|
||||||
tag: pageTag,
|
|
||||||
entity: pageEntity,
|
|
||||||
stash: pageStash,
|
|
||||||
} = pageProps;
|
|
||||||
|
|
||||||
const scenes = ref(pageProps.scenes);
|
|
||||||
const aggYears = ref(pageProps.aggYears || []);
|
|
||||||
const aggActors = ref(pageProps.aggActors || []);
|
|
||||||
const aggTags = ref(pageProps.aggTags || []);
|
|
||||||
const aggActorTags = ref(pageProps.aggActorTags || []);
|
|
||||||
const aggChannels = ref(pageProps.aggChannels || []);
|
|
||||||
|
|
||||||
const currentPage = ref(Number(routeParams.page));
|
|
||||||
const scope = ref(routeParams.scope || props.defaultScope);
|
|
||||||
const total = ref(Number(pageProps.sceneTotal || pageProps.total));
|
|
||||||
const loading = ref(false);
|
|
||||||
const scenesContainer = ref(null);
|
|
||||||
|
|
||||||
const view = ref(env.scenesView || 'grid');
|
|
||||||
|
|
||||||
const actorIds = urlParsed.search.actors?.split(',').map((identifier) => parseActorIdentifier(identifier)?.id).filter(Boolean) || [];
|
|
||||||
const queryActors = actorIds.map((urlActorId) => aggActors.value.find((aggActor) => aggActor.id === urlActorId)).filter(Boolean);
|
|
||||||
|
|
||||||
const networks = Object.fromEntries(aggChannels.value.map((channel) => (channel.type === 'network' ? channel : channel.parent)).filter(Boolean).map((parent) => [`_${parent.slug}`, parent]));
|
|
||||||
const channels = Object.fromEntries(aggChannels.value.filter((channel) => channel.type === 'channel').map((channel) => [channel.slug, channel]));
|
|
||||||
|
|
||||||
const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e];
|
|
||||||
|
|
||||||
const filters = ref({
|
|
||||||
search: urlParsed.search.q,
|
|
||||||
years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
|
|
||||||
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
|
|
||||||
entity: queryEntity,
|
|
||||||
actors: queryActors,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sceneCampaign = campaigns?.scenes;
|
|
||||||
const campaignIndex = campaigns?.index;
|
|
||||||
|
|
||||||
const campaignScenes = computed(() => scenes.value.flatMap((scene, index) => (sceneCampaign && index === campaignIndex ? ['campaign', scene] : scene)));
|
|
||||||
|
|
||||||
function getPath(targetScope, preserveQuery) {
|
|
||||||
const path = parse(routeParams.path).map((segment) => {
|
|
||||||
if (segment.name === 'scope') {
|
|
||||||
return `${segment.prefix}${targetScope}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.name === 'page') {
|
|
||||||
return `${segment.prefix}${1}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
if (preserveQuery && urlParsed.searchOriginal) {
|
|
||||||
return `${path}${urlParsed.searchOriginal}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function search(options = {}) {
|
|
||||||
if (options.resetPage !== false) {
|
|
||||||
currentPage.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.autoScope !== false) {
|
|
||||||
if (filters.value.search) {
|
|
||||||
scope.value = 'results';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filters.value.search && scope.value === 'results') {
|
|
||||||
scope.value = 'latest';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
q: filters.value.search || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const entity = filters.value.entity || pageEntity;
|
|
||||||
// const entitySlug = entity?.type === 'network' ? `_${entity.slug}` : entity?.slug;
|
|
||||||
const entitySlug = entity && `${entityPrefixes[entity.type]}${entity.slug}`;
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
navigate(getPath(scope.value, false), {
|
|
||||||
...query,
|
|
||||||
years: filters.value.years.join(',') || undefined,
|
|
||||||
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
|
|
||||||
tags: filters.value.tags.join(',') || undefined,
|
|
||||||
// e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
|
|
||||||
e: filters.value.entity ? `${entityPrefixes[filters.value.entity.type]}${filters.value.entity.slug}` : undefined,
|
|
||||||
}, { redirect: false });
|
|
||||||
|
|
||||||
const res = await get('/scenes', {
|
|
||||||
...query,
|
|
||||||
years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
|
|
||||||
actors: [pageActor, ...filters.value.actors].filter(Boolean).map((filterActor) => getActorIdentifier(filterActor)).join(','), // if we're on an actor page, that actor ID needs to be included
|
|
||||||
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
|
|
||||||
stashId: pageStash?.id,
|
|
||||||
e: entitySlug,
|
|
||||||
scope: scope.value,
|
|
||||||
page: currentPage.value, // client uses param rather than query pagination
|
|
||||||
});
|
|
||||||
|
|
||||||
scenes.value = res.scenes;
|
|
||||||
aggYears.value = res.aggYears;
|
|
||||||
aggActors.value = res.aggActors;
|
|
||||||
aggTags.value = res.aggTags;
|
|
||||||
aggActorTags.value = res.aggActorTags;
|
|
||||||
aggChannels.value = res.aggChannels;
|
|
||||||
|
|
||||||
total.value = res.total;
|
|
||||||
loading.value = false;
|
|
||||||
|
|
||||||
// events.emit('scrollUp');
|
|
||||||
scenesContainer.value?.scrollIntoView(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilter(prop, value, reload = true) {
|
|
||||||
filters.value[prop] = value;
|
|
||||||
|
|
||||||
if (reload) {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setView(newView) {
|
|
||||||
view.value = newView;
|
|
||||||
Cookies.set('scenesView', newView);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.scenes-page {
|
.scenes-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,158 @@
|
|||||||
|
<script setup>
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { inject, onMounted, ref } from 'vue';
|
||||||
|
import { parse } from 'yaml';
|
||||||
|
|
||||||
|
import defaultTemplate from '#/assets/summary.yaml?raw';
|
||||||
|
import { del, get, post } from '#/src/api.js';
|
||||||
|
// import slugify from '#/utils/slugify.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
release: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const cookies = Cookies.withConverter({
|
||||||
|
write: (value) => value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = ref(pageContext.assets.templates);
|
||||||
|
const selectedTemplate = ref(Number(pageContext.urlParsed.search.t) || templates.value.at(0)?.id || null);
|
||||||
|
|
||||||
|
const initialTemplate = templates.value.find((storedTemplate) => storedTemplate.id === selectedTemplate.value) || null;
|
||||||
|
|
||||||
|
const template = ref(initialTemplate?.template || defaultTemplate);
|
||||||
|
const hasError = ref(false);
|
||||||
|
const input = ref(null);
|
||||||
|
const changed = ref(false);
|
||||||
|
|
||||||
|
const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`);
|
||||||
|
|
||||||
|
function getSummary() {
|
||||||
|
return processSummaryTemplate(template.value, props.release, pageContext.env);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = ref(getSummary());
|
||||||
|
|
||||||
|
function selectTemplate(templateId) {
|
||||||
|
selectedTemplate.value = templateId;
|
||||||
|
|
||||||
|
const nextTemplate = templates.value.find((storedTemplate) => storedTemplate.id === templateId);
|
||||||
|
|
||||||
|
template.value = nextTemplate.template;
|
||||||
|
templateName.value = nextTemplate.name;
|
||||||
|
summary.value = getSummary();
|
||||||
|
|
||||||
|
cookies.set('selectedTemplate', String(templateId));
|
||||||
|
window.location.href = `${window.location.origin}${window.location.pathname}?t=${nextTemplate.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
hasError.value = false;
|
||||||
|
changed.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
summary.value = getSummary();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
hasError.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(explicit) {
|
||||||
|
if (!explicit && !changed.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
parse(template.value);
|
||||||
|
|
||||||
|
const createdTemplate = await post('/templates', {
|
||||||
|
name: templateName.value,
|
||||||
|
template: template.value,
|
||||||
|
}, {
|
||||||
|
successFeedback: `Saved summary template '${templateName.value}'`,
|
||||||
|
errorFeedback: `Failed to save summary template '${templateName.value}'`,
|
||||||
|
});
|
||||||
|
|
||||||
|
changed.value = false;
|
||||||
|
templates.value = await get(`/users/${pageContext.user.id}/templates`);
|
||||||
|
|
||||||
|
selectTemplate(createdTemplate.id);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to save summary template '${templateName.value}': ${error.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
selectedTemplate.value = null;
|
||||||
|
template.value = '';
|
||||||
|
templateName.value = `custom_${Date.now()}`;
|
||||||
|
summary.value = '';
|
||||||
|
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove() {
|
||||||
|
if (confirm(`Are you sure you want to delete summary template ${templateName.value}?`)) { // eslint-disable-line no-alert
|
||||||
|
await del(`/templates/${selectedTemplate.value}`, {
|
||||||
|
undoFeedback: `Deleted summary template '${templateName.value}'`,
|
||||||
|
errorFeedback: `Failed to remove summary template '${templateName.value}'`,
|
||||||
|
});
|
||||||
|
|
||||||
|
templates.value = await get(`/users/${pageContext.user.id}/templates`);
|
||||||
|
|
||||||
|
selectTemplate(templates.value.at(-1)?.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy() {
|
||||||
|
navigator.clipboard.writeText(summary.value);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Summary copied to clipboard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (confirm('Are you sure you want to reset the summary template to the default? Your custom template will be discarded.')) { // eslint-disable-line no-alert
|
||||||
|
template.value = defaultTemplate;
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'undo',
|
||||||
|
message: 'Reset summary template',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indent() {
|
||||||
|
// YAML does not support tabs
|
||||||
|
input.value.setRangeText(' ', input.value.selectionStart, input.value.selectionEnd, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('beforeunload', (event) => {
|
||||||
|
if (changed.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
@@ -89,159 +244,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject, onMounted } from 'vue';
|
|
||||||
import { parse } from 'yaml';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
|
|
||||||
// import slugify from '#/utils/slugify.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
import { get, post, del } from '#/src/api.js';
|
|
||||||
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
|
||||||
|
|
||||||
import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const cookies = Cookies.withConverter({
|
|
||||||
write: (value) => value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
release: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const templates = ref(pageContext.assets.templates);
|
|
||||||
const selectedTemplate = ref(Number(pageContext.urlParsed.search.t) || templates.value.at(0)?.id || null);
|
|
||||||
|
|
||||||
const initialTemplate = templates.value.find((storedTemplate) => storedTemplate.id === selectedTemplate.value) || null;
|
|
||||||
|
|
||||||
const template = ref(initialTemplate?.template || defaultTemplate);
|
|
||||||
const hasError = ref(false);
|
|
||||||
const input = ref(null);
|
|
||||||
const changed = ref(false);
|
|
||||||
|
|
||||||
const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`);
|
|
||||||
|
|
||||||
function getSummary() {
|
|
||||||
return processSummaryTemplate(template.value, props.release, pageContext.env);
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary = ref(getSummary());
|
|
||||||
|
|
||||||
function selectTemplate(templateId) {
|
|
||||||
selectedTemplate.value = templateId;
|
|
||||||
|
|
||||||
const nextTemplate = templates.value.find((storedTemplate) => storedTemplate.id === templateId);
|
|
||||||
|
|
||||||
template.value = nextTemplate.template;
|
|
||||||
templateName.value = nextTemplate.name;
|
|
||||||
summary.value = getSummary();
|
|
||||||
|
|
||||||
cookies.set('selectedTemplate', String(templateId));
|
|
||||||
window.location.href = `${window.location.origin}${window.location.pathname}?t=${nextTemplate.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
hasError.value = false;
|
|
||||||
changed.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
summary.value = getSummary();
|
|
||||||
} catch (error) {
|
|
||||||
hasError.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(explicit) {
|
|
||||||
if (!explicit && !changed.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
parse(template.value);
|
|
||||||
|
|
||||||
const createdTemplate = await post('/templates', {
|
|
||||||
name: templateName.value,
|
|
||||||
template: template.value,
|
|
||||||
}, {
|
|
||||||
successFeedback: `Saved summary template '${templateName.value}'`,
|
|
||||||
errorFeedback: `Failed to save summary template '${templateName.value}'`,
|
|
||||||
});
|
|
||||||
|
|
||||||
changed.value = false;
|
|
||||||
templates.value = await get(`/users/${pageContext.user.id}/templates`);
|
|
||||||
|
|
||||||
selectTemplate(createdTemplate.id);
|
|
||||||
} catch (error) {
|
|
||||||
events.emit('feedback', {
|
|
||||||
type: 'error',
|
|
||||||
message: `Failed to save summary template '${templateName.value}': ${error.message}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function add() {
|
|
||||||
selectedTemplate.value = null;
|
|
||||||
template.value = '';
|
|
||||||
templateName.value = `custom_${Date.now()}`;
|
|
||||||
summary.value = '';
|
|
||||||
|
|
||||||
input.value.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remove() {
|
|
||||||
if (confirm(`Are you sure you want to delete summary template ${templateName.value}?`)) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
await del(`/templates/${selectedTemplate.value}`, {
|
|
||||||
undoFeedback: `Deleted summary template '${templateName.value}'`,
|
|
||||||
errorFeedback: `Failed to remove summary template '${templateName.value}'`,
|
|
||||||
});
|
|
||||||
|
|
||||||
templates.value = await get(`/users/${pageContext.user.id}/templates`);
|
|
||||||
|
|
||||||
selectTemplate(templates.value.at(-1)?.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copy() {
|
|
||||||
navigator.clipboard.writeText(summary.value);
|
|
||||||
|
|
||||||
events.emit('feedback', {
|
|
||||||
type: 'success',
|
|
||||||
message: 'Summary copied to clipboard',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
if (confirm('Are you sure you want to reset the summary template to the default? Your custom template will be discarded.')) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
template.value = defaultTemplate;
|
|
||||||
|
|
||||||
update();
|
|
||||||
|
|
||||||
events.emit('feedback', {
|
|
||||||
type: 'undo',
|
|
||||||
message: 'Reset summary template',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function indent() {
|
|
||||||
// YAML does not support tabs
|
|
||||||
input.value.setRangeText(' ', input.value.selectionStart, input.value.selectionEnd, 'end');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('beforeunload', (event) => {
|
|
||||||
if (changed.value) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.editor {
|
.editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import Meta from '#/components/scenes/meta.vue';
|
||||||
|
|
||||||
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { user } = pageContext;
|
||||||
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||||
|
|
||||||
|
const tagsById = Object.fromEntries(props.scene.tags.map((tag) => [tag.id, tag]));
|
||||||
|
const sceneTags = Array.from(new Set(props.scene.tags.map((tag) => tag.id))).map((tagId) => tagsById[tagId]);
|
||||||
|
|
||||||
|
const priorityTags = sceneTags.map((tag) => tag.name).slice(0, 2);
|
||||||
|
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="tile"
|
class="tile"
|
||||||
@@ -115,33 +142,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
import Meta from '#/components/scenes/meta.vue';
|
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
scene: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { user } = pageContext;
|
|
||||||
const pageStash = pageContext.pageProps.stash;
|
|
||||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
|
||||||
|
|
||||||
const tagsById = Object.fromEntries(props.scene.tags.map((tag) => [tag.id, tag]));
|
|
||||||
const sceneTags = Array.from(new Set(props.scene.tags.map((tag) => tag.id))).map((tagId) => tagsById[tagId]);
|
|
||||||
|
|
||||||
const priorityTags = sceneTags.map((tag) => tag.name).slice(0, 2);
|
|
||||||
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tile {
|
.tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
serie: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="serie-tile">
|
<div class="serie-tile">
|
||||||
<a
|
<a
|
||||||
@@ -22,17 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
serie: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.serie-tile {
|
.serie-tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
|
||||||
|
const cookies = Cookies.withConverter({
|
||||||
|
write: (value) => value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags = {
|
||||||
|
'anal': 'anal',
|
||||||
|
'anal-prolapse': 'anal prolapse',
|
||||||
|
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
||||||
|
'pissing': 'pissing',
|
||||||
|
'gay': 'gay',
|
||||||
|
'transsexual': 'transsexual',
|
||||||
|
'bisexual': 'bisexual',
|
||||||
|
'compilation': 'compilation',
|
||||||
|
'bts': 'behind the scenes',
|
||||||
|
'vr': 'virtual reality',
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedTags = cookies.get('tags');
|
||||||
|
const checkedTags = ref(new Set(storedTags ? JSON.parse(storedTags) : []));
|
||||||
|
|
||||||
|
function toggleTag(tag, isChecked) {
|
||||||
|
if (isChecked) {
|
||||||
|
checkedTags.value.add(tag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
checkedTags.value.delete(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.set('tags', JSON.stringify(Array.from(checkedTags.value)), { expires: 400 }); // 100 years from now
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog title="Settings">
|
<Dialog title="Settings">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
@@ -27,44 +66,6 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
|
|
||||||
import Dialog from '#/components/dialog/dialog.vue';
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
|
|
||||||
const cookies = Cookies.withConverter({
|
|
||||||
write: (value) => value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tags = {
|
|
||||||
anal: 'anal',
|
|
||||||
'anal-prolapse': 'anal prolapse',
|
|
||||||
'extreme-insertion': 'extreme insertion (oversized dildos)',
|
|
||||||
pissing: 'pissing',
|
|
||||||
gay: 'gay',
|
|
||||||
transsexual: 'transsexual',
|
|
||||||
bisexual: 'bisexual',
|
|
||||||
compilation: 'compilation',
|
|
||||||
bts: 'behind the scenes',
|
|
||||||
vr: 'virtual reality',
|
|
||||||
};
|
|
||||||
|
|
||||||
const storedTags = cookies.get('tags');
|
|
||||||
const checkedTags = ref(new Set(storedTags ? JSON.parse(storedTags) : []));
|
|
||||||
|
|
||||||
function toggleTag(tag, isChecked) {
|
|
||||||
if (isChecked) {
|
|
||||||
checkedTags.value.add(tag);
|
|
||||||
} else {
|
|
||||||
checkedTags.value.delete(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies.set('tags', JSON.stringify(Array.from(checkedTags.value)), { expires: 400 }); // 100 years from now
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
import logo from '#/assets/img/logo.svg?raw';
|
||||||
|
|
||||||
|
const emit = defineEmits(['sidebar']);
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sidebar-container"
|
class="sidebar-container"
|
||||||
@@ -85,17 +96,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import logo from '#/assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
|
||||||
|
|
||||||
const emit = defineEmits(['sidebar']);
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
|
|
||||||
|
import { post } from '#/src/api.js';
|
||||||
|
|
||||||
|
const emit = defineEmits(['created', 'close']);
|
||||||
|
|
||||||
|
const stashName = ref(null);
|
||||||
|
const stashNameInput = ref(null);
|
||||||
|
|
||||||
|
async function createStash() {
|
||||||
|
const newStash = await post('/stashes', {
|
||||||
|
name: stashName.value,
|
||||||
|
public: false,
|
||||||
|
}, {
|
||||||
|
successFeedback: `Stash '${stashName.value}' created`,
|
||||||
|
errorFeedback: `Failed to create stash '${stashName.value}'`,
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('created', newStash);
|
||||||
|
stashName.value = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
title="New stash"
|
title="New stash"
|
||||||
@@ -23,33 +50,6 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { post } from '#/src/api.js';
|
|
||||||
|
|
||||||
import Dialog from '#/components/dialog/dialog.vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['created', 'close']);
|
|
||||||
|
|
||||||
const stashName = ref(null);
|
|
||||||
const stashNameInput = ref(null);
|
|
||||||
|
|
||||||
async function createStash() {
|
|
||||||
const newStash = await post('/stashes', {
|
|
||||||
name: stashName.value,
|
|
||||||
public: false,
|
|
||||||
}, {
|
|
||||||
successFeedback: `Stash '${stashName.value}' created`,
|
|
||||||
errorFeedback: `Failed to create stash '${stashName.value}'`,
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('created', newStash);
|
|
||||||
stashName.value = null;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
@@ -1,3 +1,204 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import AlertDialog from '#/components/alerts/create.vue';
|
||||||
|
import Icon from '#/components/icon/icon.vue';
|
||||||
|
|
||||||
|
import StashDialog from '#/components/stashes/create.vue';
|
||||||
|
import StashMenu from '#/components/stashes/menu.vue';
|
||||||
|
import { del, get, post } from '#/src/api.js';
|
||||||
|
import ellipsis from '#/utils/ellipsis.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
domain: {
|
||||||
|
type: String,
|
||||||
|
default: 'scenes',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showSecondary: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['stashed', 'unstashed']);
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
const user = pageContext.user;
|
||||||
|
|
||||||
|
const stashes = ref(pageContext.assets?.stashes);
|
||||||
|
const primaryStash = pageContext.assets?.primaryStash;
|
||||||
|
const itemStashes = ref(props.item.stashes);
|
||||||
|
const itemAlerts = ref(props.item.alerts);
|
||||||
|
const itemAlerted = ref(props.item.alerts?.only.length > 0);
|
||||||
|
const hasSecondaryStash = computed(() => itemStashes.value.some((itemStash) => !itemStash.isPrimary));
|
||||||
|
|
||||||
|
const done = ref(true);
|
||||||
|
const stashMenuDropdownId = useId();
|
||||||
|
const stashTogglesDropdownId = useId();
|
||||||
|
const showStashes = ref(false);
|
||||||
|
const showStashDialog = ref(false);
|
||||||
|
const showAlertDialog = ref(false);
|
||||||
|
const feedbackCutoff = 20;
|
||||||
|
|
||||||
|
const itemKeys = {
|
||||||
|
scenes: 'sceneId',
|
||||||
|
actors: 'actorId',
|
||||||
|
movies: 'movieId',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function stashItem(stash) {
|
||||||
|
if (!done.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stashState = itemStashes.value;
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
itemStashes.value = itemStashes.value.concat(stash);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await post(`/stashes/${stash.id}/${props.domain}`, { [itemKeys[props.domain]]: props.item.id }, {
|
||||||
|
successFeedback: `"${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" stashed to ${stash.name}`,
|
||||||
|
errorFeedback: `Failed to stash "${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" to ${stash.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('stashed', stash);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
itemStashes.value = stashState;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unstashItem(stash) {
|
||||||
|
if (!done.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stashState = itemStashes.value;
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
itemStashes.value = itemStashes.value.filter((itemStash) => itemStash.id !== stash.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del(`/stashes/${stash.id}/${props.domain}/${props.item.id}`, {
|
||||||
|
undoFeedback: `"${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" unstashed from ${stash.name}`,
|
||||||
|
errorFeedback: `Failed to unstash "${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" from ${stash.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('unstashed', stash);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
itemStashes.value = stashState;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alertItem() {
|
||||||
|
if (!done.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertLabel = props.item.title || props.item.name;
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
itemAlerted.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAlert = await post('/alerts', {
|
||||||
|
...(props.domain === 'actors' && { actors: [props.item.id] }),
|
||||||
|
...(props.domain === 'tags' && { tags: [props.item.id] }),
|
||||||
|
...(props.domain === 'entities' && { entities: [props.item.id] }),
|
||||||
|
notify: true,
|
||||||
|
email: false,
|
||||||
|
preset: true,
|
||||||
|
}, {
|
||||||
|
successFeedback: `Alert set for '${alertLabel}'`,
|
||||||
|
errorFeedback: `Failed to set alert for '${alertLabel}'`,
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
itemAlerts.value.only = itemAlerts.value.only.concat(newAlert.id);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
itemAlerted.value = false;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAlert() {
|
||||||
|
if (done.value === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertLabel = props.item.title || props.item.name;
|
||||||
|
const alertIds = itemAlerts.value.only;
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
itemAlerted.value = false;
|
||||||
|
itemAlerts.value.only = itemAlerts.value.only.filter((alertId) => !alertIds.includes(alertId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del(`/alerts/${alertIds.join(',')}`, {
|
||||||
|
undoFeedback: `Removed alert for '${alertLabel}'`,
|
||||||
|
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
itemAlerted.value = true;
|
||||||
|
itemAlerts.value.only = itemAlerts.value.only.concat(alertIds);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
done.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAlerted(alert) {
|
||||||
|
// only set alerted status if the current item is the only one in the new stash
|
||||||
|
const items = [...alert.actors, ...alert.tags, ...alert.entities, ...alert.matches];
|
||||||
|
|
||||||
|
if (items.length === 1 && alert[props.domain][0]?.id === props.item.id) {
|
||||||
|
itemAlerted.value = true;
|
||||||
|
itemAlerts.value.only = itemAlerts.value.only.concat(alert.id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
itemAlerts.value.multi = itemAlerts.value.multi.concat(alert.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShowStashes(state) {
|
||||||
|
if (props.showSecondary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
showStashes.value = state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showStashes.value = !showStashes.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadStashes(newStash) {
|
||||||
|
stashes.value = await get(`/users/${user.id}/stashes`);
|
||||||
|
|
||||||
|
await stashItem(newStash);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="user"
|
v-if="user"
|
||||||
@@ -37,6 +238,7 @@
|
|||||||
|
|
||||||
<VDropdown
|
<VDropdown
|
||||||
v-if="itemStashes && (showSecondary || (hasSecondaryStash && pageStash?.user.id !== user.id))"
|
v-if="itemStashes && (showSecondary || (hasSecondaryStash && pageStash?.user.id !== user.id))"
|
||||||
|
:aria-id="stashMenuDropdownId"
|
||||||
:shown="showStashes"
|
:shown="showStashes"
|
||||||
@hide="showStashes = false"
|
@hide="showStashes = false"
|
||||||
>
|
>
|
||||||
@@ -60,6 +262,7 @@
|
|||||||
|
|
||||||
<VDropdown
|
<VDropdown
|
||||||
v-if="itemStashes && (showSecondary || !hasSecondaryStash || pageStash?.user.id === user.id)"
|
v-if="itemStashes && (showSecondary || !hasSecondaryStash || pageStash?.user.id === user.id)"
|
||||||
|
:aria-id="stashTogglesDropdownId"
|
||||||
:triggers="[]"
|
:triggers="[]"
|
||||||
:shown="showStashes"
|
:shown="showStashes"
|
||||||
:disabled="showSecondary"
|
:disabled="showSecondary"
|
||||||
@@ -70,7 +273,7 @@
|
|||||||
v-if="itemStashes.some((itemStash) => itemStash.id === pageStash.id)"
|
v-if="itemStashes.some((itemStash) => itemStash.id === pageStash.id)"
|
||||||
:icon="pageStash.isPrimary ? 'heart7' : 'folder-heart'"
|
:icon="pageStash.isPrimary ? 'heart7' : 'folder-heart'"
|
||||||
class="heart favorited noselect"
|
class="heart favorited noselect"
|
||||||
@click.native.stop="unstashItem(pageStash)"
|
@click.stop="unstashItem(pageStash)"
|
||||||
@contextmenu.prevent="toggleShowStashes(true)"
|
@contextmenu.prevent="toggleShowStashes(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -78,7 +281,7 @@
|
|||||||
v-else
|
v-else
|
||||||
:icon="pageStash.isPrimary ? 'heart8' : 'folder-heart'"
|
:icon="pageStash.isPrimary ? 'heart8' : 'folder-heart'"
|
||||||
class="heart noselect"
|
class="heart noselect"
|
||||||
@click.native.stop="stashItem(pageStash)"
|
@click.stop="stashItem(pageStash)"
|
||||||
@contextmenu.prevent="toggleShowStashes(true)"
|
@contextmenu.prevent="toggleShowStashes(true)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,7 +291,7 @@
|
|||||||
v-if="itemStashes.some((itemStash) => itemStash.id === primaryStash.id)"
|
v-if="itemStashes.some((itemStash) => itemStash.id === primaryStash.id)"
|
||||||
icon="heart7"
|
icon="heart7"
|
||||||
class="heart favorited noselect"
|
class="heart favorited noselect"
|
||||||
@click.native.stop="unstashItem(primaryStash)"
|
@click.stop="unstashItem(primaryStash)"
|
||||||
@contextmenu.prevent="toggleShowStashes(true)"
|
@contextmenu.prevent="toggleShowStashes(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -96,7 +299,7 @@
|
|||||||
v-else
|
v-else
|
||||||
icon="heart8"
|
icon="heart8"
|
||||||
class="heart noselect"
|
class="heart noselect"
|
||||||
@click.native.stop="stashItem(primaryStash)"
|
@click.stop="stashItem(primaryStash)"
|
||||||
@contextmenu.prevent="toggleShowStashes(true)"
|
@contextmenu.prevent="toggleShowStashes(true)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -127,196 +330,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import { get, post, del } from '#/src/api.js';
|
|
||||||
import ellipsis from '#/utils/ellipsis.js';
|
|
||||||
|
|
||||||
import Icon from '#/components/icon/icon.vue';
|
|
||||||
import StashMenu from '#/components/stashes/menu.vue';
|
|
||||||
import StashDialog from '#/components/stashes/create.vue';
|
|
||||||
import AlertDialog from '#/components/alerts/create.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
domain: {
|
|
||||||
type: String,
|
|
||||||
default: 'scenes',
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showSecondary: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['stashed', 'unstashed']);
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const pageStash = pageContext.pageProps.stash;
|
|
||||||
const user = pageContext.user;
|
|
||||||
|
|
||||||
const stashes = ref(pageContext.assets?.stashes);
|
|
||||||
const primaryStash = pageContext.assets?.primaryStash;
|
|
||||||
const itemStashes = ref(props.item.stashes);
|
|
||||||
const itemAlerts = ref(props.item.alerts);
|
|
||||||
const itemAlerted = ref(props.item.alerts?.only.length > 0);
|
|
||||||
const hasSecondaryStash = computed(() => itemStashes.value.some((itemStash) => !itemStash.isPrimary));
|
|
||||||
|
|
||||||
const done = ref(true);
|
|
||||||
const showStashes = ref(false);
|
|
||||||
const showStashDialog = ref(false);
|
|
||||||
const showAlertDialog = ref(false);
|
|
||||||
const feedbackCutoff = 20;
|
|
||||||
|
|
||||||
const itemKeys = {
|
|
||||||
scenes: 'sceneId',
|
|
||||||
actors: 'actorId',
|
|
||||||
movies: 'movieId',
|
|
||||||
};
|
|
||||||
|
|
||||||
async function stashItem(stash) {
|
|
||||||
if (!done.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stashState = itemStashes.value;
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
itemStashes.value = itemStashes.value.concat(stash);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await post(`/stashes/${stash.id}/${props.domain}`, { [itemKeys[props.domain]]: props.item.id }, {
|
|
||||||
successFeedback: `"${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" stashed to ${stash.name}`,
|
|
||||||
errorFeedback: `Failed to stash "${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" to ${stash.name}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('stashed', stash);
|
|
||||||
} catch (error) {
|
|
||||||
itemStashes.value = stashState;
|
|
||||||
} finally {
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unstashItem(stash) {
|
|
||||||
if (!done.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stashState = itemStashes.value;
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
itemStashes.value = itemStashes.value.filter((itemStash) => itemStash.id !== stash.id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await del(`/stashes/${stash.id}/${props.domain}/${props.item.id}`, {
|
|
||||||
undoFeedback: `"${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" unstashed from ${stash.name}`,
|
|
||||||
errorFeedback: `Failed to unstash "${ellipsis(props.item.title || props.item.name, feedbackCutoff)}" from ${stash.name}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('unstashed', stash);
|
|
||||||
} catch (error) {
|
|
||||||
itemStashes.value = stashState;
|
|
||||||
} finally {
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function alertItem() {
|
|
||||||
if (!done.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertLabel = props.item.title || props.item.name;
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
itemAlerted.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newAlert = await post('/alerts', {
|
|
||||||
...(props.domain === 'actors' && { actors: [props.item.id] }),
|
|
||||||
...(props.domain === 'tags' && { tags: [props.item.id] }),
|
|
||||||
...(props.domain === 'entities' && { entities: [props.item.id] }),
|
|
||||||
notify: true,
|
|
||||||
email: false,
|
|
||||||
preset: true,
|
|
||||||
}, {
|
|
||||||
successFeedback: `Alert set for '${alertLabel}'`,
|
|
||||||
errorFeedback: `Failed to set alert for '${alertLabel}'`,
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
itemAlerts.value.only = itemAlerts.value.only.concat(newAlert.id);
|
|
||||||
} catch (error) {
|
|
||||||
itemAlerted.value = false;
|
|
||||||
} finally {
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAlert() {
|
|
||||||
if (done.value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertLabel = props.item.title || props.item.name;
|
|
||||||
const alertIds = itemAlerts.value.only;
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
itemAlerted.value = false;
|
|
||||||
itemAlerts.value.only = itemAlerts.value.only.filter((alertId) => !alertIds.includes(alertId));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await del(`/alerts/${alertIds.join(',')}`, {
|
|
||||||
undoFeedback: `Removed alert for '${alertLabel}'`,
|
|
||||||
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
itemAlerted.value = true;
|
|
||||||
itemAlerts.value.only = itemAlerts.value.only.concat(alertIds);
|
|
||||||
} finally {
|
|
||||||
done.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAlerted(alert) {
|
|
||||||
// only set alerted status if the current item is the only one in the new stash
|
|
||||||
const items = [...alert.actors, ...alert.tags, ...alert.entities, ...alert.matches];
|
|
||||||
|
|
||||||
if (items.length === 1 && alert[props.domain][0]?.id === props.item.id) {
|
|
||||||
itemAlerted.value = true;
|
|
||||||
itemAlerts.value.only = itemAlerts.value.only.concat(alert.id);
|
|
||||||
} else {
|
|
||||||
itemAlerts.value.multi = itemAlerts.value.multi.concat(alert.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleShowStashes(state) {
|
|
||||||
if (props.showSecondary) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
showStashes.value = state;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStashes.value = !showStashes.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reloadStashes(newStash) {
|
|
||||||
stashes.value = await get(`/users/${user.id}/stashes`);
|
|
||||||
|
|
||||||
await stashItem(newStash);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bookmarks {
|
.bookmarks {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
stashes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
itemStashes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['stash', 'unstash', 'create']);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="stash-menu nolist noselect">
|
<ul class="stash-menu nolist noselect">
|
||||||
<li
|
<li
|
||||||
@@ -26,23 +43,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
stashes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
itemStashes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['stash', 'unstash', 'create']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import Domains from '#/components/domains/domains.vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { routeParams } = pageContext;
|
||||||
|
|
||||||
|
const domain = routeParams.domain;
|
||||||
|
const stash = pageContext.pageProps.stash;
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="stash">
|
<div class="stash">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -44,18 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
import Domains from '#/components/domains/domains.vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { routeParams } = pageContext;
|
|
||||||
|
|
||||||
const domain = routeParams.domain;
|
|
||||||
const stash = pageContext.pageProps.stash;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stash {
|
.stash {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
import StashDialog from '#/components/stashes/create.vue';
|
||||||
|
import StashTile from '#/components/stashes/tile.vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const user = pageContext.user;
|
||||||
|
const stashes = ref(pageContext.pageProps.stashes);
|
||||||
|
const profile = ref(pageContext.pageProps.profile);
|
||||||
|
|
||||||
|
const showStashDialog = ref(false);
|
||||||
|
|
||||||
|
async function reloadStashes() {
|
||||||
|
// profile.value = await get(`/users/${profile.value.id}`);
|
||||||
|
stashes.value = await get(`/users/${profile.value.id}/stashes`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="profile-section">
|
<section class="profile-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -34,28 +56,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
|
|
||||||
import StashTile from '#/components/stashes/tile.vue';
|
|
||||||
import StashDialog from '#/components/stashes/create.vue';
|
|
||||||
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const user = pageContext.user;
|
|
||||||
const stashes = ref(pageContext.pageProps.stashes);
|
|
||||||
const profile = ref(pageContext.pageProps.profile);
|
|
||||||
|
|
||||||
const showStashDialog = ref(false);
|
|
||||||
|
|
||||||
async function reloadStashes() {
|
|
||||||
// profile.value = await get(`/users/${profile.value.id}`);
|
|
||||||
stashes.value = await get(`/users/${profile.value.id}/stashes`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stashes {
|
.stashes {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,3 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import Dialog from '#/components/dialog/dialog.vue';
|
||||||
|
import { del, patch } from '#/src/api.js';
|
||||||
|
|
||||||
|
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
stash: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['reload']);
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const user = pageContext.user;
|
||||||
|
|
||||||
|
const stashName = ref(props.stash.name);
|
||||||
|
const stashNameInput = ref(null);
|
||||||
|
const showRenameDialog = ref(false);
|
||||||
|
const done = ref(true);
|
||||||
|
const privateTooltipId = useId();
|
||||||
|
const menuDropdownId = useId();
|
||||||
|
|
||||||
|
const domainCounts = {
|
||||||
|
scenes: props.stash.stashedScenes,
|
||||||
|
actors: props.stash.stashedActors,
|
||||||
|
movies: props.stash.stashedMovies,
|
||||||
|
};
|
||||||
|
|
||||||
|
const primaryDomain = Object.entries(domainCounts).toSorted((domainA, domainB) => domainB[1] - domainA[1])[0][0];
|
||||||
|
|
||||||
|
async function setPublic(isPublic) {
|
||||||
|
if (done.value === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
await patch(`/stashes/${props.stash.id}`, { isPublic }, {
|
||||||
|
undoFeedback: !isPublic && `Stash '${props.stash.name}' set to private`,
|
||||||
|
successFeedback: isPublic && `Stash '${props.stash.name}' set to public`,
|
||||||
|
errorFeedback: 'Failed to update stash',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
}).finally(() => { done.value = true; });
|
||||||
|
|
||||||
|
emit('reload');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove() {
|
||||||
|
if (done.value === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to remove the stash '${props.stash.name}' and all its scenes, movies and actors? This is permament, it cannot be undone by you or staff!`)) { // eslint-disable-line no-alert
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
await del(`/stashes/${props.stash.id}`, {
|
||||||
|
undoFeedback: `Stash '${props.stash.name}' removed`,
|
||||||
|
errorFeedback: 'Failed to remove stash',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
}).finally(() => { done.value = true; });
|
||||||
|
|
||||||
|
emit('reload');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rename() {
|
||||||
|
if (done.value === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
done.value = false;
|
||||||
|
|
||||||
|
await patch(`/stashes/${props.stash.id}`, { name: stashName.value }, {
|
||||||
|
successFeedback: `Stash renamed to ${stashName.value}`,
|
||||||
|
errorFeedback: 'Failed to rename stash',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
}).finally(() => { done.value = true; });
|
||||||
|
|
||||||
|
emit('reload');
|
||||||
|
|
||||||
|
showRenameDialog.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="stash">
|
<div class="stash">
|
||||||
<div class="stash-header">
|
<div class="stash-header">
|
||||||
@@ -16,12 +111,12 @@
|
|||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-if="!stash.isPublic"
|
v-if="!stash.isPublic"
|
||||||
v-tooltip="'This stash is private'"
|
v-tooltip="{ content: 'This stash is private', ariaId: privateTooltipId }"
|
||||||
icon="eye-blocked"
|
icon="eye-blocked"
|
||||||
class="private noselect"
|
class="private noselect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VDropdown>
|
<VDropdown :aria-id="menuDropdownId">
|
||||||
<Icon
|
<Icon
|
||||||
v-if="profile.id === user?.id"
|
v-if="profile.id === user?.id"
|
||||||
icon="more2"
|
icon="more2"
|
||||||
@@ -122,99 +217,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
|
|
||||||
import { del, patch } from '#/src/api.js';
|
|
||||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
|
||||||
|
|
||||||
import Dialog from '#/components/dialog/dialog.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
stash: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const user = pageContext.user;
|
|
||||||
|
|
||||||
const stashName = ref(props.stash.name);
|
|
||||||
const stashNameInput = ref(null);
|
|
||||||
const showRenameDialog = ref(false);
|
|
||||||
const done = ref(true);
|
|
||||||
|
|
||||||
const domainCounts = {
|
|
||||||
scenes: props.stash.stashedScenes,
|
|
||||||
actors: props.stash.stashedActors,
|
|
||||||
movies: props.stash.stashedMovies,
|
|
||||||
};
|
|
||||||
|
|
||||||
const primaryDomain = Object.entries(domainCounts).toSorted((domainA, domainB) => domainB[1] - domainA[1])[0][0];
|
|
||||||
|
|
||||||
async function setPublic(isPublic) {
|
|
||||||
if (done.value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
await patch(`/stashes/${props.stash.id}`, { isPublic }, {
|
|
||||||
undoFeedback: !isPublic && `Stash '${props.stash.name}' set to private`,
|
|
||||||
successFeedback: isPublic && `Stash '${props.stash.name}' set to public`,
|
|
||||||
errorFeedback: 'Failed to update stash',
|
|
||||||
appendErrorMessage: true,
|
|
||||||
}).finally(() => { done.value = true; });
|
|
||||||
|
|
||||||
emit('reload');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remove() {
|
|
||||||
if (done.value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to remove the stash '${props.stash.name}' and all its scenes, movies and actors? This is permament, it cannot be undone by you or staff!`)) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
await del(`/stashes/${props.stash.id}`, {
|
|
||||||
undoFeedback: `Stash '${props.stash.name}' removed`,
|
|
||||||
errorFeedback: 'Failed to remove stash',
|
|
||||||
appendErrorMessage: true,
|
|
||||||
}).finally(() => { done.value = true; });
|
|
||||||
|
|
||||||
emit('reload');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rename() {
|
|
||||||
if (done.value === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
done.value = false;
|
|
||||||
|
|
||||||
await patch(`/stashes/${props.stash.id}`, { name: stashName.value }, {
|
|
||||||
successFeedback: `Stash renamed to ${stashName.value}`,
|
|
||||||
errorFeedback: 'Failed to rename stash',
|
|
||||||
appendErrorMessage: true,
|
|
||||||
}).finally(() => { done.value = true; });
|
|
||||||
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
showRenameDialog.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stash {
|
.stash {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useId } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
photo: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
favicon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const entityTooltipId = useId();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
v-if="photo?.entity"
|
v-if="photo?.entity"
|
||||||
v-tooltip="photo.entity.name"
|
v-tooltip="{ content: photo.entity.name, ariaId: entityTooltipId }"
|
||||||
:to="`/${photo.entity.type}/${photo.entity.slug}`"
|
:to="`/${photo.entity.type}/${photo.entity.slug}`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -30,19 +47,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
photo: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
favicon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.album-logo {
|
.album-logo {
|
||||||
max-height: .75rem;
|
max-height: .75rem;
|
||||||
|
|||||||
@@ -1,3 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
import Campaign from '#/components/campaigns/campaign.vue';
|
||||||
|
import Logo from '#/components/tags/logo.vue';
|
||||||
|
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tag: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['load', 'campaign']);
|
||||||
|
|
||||||
|
const { campaigns } = inject('pageContext');
|
||||||
|
|
||||||
|
const photos = computed(() => {
|
||||||
|
/* sfw not currently implemented
|
||||||
|
if (props.tag.poster && this.$store.state.ui.sfw) {
|
||||||
|
return [props.tag.poster].concat(props.tag.photos).map((photo) => photo.sfw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.ui.sfw) {
|
||||||
|
return props.tag.photos.map((photo) => photo.sfw);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (props.tag.poster) {
|
||||||
|
return [props.tag.poster].concat(props.tag.photos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.tag.photos;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="photos nobar">
|
<div class="photos nobar">
|
||||||
<Campaign
|
<Campaign
|
||||||
@@ -40,44 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
|
|
||||||
import Logo from '#/components/tags/logo.vue';
|
|
||||||
import Campaign from '#/components/campaigns/campaign.vue';
|
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
tag: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['load', 'campaign']);
|
|
||||||
|
|
||||||
const { campaigns } = inject('pageContext');
|
|
||||||
|
|
||||||
const photos = computed(() => {
|
|
||||||
/* sfw not currently implemented
|
|
||||||
if (props.tag.poster && this.$store.state.ui.sfw) {
|
|
||||||
return [props.tag.poster].concat(props.tag.photos).map((photo) => photo.sfw);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$store.state.ui.sfw) {
|
|
||||||
return props.tag.photos.map((photo) => photo.sfw);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (props.tag.poster) {
|
|
||||||
return [props.tag.poster].concat(props.tag.photos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.tag.photos;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.photos {
|
.photos {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['tag']);
|
||||||
|
const dropdownId = useId();
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const tagNames = {
|
||||||
|
dp: 'double penetration',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTags = pageContext.pageProps.tagIds
|
||||||
|
? Object.entries(pageContext.pageProps.tagIds).map(([slug, id]) => ({
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
name: tagNames[slug] || slug,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags = ref(defaultTags);
|
||||||
|
const query = ref(null);
|
||||||
|
const queryInput = ref(null);
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
tags.value = await get('/tags', { query: query.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryInput.value?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDropdown
|
<VDropdown
|
||||||
|
:aria-id="dropdownId"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="trigger"
|
class="trigger"
|
||||||
@show="focus"
|
@show="focus"
|
||||||
@@ -30,49 +75,6 @@
|
|||||||
</VDropdown>
|
</VDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
|
|
||||||
const tagNames = {
|
|
||||||
dp: 'double penetration',
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultTags = pageContext.pageProps.tagIds
|
|
||||||
? Object.entries(pageContext.pageProps.tagIds).map(([slug, id]) => ({
|
|
||||||
id,
|
|
||||||
slug,
|
|
||||||
name: tagNames[slug] || slug,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const tags = ref(defaultTags);
|
|
||||||
const query = ref(null);
|
|
||||||
const queryInput = ref(null);
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['tag']);
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
tags.value = await get('/tags', { query: query.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
setTimeout(() => {
|
|
||||||
queryInput.value?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.trigger {
|
.trigger {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,49 +1,22 @@
|
|||||||
<template>
|
|
||||||
<div class="tooltip-container">
|
|
||||||
<div
|
|
||||||
ref="handle"
|
|
||||||
class="tooltip-handle"
|
|
||||||
@click.stop="toggle"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-show="opened"
|
|
||||||
class="tooltip-layer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="tooltip"
|
|
||||||
class="tooltip-wrapper"
|
|
||||||
:style="{ transform: `translate(${position.x}px, ${position.y}px)` }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="tooltip"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
<slot
|
|
||||||
name="tooltip"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
onMounted,
|
|
||||||
onBeforeUnmount,
|
|
||||||
nextTick,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
// import { useFloating } from '@floating-ui/vue';
|
// import { useFloating } from '@floating-ui/vue';
|
||||||
import { computePosition, shift } from '@floating-ui/dom';
|
import { computePosition, shift } from '@floating-ui/dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import events from '#/src/events.js';
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
});
|
||||||
const handle = ref(null);
|
const handle = ref(null);
|
||||||
const tooltip = ref(null);
|
const tooltip = ref(null);
|
||||||
const opened = ref(false);
|
const opened = ref(false);
|
||||||
@@ -53,13 +26,6 @@ const position = ref({
|
|||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
placement: {
|
|
||||||
type: String,
|
|
||||||
default: 'top',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function open() {
|
async function open() {
|
||||||
events.emit('blur');
|
events.emit('blur');
|
||||||
opened.value = true;
|
opened.value = true;
|
||||||
@@ -97,7 +63,8 @@ function close() {
|
|||||||
function toggle() {
|
function toggle() {
|
||||||
if (opened.value) {
|
if (opened.value) {
|
||||||
close();
|
close();
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +78,39 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tooltip-container">
|
||||||
|
<div
|
||||||
|
ref="handle"
|
||||||
|
class="tooltip-handle"
|
||||||
|
@click.stop="toggle"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="opened"
|
||||||
|
class="tooltip-layer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="tooltip"
|
||||||
|
class="tooltip-wrapper"
|
||||||
|
:style="{ transform: `translate(${position.x}px, ${position.y}px)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="tooltip"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="tooltip"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tooltip-layer {
|
.tooltip-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,3 +1,57 @@
|
|||||||
|
<script setup>
|
||||||
|
import { format, formatDistanceStrict } from 'date-fns';
|
||||||
|
import { inject, ref, useId } from 'vue';
|
||||||
|
|
||||||
|
import { del, get, post } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const now = pageContext.meta.now;
|
||||||
|
const user = pageContext.user;
|
||||||
|
const keys = ref(pageContext.pageProps.keys);
|
||||||
|
const newKey = ref(null);
|
||||||
|
const flushTooltipId = useId();
|
||||||
|
|
||||||
|
async function createKey() {
|
||||||
|
const key = await post('/me/keys', null, {
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
newKey.value = key;
|
||||||
|
keys.value = await get('/me/keys');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeKey(key) {
|
||||||
|
if (confirm(`Are you sure you want to remove API key '${key.identifier}'? It can not be restored.`)) { // eslint-disable-line no-alert
|
||||||
|
newKey.value = null;
|
||||||
|
|
||||||
|
await del(`/me/keys/${key.identifier}`);
|
||||||
|
|
||||||
|
keys.value = await get('/me/keys');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushKeys() {
|
||||||
|
if (confirm('Are you sure you want to remove ALL your API keys? They can not be restored.')) { // eslint-disable-line no-alert
|
||||||
|
newKey.value = null;
|
||||||
|
|
||||||
|
await del('/me/keys');
|
||||||
|
|
||||||
|
keys.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyKey(event) {
|
||||||
|
event.target.select();
|
||||||
|
navigator.clipboard.writeText(newKey.value.key);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Key copied to clipboard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="profile-section">
|
<section class="profile-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -5,7 +59,7 @@
|
|||||||
|
|
||||||
<div class="keys-actions">
|
<div class="keys-actions">
|
||||||
<Icon
|
<Icon
|
||||||
v-tooltip="'Flush all keys'"
|
v-tooltip="{ content: 'Flush all keys', ariaId: flushTooltipId }"
|
||||||
icon="stack-cancel"
|
icon="stack-cancel"
|
||||||
class="keys-flush"
|
class="keys-flush"
|
||||||
@click="flushKeys"
|
@click="flushKeys"
|
||||||
@@ -63,7 +117,7 @@
|
|||||||
<Icon icon="plus-circle" />
|
<Icon icon="plus-circle" />
|
||||||
|
|
||||||
<time
|
<time
|
||||||
v-tooltip="`Created ${format(key.createdAt, 'yyyy-MM-dd hh:mm:ss')}`"
|
v-tooltip="{ content: `Created ${format(key.createdAt, 'yyyy-MM-dd hh:mm:ss')}`, ariaId: `key-created-${key.id}` }"
|
||||||
:datetime="key.createdAt.toISOString()"
|
:datetime="key.createdAt.toISOString()"
|
||||||
>{{ formatDistanceStrict(key.createdAt, now) }} ago</time>
|
>{{ formatDistanceStrict(key.createdAt, now) }} ago</time>
|
||||||
</span>
|
</span>
|
||||||
@@ -73,7 +127,7 @@
|
|||||||
|
|
||||||
<template v-if="key.lastUsedAt">
|
<template v-if="key.lastUsedAt">
|
||||||
<time
|
<time
|
||||||
v-tooltip="`Last used ${format(key.lastUsedAt, 'yyyy-MM-dd hh:mm:ss')} from IP ${key.lastUsedIp}`"
|
v-tooltip="{ content: `Last used ${format(key.lastUsedAt, 'yyyy-MM-dd hh:mm:ss')} from IP ${key.lastUsedIp}`, ariaId: `key-used-${key.id}` }"
|
||||||
:datetime="key.lastUsedAt.toISOString()"
|
:datetime="key.lastUsedAt.toISOString()"
|
||||||
>{{ formatDistanceStrict(key.lastUsedAt, now) }} ago</time>
|
>{{ formatDistanceStrict(key.lastUsedAt, now) }} ago</time>
|
||||||
</template>
|
</template>
|
||||||
@@ -99,59 +153,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject } from 'vue';
|
|
||||||
import { format, formatDistanceStrict } from 'date-fns';
|
|
||||||
|
|
||||||
import { get, post, del } from '#/src/api.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const now = pageContext.meta.now;
|
|
||||||
const user = pageContext.user;
|
|
||||||
const keys = ref(pageContext.pageProps.keys);
|
|
||||||
const newKey = ref(null);
|
|
||||||
|
|
||||||
async function createKey() {
|
|
||||||
const key = await post('/me/keys', null, {
|
|
||||||
appendErrorMessage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
newKey.value = key;
|
|
||||||
keys.value = await get('/me/keys');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeKey(key) {
|
|
||||||
if (confirm(`Are you sure you want to remove API key '${key.identifier}'? It can not be restored.`)) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
newKey.value = null;
|
|
||||||
|
|
||||||
await del(`/me/keys/${key.identifier}`);
|
|
||||||
|
|
||||||
keys.value = await get('/me/keys');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function flushKeys() {
|
|
||||||
if (confirm('Are you sure you want to remove ALL your API keys? They can not be restored.')) { // eslint-disable-line no-restricted-globals, no-alert
|
|
||||||
newKey.value = null;
|
|
||||||
|
|
||||||
await del('/me/keys');
|
|
||||||
|
|
||||||
keys.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyKey(event) {
|
|
||||||
event.target.select();
|
|
||||||
navigator.clipboard.writeText(newKey.value.key);
|
|
||||||
|
|
||||||
events.emit('feedback', {
|
|
||||||
type: 'success',
|
|
||||||
message: 'Key copied to clipboard',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.keys-header {
|
.keys-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
<template>
|
|
||||||
<video
|
|
||||||
ref="player"
|
|
||||||
:poster="poster"
|
|
||||||
class="player video-js vjs-big-play-centered"
|
|
||||||
@playing="emit('play')"
|
|
||||||
@pause="emit('pause')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted } from 'vue';
|
|
||||||
import videoJs from 'video.js';
|
import videoJs from 'video.js';
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
// import 'videojs-vr/dist/videojs-vr.min';
|
// import 'videojs-vr/dist/videojs-vr.min';
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
@@ -66,6 +56,16 @@ watch(() => props.video, () => updatePlayer());
|
|||||||
watch(() => props.poster, () => updatePlayer());
|
watch(() => props.poster, () => updatePlayer());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<video
|
||||||
|
ref="player"
|
||||||
|
:poster="poster"
|
||||||
|
class="player video-js vjs-big-play-centered"
|
||||||
|
@playing="emit('play')"
|
||||||
|
@pause="emit('pause')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.player.video-js {
|
.player.video-js {
|
||||||
.vjs-poster {
|
.vjs-poster {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ module.exports = {
|
|||||||
login: true,
|
login: true,
|
||||||
signup: true,
|
signup: true,
|
||||||
usernameLength: [2, 24],
|
usernameLength: [2, 24],
|
||||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
usernamePattern: /^[\w-]+$/,
|
||||||
captcha: {
|
captcha: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||||
@@ -178,8 +178,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
stashes: {
|
stashes: {
|
||||||
nameLength: [2, 24],
|
nameLength: [2, 24],
|
||||||
namePattern: /^[a-zA-Z0-9!?$&\s_-]+$/,
|
namePattern: /^[\w!?$&\s-]+$/,
|
||||||
viewRefreshCron: '* * * * *', // every minute
|
viewRefreshCrontab: '* * * * *', // every minute
|
||||||
viewRefreshCooldowns: {
|
viewRefreshCooldowns: {
|
||||||
actors: 60, // minutes
|
actors: 60, // minutes
|
||||||
stashes: 1,
|
stashes: 1,
|
||||||
@@ -192,4 +192,8 @@ module.exports = {
|
|||||||
s3Path: 'https://s3.wasabisys.com',
|
s3Path: 'https://s3.wasabisys.com',
|
||||||
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos
|
videoRestrictions: [], // entity slugs, _ prefix for networks, hides trailer and teaser videos
|
||||||
},
|
},
|
||||||
|
sync: {
|
||||||
|
enabled: true,
|
||||||
|
crontab: '*/10 * * * *', // every 10 minutes
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
50
eslint.config.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import antfu from '@antfu/eslint-config';
|
||||||
|
|
||||||
|
export default antfu(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'common/**',
|
||||||
|
'static/**',
|
||||||
|
],
|
||||||
|
vue: true,
|
||||||
|
// jsonc/yaml/toml stay off: assets/*.yaml are data files, not source, and weren't linted before.
|
||||||
|
jsonc: false,
|
||||||
|
yaml: false,
|
||||||
|
toml: false,
|
||||||
|
stylistic: {
|
||||||
|
indent: 'tab',
|
||||||
|
quotes: 'single',
|
||||||
|
semi: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
CONFIG: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'default-param-last': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'style/arrow-parens': ['error', 'always'],
|
||||||
|
'no-param-reassign': ['error', {
|
||||||
|
props: true,
|
||||||
|
ignorePropertyModificationsFor: ['state', 'acc', 'req'],
|
||||||
|
}],
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'unused-imports/no-unused-vars': ['error', {
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true,
|
||||||
|
caughtErrors: 'none',
|
||||||
|
}],
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/no-reserved-component-names': 'off',
|
||||||
|
'vue/multiline-html-element-content-newline': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||