Compare commits
1716 Commits
33ddfe7c46
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33a327a04b | ||
|
|
a46061e247 | ||
|
|
94e07ff23d | ||
|
|
4811befcf6 | ||
|
|
c455f02c66 | ||
|
|
efc5620a28 | ||
|
|
61123fdb6a | ||
|
|
3ec6911d46 | ||
|
|
2021093645 | ||
|
|
1c72dc202f | ||
|
|
1ef946fa77 | ||
|
|
3b6bbc39ff | ||
|
|
481c9feada | ||
|
|
953b3e9568 | ||
|
|
bdd2e68f49 | ||
|
|
e4cc349302 | ||
|
|
6547b93e55 | ||
|
|
bb9649d23b | ||
|
|
9e2eaef9d1 | ||
|
|
1c3ee75d3b | ||
|
|
15c9af8057 | ||
|
|
295573c1ef | ||
|
|
e93e8ace5c | ||
|
|
43af7ba777 | ||
|
|
0dad5b0d68 | ||
|
|
ae9b793318 | ||
|
|
fd8170f223 | ||
|
|
661b8b716b | ||
|
|
5ff076cac3 | ||
|
|
41c100ac4e | ||
|
|
c6e977f842 | ||
|
|
50b7f521b5 | ||
|
|
f0d0ee3acc | ||
|
|
7b3bdadd44 | ||
|
|
5deba6b90f | ||
|
|
a5afffc968 | ||
|
|
a239a5c593 | ||
|
|
e56e7333e3 | ||
|
|
d55e3c37cd | ||
|
|
97b78ea016 | ||
|
|
3e290b74dc | ||
|
|
65141207ae | ||
|
|
17dfeac1af | ||
|
|
4a9c428d69 | ||
|
|
333f252099 | ||
|
|
38232f258a | ||
|
|
582269cfaa | ||
|
|
0b646429fd | ||
|
|
fecef6c1cf | ||
|
|
73e5404c44 | ||
|
|
690d2bb3ed | ||
|
|
1dd935e1e9 | ||
|
|
8386230f33 | ||
|
|
5a68b06137 | ||
|
|
5918364cf5 | ||
|
|
df4d860d35 | ||
|
|
d9f0db6e3c | ||
|
|
7372b560b2 | ||
|
|
9d638c685c | ||
|
|
5da1acc38d | ||
|
|
c7c62e220d | ||
|
|
9edd652a2c | ||
|
|
cde760c1ea | ||
|
|
19c7f958e1 | ||
|
|
c7710afdbf | ||
|
|
ba18a3dadb | ||
|
|
496c29e569 | ||
|
|
98a72a4929 | ||
|
|
c4f0b48932 | ||
|
|
93abbab873 | ||
|
|
f7bbab08bd | ||
|
|
1e4ddf2bbf | ||
|
|
9b7879bff7 | ||
|
|
03d96d4dec | ||
|
|
1dfa034332 | ||
|
|
196a1d33e3 | ||
|
|
3d349c413f | ||
|
|
bd5babd37e | ||
|
|
fffe9080f3 | ||
|
|
5630b16e99 | ||
|
|
4bcf7ef45b | ||
|
|
9abc1d48ac | ||
|
|
ceaf5a3217 | ||
|
|
36d84fb98b | ||
|
|
3d80e9d77b | ||
|
|
1dc38ffacc | ||
|
|
ee210b5c68 | ||
|
|
3aae3cd3a1 | ||
|
|
f1a7f2f905 | ||
|
|
0b7e0319f1 | ||
|
|
10b8abf706 | ||
|
|
b163223ff1 | ||
|
|
b9f3eb85f9 | ||
|
|
285a65f018 | ||
|
|
815d56d334 | ||
|
|
27a9a233e2 | ||
|
|
22864105ac | ||
|
|
7cf47cbf8d | ||
|
|
502c588958 | ||
|
|
81792a7c2f | ||
|
|
784d326584 | ||
|
|
83dc7aa578 | ||
|
|
5a3b27fbf7 | ||
|
|
5ff83a9497 | ||
|
|
07cb39c756 | ||
|
|
fffd54995d | ||
|
|
5302176f0b | ||
|
|
efd7cf8acc | ||
|
|
52218c30f8 | ||
|
|
04c176fa8b | ||
|
|
af320684b4 | ||
|
|
b2ba14ce45 | ||
|
|
d776cc2f71 | ||
|
|
b4bed2f623 | ||
|
|
5c5a11fdca | ||
|
|
a22c62d893 | ||
|
|
90bc2f3724 | ||
|
|
8cdc794a3a | ||
|
|
250618df8b | ||
|
|
b62ae00c54 | ||
|
|
67d0a9e0e0 | ||
| b4425bc3bb | |||
| d77c145de3 | |||
|
|
6753f923d9 | ||
|
|
452f725332 | ||
|
|
7c1f45bcfb | ||
|
|
11ea54f6e8 | ||
|
|
5b623ee46f | ||
|
|
7ceec1c739 | ||
|
|
fc318536aa | ||
|
|
cd8f690ac6 | ||
|
|
240a8e86fd | ||
|
|
c557f0f1f0 | ||
|
|
86f56695e2 | ||
|
|
20da2d1cf6 | ||
|
|
372db86927 | ||
|
|
4c503a3177 | ||
|
|
57d46f5842 | ||
|
|
7f11d0ca91 | ||
|
|
bbd788699e | ||
|
|
e24add98ea | ||
|
|
506971b44b | ||
|
|
43a0bc8a2c | ||
|
|
baebdbc0bb | ||
|
|
1c24cae3b1 | ||
|
|
b9146aee5f | ||
|
|
09bfcb01f5 | ||
|
|
5d80b6dc21 | ||
|
|
140a38c349 | ||
|
|
6760c11881 | ||
|
|
2dcdbf9c37 | ||
|
|
3b958a02ef | ||
|
|
605242b399 | ||
|
|
299f257a01 | ||
|
|
0ad64ef5f4 | ||
|
|
3bfe1e8fdb | ||
|
|
7d992f4129 | ||
|
|
280032937f | ||
|
|
d93670842b | ||
|
|
dc187a9a3a | ||
|
|
5f89c6e14c | ||
|
|
9f10420eb9 | ||
|
|
9568370132 | ||
|
|
1c0c30be11 | ||
|
|
cdb7dcd6e9 | ||
|
|
8e4be06675 | ||
|
|
de7a8c756a | ||
|
|
65e2b72c6a | ||
|
|
523c36ecd4 | ||
|
|
5b5688438c | ||
|
|
4ca07631c7 | ||
|
|
ec4e7bc12a | ||
|
|
26d15c0a6f | ||
|
|
a484396db4 | ||
|
|
681db02784 | ||
|
|
e88554666c | ||
|
|
9f37ec4cff | ||
|
|
abd063a578 | ||
|
|
883e57ce1f | ||
|
|
ad04502a8c | ||
|
|
f65ed16842 | ||
|
|
1918dd4ad5 | ||
|
|
3983d3464f | ||
|
|
61daf5f037 | ||
|
|
eafa144104 | ||
|
|
60c151ff6a | ||
|
|
c97d18fcf9 | ||
|
|
e41f9fa937 | ||
|
|
56a7fb0ad9 | ||
|
|
e29cbc9fea | ||
|
|
08f725a0b6 | ||
|
|
b93a5715cb | ||
|
|
2018d5c298 | ||
|
|
87094a9498 | ||
|
|
53a1c697d0 | ||
|
|
f04eb72891 | ||
|
|
dfeb0c08cf | ||
|
|
9d7183ac69 | ||
|
|
2539e88f47 | ||
|
|
20d0d860d3 | ||
|
|
6b4aa64d74 | ||
|
|
c7b9ec7a4b | ||
|
|
e81864ebde | ||
|
|
0305a22a24 | ||
|
|
a406eb584d | ||
|
|
2be3ea9bbb | ||
|
|
0a8a7ff9a5 | ||
|
|
98c103c625 | ||
|
|
e0908a5f5e | ||
|
|
f0b7678444 | ||
|
|
64e9efe095 | ||
|
|
ab29ab053e | ||
|
|
a5081bc7c9 | ||
|
|
da9c628f9b | ||
|
|
26539b74a5 | ||
|
|
ccb99e278c | ||
|
|
a867817dc1 | ||
|
|
d1e05915b5 | ||
|
|
b764fdec85 | ||
|
|
8322d43b09 | ||
|
|
29b8c5e38e | ||
|
|
83dd233991 | ||
|
|
0864154a0e | ||
|
|
53357d4bd2 | ||
|
|
69bf98edf1 | ||
|
|
a22c4d5679 | ||
|
|
e5ad1648eb | ||
|
|
100a35b4e8 | ||
|
|
6c5d4389fe | ||
|
|
92f9ff4104 | ||
|
|
0e4fd12d70 | ||
|
|
9040285ce5 | ||
|
|
972b15e72d | ||
|
|
c1aea78496 | ||
|
|
49f891ba44 | ||
|
|
239057f1b6 | ||
|
|
167df35d37 | ||
|
|
b1b2ad2111 | ||
|
|
f1ff662be2 | ||
|
|
2418fec9c9 | ||
|
|
aaaa0a6afb | ||
|
|
afe22003e1 | ||
|
|
b6ad2903f3 | ||
|
|
6c298cd639 | ||
|
|
dbff3e9539 | ||
|
|
fcc6b33d07 | ||
|
|
0224c26ca0 | ||
|
|
294f15e694 | ||
|
|
522584711b | ||
|
|
ae40f33283 | ||
|
|
172e01c80e | ||
|
|
901acf1390 | ||
|
|
a316da3083 | ||
|
|
0b46e27497 | ||
|
|
b164c5dad7 | ||
|
|
2d6b285817 | ||
|
|
65c79567d2 | ||
|
|
d3633f31ac | ||
|
|
73a9a11aa6 | ||
|
|
c21bd8b01e | ||
|
|
14f4d24872 | ||
|
|
731abc79ee | ||
|
|
d542889827 | ||
|
|
8c5ef21459 | ||
|
|
7bfca9596d | ||
|
|
7bd858f96b | ||
|
|
b8657cb6e6 | ||
|
|
69a7e8f13d | ||
|
|
23537e0243 | ||
|
|
546f778b1b | ||
|
|
245f69a122 | ||
|
|
c6f3f7a239 | ||
|
|
f0a6e80e5a | ||
|
|
0cbb985945 | ||
|
|
bb384029ac | ||
|
|
85c6b581cb | ||
|
|
dd18644796 | ||
|
|
1628e41d09 | ||
|
|
a77d8f4cea | ||
|
|
0a4152b4b7 | ||
|
|
3fc4dc15df | ||
|
|
4ee0dcef9b | ||
|
|
011482ac9d | ||
|
|
6a8c9d89cb | ||
|
|
e0905ab8fc | ||
|
|
eb1f8f86fd | ||
|
|
959b5d9d0e | ||
|
|
0c19a026ef | ||
|
|
b24973eb19 | ||
|
|
4b18867883 | ||
|
|
5e292a0880 | ||
|
|
f00e37490c | ||
|
|
715e44cf21 | ||
|
|
25d1c1b229 | ||
|
|
094226eeb5 | ||
|
|
b08cb46ae5 | ||
|
|
6b88cf1040 | ||
|
|
aacfd1b29d | ||
|
|
0427e1e276 | ||
|
|
ee33bd8e63 | ||
|
|
25b8bd689e | ||
|
|
b6468b03a7 | ||
|
|
a848d6991b | ||
|
|
65c3053b49 | ||
|
|
1d686d7e40 | ||
|
|
488d1082e4 | ||
|
|
8967907893 | ||
|
|
e527a67dc1 | ||
|
|
6847ef690c | ||
|
|
96a2125248 | ||
|
|
c5e4310a6b | ||
|
|
23b41fc4f3 | ||
|
|
4fb41a4c35 | ||
|
|
12c0e8e828 | ||
|
|
dce27e985e | ||
|
|
0a343dfa98 | ||
|
|
729ca0f968 | ||
|
|
385dfb9f75 | ||
|
|
afbae24f43 | ||
|
|
eb7009832a | ||
|
|
d1480da076 | ||
|
|
10a2731caf | ||
|
|
ab1329dd67 | ||
|
|
e9a0700742 | ||
|
|
3f473589ad | ||
|
|
7a44c7aaaa | ||
|
|
bed329cd8c | ||
|
|
011bb4efa3 | ||
|
|
c979173422 | ||
|
|
42791c528e | ||
|
|
c76c8054b9 | ||
|
|
6107c7d0ef | ||
|
|
d0d045a2ab | ||
|
|
102e053021 | ||
|
|
ca0660c1cc | ||
|
|
a4a05232db | ||
|
|
0f8d5d4456 | ||
|
|
478a2c4b48 | ||
|
|
55e240e68d | ||
|
|
71b25774d0 | ||
|
|
1697728b2a | ||
|
|
4242efbd4c | ||
|
|
0b825a61bb | ||
|
|
7d974e6b89 | ||
|
|
ae1b9c0d73 | ||
|
|
91ebcace0a | ||
|
|
846b860c06 | ||
|
|
83ed793e39 | ||
|
|
3f55b90ab8 | ||
|
|
4806b0aa41 | ||
|
|
3b91493995 | ||
|
|
8bf9fff7dc | ||
|
|
eed563e06f | ||
|
|
fc1c2fc2f3 | ||
|
|
f8a3bf6a64 | ||
|
|
c5e74c33b7 | ||
|
|
95f3b1c03a | ||
|
|
0773a8019c | ||
|
|
1116e09af5 | ||
|
|
52e215d3bc | ||
|
|
7f25846d55 | ||
|
|
d36e52d5d1 | ||
|
|
da0cbced15 | ||
|
|
837fc98ad2 | ||
|
|
a0f41da80a | ||
|
|
4a2d2ad996 | ||
|
|
010da8954b | ||
|
|
e643e0a924 | ||
|
|
c386a9098f | ||
|
|
d17dbf1b36 | ||
|
|
3961f83ef6 | ||
|
|
d0648b5006 | ||
|
|
337b6c70f4 | ||
|
|
2cd007dae3 | ||
|
|
43e55446f7 | ||
|
|
f47be86df3 | ||
|
|
d5bf253011 | ||
|
|
093d447328 | ||
|
|
e12de5ec00 | ||
|
|
a64b25eb51 | ||
|
|
bbf058480f | ||
|
|
b2105c8fb0 | ||
|
|
8ff5a8c5e1 | ||
|
|
98624c9954 | ||
|
|
3b9e8e3cc3 | ||
|
|
193af9bab5 | ||
|
|
b2ad031c54 | ||
|
|
0419cc633b | ||
|
|
63143fb185 | ||
|
|
3f6ecfd92c | ||
|
|
a208c922f7 | ||
|
|
757d554e7b | ||
|
|
74afc55dc6 | ||
|
|
29b0451608 | ||
|
|
cfa1ed7a61 | ||
|
|
c70f500acc | ||
|
|
76a5ccf3f6 | ||
|
|
35c28dede2 | ||
|
|
11e043ca2e | ||
|
|
7ac64c57ae | ||
|
|
9ff70e5578 | ||
|
|
348aa91832 | ||
|
|
de5d104e1e | ||
|
|
565cf551f0 | ||
|
|
819d53fc2b | ||
|
|
67f22a6e08 | ||
|
|
eee47111a6 | ||
|
|
07643870cd | ||
|
|
bb949e0a3b | ||
|
|
a7cf3f689e | ||
|
|
d4919016b6 | ||
|
|
67af9f2ea2 | ||
|
|
bb9d6ee8fc | ||
|
|
e88cf4e3f4 | ||
|
|
5577e4fee5 | ||
|
|
489d253a48 | ||
|
|
06e6d3940b | ||
|
|
42a4fe581f | ||
|
|
292faa1e48 | ||
|
|
4bc6ff846d | ||
|
|
011f10fba8 | ||
|
|
d0e987a2aa | ||
|
|
6e8af52237 | ||
|
|
e301e2184c | ||
|
|
3b3f4a1f2d | ||
|
|
c8ac8d6564 | ||
|
|
626cbc4fc5 | ||
|
|
fe2004b3da | ||
|
|
fba4cbfb7b | ||
|
|
731a2792c5 | ||
|
|
f3d55806d1 | ||
|
|
f0265c2f5d | ||
|
|
cc27f202af | ||
|
|
41261adc76 | ||
|
|
1a9ded19c5 | ||
|
|
862a29bb6e | ||
|
|
0a92586c53 | ||
|
|
90b3d8a4d6 | ||
|
|
5a2e93e900 | ||
|
|
4e81a8a1d6 | ||
|
|
83d3621441 | ||
|
|
336b91c872 | ||
|
|
36a8adbd8c | ||
|
|
586ff6d4bd | ||
|
|
6fef87b0f1 | ||
|
|
0d7a03f3e5 | ||
|
|
1703e9a541 | ||
|
|
ece9569d66 | ||
|
|
3bebf5bf51 | ||
|
|
398161b03b | ||
|
|
1fb7d384fb | ||
|
|
7c7b38e869 | ||
|
|
8e06d465cb | ||
|
|
a0be8f0aa3 | ||
|
|
41d06f7e9d | ||
|
|
cb447da7d0 | ||
|
|
77b40817f2 | ||
|
|
e371e9725a | ||
|
|
816529b0ca | ||
|
|
99cfd3dc3f | ||
|
|
7c4b9063a7 | ||
|
|
e2e29a8dbb | ||
|
|
66ffa420f8 | ||
|
|
c33f193a0c | ||
|
|
00c06778ef | ||
|
|
1aab492f38 | ||
|
|
7f53f585c0 | ||
|
|
5db5d0c7ed | ||
|
|
74fcd24a8d | ||
|
|
6b8ed89566 | ||
|
|
0c115e78e2 | ||
|
|
c7b5611d68 | ||
|
|
18684a16eb | ||
|
|
a71752b18b | ||
|
|
6a9d725633 | ||
|
|
63ecd3b568 | ||
|
|
1f9963075c | ||
|
|
d60da3c99e | ||
|
|
f91437e03c | ||
|
|
44523609c1 | ||
|
|
de460f53b1 | ||
|
|
a275d0c855 | ||
|
|
35cd449e79 | ||
|
|
e67f029d53 | ||
|
|
3389dddd08 | ||
|
|
7522404abb | ||
|
|
44bb9b33d9 | ||
|
|
4a9adbf588 | ||
|
|
17e6f5a5da | ||
|
|
9ae113ab92 | ||
|
|
cd93615b39 | ||
|
|
fa30fe5169 | ||
|
|
0c98df232e | ||
|
|
254e933740 | ||
|
|
ed7bffd2d6 | ||
|
|
721e6494cf | ||
|
|
96f9c8f01d | ||
|
|
e9ed23abe4 | ||
|
|
4a963885bc | ||
|
|
5c6b5a0668 | ||
|
|
fc6de64311 | ||
|
|
d94e0ac8fc | ||
|
|
6742bf7d48 | ||
|
|
2cda689b3c | ||
|
|
7ae55db7f4 | ||
|
|
4d49737536 | ||
|
|
8ac8e21d78 | ||
|
|
21ec821b8c | ||
|
|
74e33303ed | ||
|
|
cdb47066cc | ||
|
|
cf4978b37f | ||
|
|
abe56d1207 | ||
|
|
370f0e784c | ||
|
|
780993eb63 | ||
|
|
9909bbeba5 | ||
|
|
c2779658c6 | ||
|
|
2f401765b6 | ||
|
|
0124561686 | ||
|
|
07bc49604e | ||
|
|
25ec52f53e | ||
|
|
e3b1934653 | ||
|
|
4e6f19d620 | ||
|
|
408066aba0 | ||
|
|
d6bebd8fec | ||
|
|
b64c7ddc96 | ||
|
|
c2ec4c15e3 | ||
|
|
357b0287b2 | ||
|
|
eca24a7c65 | ||
|
|
46a3906bde | ||
|
|
90ca08d8ac | ||
|
|
8eebcae85d | ||
|
|
e095d8317b | ||
|
|
c2a008afbe | ||
|
|
a45c5f8f37 | ||
|
|
e55bc1ef33 | ||
|
|
e60153ad01 | ||
|
|
cdea877024 | ||
|
|
2deed3a7eb | ||
|
|
162e5c2181 | ||
|
|
bb20659934 | ||
|
|
0eba0461c9 | ||
|
|
f018735052 | ||
|
|
0265ad35c9 | ||
|
|
4ca6c37cc8 | ||
|
|
5c028e75a7 | ||
|
|
34608fe0d7 | ||
|
|
772fef5ff8 | ||
|
|
8641651a55 | ||
|
|
20129eca5d | ||
|
|
16a5d92efe | ||
|
|
bb9fbc77a9 | ||
|
|
3310236767 | ||
|
|
1b3bf01ed7 | ||
|
|
85372581bd | ||
|
|
b4129891dc | ||
|
|
342d8da29b | ||
|
|
887bc003e6 | ||
|
|
2b76dcd15f | ||
|
|
1eb5451060 | ||
|
|
7818328378 | ||
|
|
3ad1f3d1e3 | ||
|
|
312c8903e4 | ||
|
|
fe59b23c84 | ||
|
|
07201c6563 | ||
|
|
6b2a66cf72 | ||
|
|
fe3f820d33 | ||
|
|
fb2217a733 | ||
|
|
af131f903e | ||
|
|
736a15958a | ||
|
|
ff862dbff9 | ||
|
|
fae288633c | ||
|
|
f44cb8bf4c | ||
|
|
c32a5d2930 | ||
|
|
2775b593ab | ||
|
|
50627f08b4 | ||
|
|
e6e163b733 | ||
|
|
39d8b0a17f | ||
|
|
29765f488b | ||
|
|
44c12a0654 | ||
|
|
2fa48abb62 | ||
|
|
2b5aac7633 | ||
|
|
c1829c64c2 | ||
|
|
e9603ecec9 | ||
|
|
37e39dc1ec | ||
|
|
9a65d8c0eb | ||
|
|
c1c58f659d | ||
|
|
b79e75349c | ||
|
|
c0347eed50 | ||
|
|
8a739893ea | ||
|
|
a39750d808 | ||
|
|
f310fec869 | ||
|
|
39e2abd80a | ||
|
|
3ad9a359f4 | ||
|
|
5e2909c531 | ||
|
|
58c01bdfcf | ||
|
|
c51cd080fa | ||
|
|
3469da674a | ||
|
|
67055bf920 | ||
|
|
b26a029f66 | ||
|
|
8a7baa02c1 | ||
|
|
e39afa8b39 | ||
|
|
372b5da704 | ||
|
|
f79505f3f6 | ||
|
|
4c306effb7 | ||
|
|
fbbbd99d3d | ||
|
|
24ea7e0c5c | ||
|
|
d80dd67ad0 | ||
|
|
a14227b588 | ||
|
|
42a2fd8800 | ||
|
|
098d2ef693 | ||
|
|
9aca5baa2b | ||
|
|
5ba2c0ebd3 | ||
|
|
23335f8bd9 | ||
|
|
b6022a3e90 | ||
|
|
b6b3def8fa | ||
|
|
6e2527e5c5 | ||
|
|
7ff222ce25 | ||
|
|
62ad786318 | ||
|
|
a6c7c60290 | ||
|
|
acc496be47 | ||
|
|
a04c7dda37 | ||
|
|
38f53d8de8 | ||
|
|
dcad0cbe8f | ||
|
|
fa57575cc3 | ||
|
|
73b28866ac | ||
|
|
998774fe5c | ||
|
|
0827ced0d7 | ||
|
|
1cf0e166bb | ||
|
|
df71bfb483 | ||
|
|
80b8fe3654 | ||
|
|
dbfbd1f04d | ||
|
|
b94a3e05bc | ||
|
|
368aa8a89f | ||
|
|
80fa953f60 | ||
|
|
a95a159978 | ||
|
|
6be787ecae | ||
|
|
7b482e6903 | ||
|
|
2db0cab892 | ||
|
|
457256e731 | ||
|
|
8aaf060979 | ||
|
|
688863d16e | ||
|
|
c9b774c498 | ||
|
|
3e813ca251 | ||
|
|
647e9bb186 | ||
|
|
28a68aa721 | ||
|
|
e0e584658e | ||
|
|
980f66fb33 | ||
|
|
3e0a4406eb | ||
|
|
5906ed5948 | ||
|
|
8902654c4e | ||
|
|
15cc970ee2 | ||
|
|
8af562e284 | ||
|
|
101f5a1453 | ||
|
|
4ae130a646 | ||
|
|
bd59bdd215 | ||
|
|
054ea6ac66 | ||
|
|
c5e1f2de2e | ||
|
|
4abfcaf9ce | ||
|
|
0b14f4ab5d | ||
|
|
5e12a1e1b1 | ||
|
|
456b240df8 | ||
|
|
f217b161b4 | ||
|
|
4594dbc763 | ||
|
|
98cae9270a | ||
|
|
91ba916884 | ||
|
|
315bf0fc89 | ||
|
|
733d17ae7a | ||
|
|
ff123b99b7 | ||
|
|
0b99e72924 | ||
|
|
2da5939648 | ||
|
|
79b51eca67 | ||
|
|
cd417f40a8 | ||
|
|
85d42dec03 | ||
|
|
8dabef57b6 | ||
|
|
2a51fc82fd | ||
|
|
11ad5f8bad | ||
|
|
e3dc989798 | ||
|
|
8ea3fccb61 | ||
|
|
97cfca74ad | ||
|
|
0c2e45141d | ||
|
|
824fb9ef37 | ||
|
|
a19f235684 | ||
|
|
6d93083581 | ||
|
|
8337ce8dbd | ||
|
|
92ba264cff | ||
|
|
4db9a34a34 | ||
|
|
251c3964bd | ||
|
|
1e43c0e4c3 | ||
|
|
696eb9a9d0 | ||
|
|
45badad8f8 | ||
|
|
2bd02a80a6 | ||
|
|
133aa065fb | ||
|
|
b856f81148 | ||
|
|
5988e9b1a8 | ||
|
|
0b7d7e0602 | ||
|
|
dc331637a5 | ||
|
|
3bbc5a5e87 | ||
|
|
55cadcfe82 | ||
|
|
c1124abde0 | ||
|
|
fddbafc2d5 | ||
|
|
63f43013c3 | ||
|
|
e4e0eb23dd | ||
|
|
89a729924d | ||
|
|
6364912aa8 | ||
|
|
ab83dd2e55 | ||
|
|
d5cdfb36a9 | ||
|
|
46c0b269c3 | ||
|
|
4b5cd50122 | ||
|
|
aade7490f8 | ||
|
|
97c088cfb4 | ||
|
|
bfb5006e95 | ||
|
|
d3d08b9c21 | ||
|
|
9535ab7953 | ||
|
|
b42bdc1d3e | ||
|
|
89956f3ad5 | ||
|
|
b58ceb85bc | ||
|
|
6938e88fbf | ||
|
|
1fc67704dc | ||
|
|
b7aaeada45 | ||
|
|
4d89256a4c | ||
|
|
7185c8dc08 | ||
|
|
b506a00e7d | ||
|
|
5a975ad0bf | ||
|
|
b80eca35d8 | ||
|
|
dba99a4170 | ||
|
|
5547ff7e76 | ||
|
|
dc98fcad5a | ||
|
|
41259eae5d | ||
|
|
d3703d81b7 | ||
|
|
24178e7b04 | ||
|
|
486dbc5613 | ||
|
|
e9cbf5dab2 | ||
|
|
b719a166d2 | ||
|
|
297f79f6e2 | ||
|
|
af59ad3d33 | ||
|
|
3573b84c97 | ||
|
|
e105d665ae | ||
|
|
714f70c9ce | ||
|
|
4151412156 | ||
|
|
f7f9862489 | ||
|
|
7e52f6d18d | ||
|
|
e1b52de7a3 | ||
|
|
0a0a3ddd7b | ||
|
|
fdb48f0d6d | ||
|
|
2f3eb0e16c | ||
|
|
18a1d74a9a | ||
|
|
0f29151200 | ||
|
|
0a7378feb4 | ||
|
|
59ba84b7b1 | ||
|
|
985b523031 | ||
|
|
40e0c92ec7 | ||
|
|
bd9b795516 | ||
|
|
130368ca70 | ||
|
|
611a2d66a0 | ||
|
|
b16cc26024 | ||
|
|
3babb9ee68 | ||
|
|
9388eb5993 | ||
|
|
0b1f5c06bc | ||
|
|
1c1b933438 | ||
|
|
0663109634 | ||
|
|
c329ffdf07 | ||
|
|
b89fe1805f | ||
|
|
ca8429150e | ||
|
|
a95e409366 | ||
|
|
f39270ee91 | ||
|
|
9debb1776b | ||
|
|
8183525961 | ||
|
|
6e8620fbbb | ||
|
|
5c8a6b3a70 | ||
|
|
46173cb6c3 | ||
|
|
bebf814577 | ||
|
|
7d1fb86e63 | ||
|
|
f9c6e6f0dc | ||
|
|
1238f90268 | ||
|
|
1dea94c0cf | ||
|
|
dd5284c55a | ||
|
|
8bc1fbf530 | ||
|
|
48f247a919 | ||
|
|
251bb9476d | ||
|
|
8387f676fc | ||
|
|
e1aa48f3c1 | ||
|
|
e3ef0a0d69 | ||
|
|
b9e4764516 | ||
|
|
7e78a39717 | ||
|
|
b8df8e6507 | ||
|
|
451ffdc48b | ||
|
|
4bbd9e8120 | ||
|
|
12c3f0d7b9 | ||
|
|
dbdf0fcc0b | ||
|
|
422b6c4252 | ||
|
|
39d149c728 | ||
|
|
cc64f2911f | ||
|
|
6fb8c77846 | ||
|
|
2bae6f693e | ||
|
|
8d8cdcd219 | ||
|
|
ae0efccb04 | ||
|
|
ef1d34e4de | ||
|
|
cdd4220cb9 | ||
|
|
bf814c0b9d | ||
|
|
f95c80f73f | ||
|
|
5d3e3b6cee | ||
|
|
52356f0f31 | ||
|
|
68aacb498a | ||
|
|
ad73c11cb4 | ||
|
|
e65828c729 | ||
|
|
db4e74fb99 | ||
|
|
e38922f372 | ||
|
|
e1d6c9e489 | ||
|
|
d2d124cccf | ||
|
|
9ca2ec6dd0 | ||
|
|
fd92e7e260 | ||
|
|
150988ecb9 | ||
|
|
bd3e1a0bde | ||
|
|
b791aaca5a | ||
|
|
a5eef66a1c | ||
|
|
df5a9c9fd9 | ||
|
|
be15e360c1 | ||
|
|
ca6478ca88 | ||
|
|
62ef041b35 | ||
|
|
ab83a42dfb | ||
|
|
cb4b5ce640 | ||
|
|
7bbb2f3557 | ||
|
|
f27af19670 | ||
|
|
c59f05a2f8 | ||
|
|
8739ec08cf | ||
|
|
70795a69c8 | ||
|
|
236d4fcde7 | ||
|
|
ba3b87471e | ||
|
|
91746c73e1 | ||
|
|
f0a90db912 | ||
|
|
4fd262dc60 | ||
|
|
8aabcd6443 | ||
|
|
af67d733ad | ||
|
|
cdc963c42c | ||
|
|
e807f049d8 | ||
|
|
a51a159886 | ||
|
|
770e5b75a5 | ||
|
|
71e76e359a | ||
|
|
a8f68f4993 | ||
|
|
92eed64fe8 | ||
|
|
13791be485 | ||
|
|
ba8a3036a5 | ||
|
|
b6bf043c48 | ||
|
|
361e8f1bd3 | ||
|
|
c7cb0e439d | ||
|
|
0f052a0631 | ||
|
|
0f88ae324e | ||
|
|
bc944c2373 | ||
|
|
5476597343 | ||
|
|
8dd10f7e77 | ||
|
|
442e69187b | ||
|
|
029099d4a5 | ||
|
|
1ec2b3ac36 | ||
|
|
6baa6b0802 | ||
|
|
679e09f27e | ||
|
|
b4f6373605 | ||
|
|
77ec2d3747 | ||
|
|
9c926a1d81 | ||
|
|
31af1ca9e6 | ||
|
|
1d1c9eae83 | ||
|
|
ded414577f | ||
|
|
f58c07137a | ||
|
|
a7e6f470f7 | ||
|
|
229d74d266 | ||
|
|
12f247a927 | ||
|
|
2e95e1e32b | ||
|
|
c503e12adb | ||
|
|
ced8f447a7 | ||
|
|
d50cfb8dd6 | ||
|
|
c1838d4390 | ||
|
|
a127dfb8af | ||
|
|
27e5583849 | ||
|
|
4a1faa0074 | ||
|
|
5f4039c5d4 | ||
|
|
cbcac0725d | ||
|
|
bd77d4347d | ||
|
|
be4d025505 | ||
|
|
17c9499ec4 | ||
|
|
67ed249239 | ||
|
|
2b808025f9 | ||
|
|
0d41fb48dc | ||
|
|
7bc4a955ba | ||
|
|
4ccd8bf07a | ||
|
|
ddf0958c04 | ||
|
|
78aa1ed724 | ||
|
|
0215216e34 | ||
|
|
02dc74e395 | ||
|
|
b8b58726ac | ||
|
|
9853bce90b | ||
|
|
116ba9c6d2 | ||
|
|
83f51219e4 | ||
|
|
5629190bf1 | ||
|
|
112970050c | ||
|
|
2e3a3fd53e | ||
|
|
5b16941ec5 | ||
|
|
5e7741afe8 | ||
|
|
aa8f1bb6be | ||
|
|
03915110cf | ||
|
|
d67dca60fc | ||
|
|
a3306ad4e7 | ||
|
|
cd8e810c35 | ||
|
|
d0f8e21466 | ||
|
|
9413bd5357 | ||
|
|
e6de8f0f9a | ||
|
|
cc83b832f1 | ||
|
|
6ed02933a6 | ||
|
|
61b8f62221 | ||
|
|
07f0249717 | ||
|
|
80b0f9ee0f | ||
|
|
71196688ae | ||
|
|
f0bec85ef8 | ||
|
|
2e0fba3de9 | ||
|
|
be1821b9eb | ||
|
|
f5939c81d3 | ||
|
|
2656e3adb0 | ||
|
|
bfbfa761ef | ||
|
|
6f19a8a642 | ||
|
|
bfbd2ddc00 | ||
|
|
eaa40190cc | ||
|
|
2758b90019 | ||
|
|
9a61d2305c | ||
|
|
71c884fe48 | ||
|
|
9a183c7ffb | ||
|
|
60485751e2 | ||
|
|
4e559f63e3 | ||
|
|
1b407254a7 | ||
|
|
0b86def315 | ||
|
|
6633ce78d0 | ||
|
|
54df9d0c78 | ||
|
|
980efbc93d | ||
|
|
711a9441a6 | ||
|
|
eae9ee3cbe | ||
|
|
8688c28d0f | ||
|
|
aa1bba84aa | ||
|
|
ca7c8f0afd | ||
|
|
d4c5da2a76 | ||
|
|
0633197793 | ||
|
|
3d427f7e1d | ||
|
|
6a5063cf32 | ||
|
|
081a5a1e8c | ||
|
|
b9b777c621 | ||
|
|
5d0fe44130 | ||
|
|
77f9193669 | ||
|
|
ecc90be12c | ||
|
|
d14ef90136 | ||
|
|
b952b758d7 | ||
|
|
df9a6eac05 | ||
|
|
b3a5d7f379 | ||
|
|
cc7c9f3b31 | ||
|
|
8a22ff07a6 | ||
|
|
2063d66550 | ||
|
|
e698146d94 | ||
|
|
4408507371 | ||
|
|
574f53c66d | ||
|
|
a3c18ca577 | ||
|
|
ec7acd46a0 | ||
|
|
6976eb337d | ||
|
|
994413c509 | ||
|
|
0078dba085 | ||
|
|
9a636b81a6 | ||
|
|
734de26559 | ||
|
|
aa5e9a9c8b | ||
|
|
8586817963 | ||
|
|
ccca6a7714 | ||
|
|
c1fffe5cdb | ||
|
|
7c11b2204e | ||
|
|
72859ede85 | ||
|
|
30c002d0f2 | ||
|
|
3855d96135 | ||
|
|
7974be05e9 | ||
|
|
39f8c037a5 | ||
|
|
4af7597441 | ||
|
|
c37d4ad01f | ||
|
|
2801732f57 | ||
|
|
b188bc5744 | ||
|
|
f4b1fb4831 | ||
|
|
8c553d5b3d | ||
|
|
e40d7ba181 | ||
|
|
4469376dd2 | ||
|
|
64a52fbb1e | ||
|
|
bf9b334b73 | ||
|
|
1869877178 | ||
|
|
209fe67bb0 | ||
|
|
7257776ba8 | ||
|
|
af7fa56e02 | ||
|
|
1d3ec96e8d | ||
|
|
99a7bfeb4c | ||
|
|
15810333b9 | ||
|
|
0027be65eb | ||
|
|
0e804db130 | ||
|
|
57a44d6643 | ||
|
|
7e3e0d8f30 | ||
|
|
07b94f1513 | ||
|
|
9619f7e7ed | ||
|
|
0bd7fca876 | ||
|
|
ef852f0191 | ||
|
|
6791053c83 | ||
|
|
47238b2969 | ||
|
|
9f3c686913 | ||
|
|
cefd91a7b9 | ||
|
|
ecdd6d8fb0 | ||
|
|
60eb599416 | ||
|
|
8e7b944b52 | ||
|
|
6b17f9d1f2 | ||
|
|
cb459d4cc7 | ||
|
|
d795266114 | ||
|
|
3e303e4b10 | ||
|
|
2f8fca0327 | ||
|
|
4a900cbbeb | ||
|
|
5bdbb5ec62 | ||
|
|
ce78e07444 | ||
|
|
593ce27312 | ||
|
|
2536405dba | ||
|
|
ca22aedaaa | ||
|
|
e6c52002f0 | ||
|
|
3d86e52b25 | ||
|
|
f38233053e | ||
|
|
99a4751c20 | ||
|
|
013e85cf2a | ||
|
|
7c856c267d | ||
|
|
8aefb8eddb | ||
|
|
3f843cc0fc | ||
|
|
566c20ea7e | ||
|
|
a9c1a91571 | ||
|
|
e78bfe4c22 | ||
|
|
aa265fc350 | ||
|
|
c94e1aaea9 | ||
|
|
d194d7107d | ||
|
|
3789ef51f2 | ||
|
|
88c16e096a | ||
|
|
3c9468b0f1 | ||
|
|
38b90b3d4c | ||
|
|
a4929819df | ||
|
|
53e8495d06 | ||
|
|
a9fa71e455 | ||
|
|
9c0efd7bf9 | ||
|
|
796a624d2b | ||
|
|
ab9d6666cf | ||
|
|
1a8de4fcf6 | ||
|
|
6d1f83bc40 | ||
|
|
0190ee9531 | ||
|
|
718abdfdba | ||
|
|
6fb15fb591 | ||
|
|
34e087098b | ||
|
|
a8c525f4fc | ||
|
|
5ef160c98d | ||
|
|
7c4dd03a8c | ||
|
|
7fd7005776 | ||
|
|
286d48c02b | ||
|
|
08edf70194 | ||
|
|
bc34c6edb4 | ||
|
|
def6e8792c | ||
|
|
ba7419d3b0 | ||
|
|
65d079eec0 | ||
|
|
beeaebbfb7 | ||
|
|
115e88cd93 | ||
|
|
b57b0a38f5 | ||
|
|
52a22b6eca | ||
|
|
2612c55c85 | ||
|
|
63e4c7d888 | ||
|
|
b791458cb8 | ||
|
|
62f5d5111a | ||
|
|
24fb267b40 | ||
|
|
4f29dd4f8c | ||
|
|
08db1d63bf | ||
|
|
0e3145a051 | ||
|
|
eb6337f6fb | ||
|
|
9499cd0265 | ||
|
|
471f8f2bec | ||
|
|
aa74c1c721 | ||
|
|
0e8024adf1 | ||
|
|
a833476437 | ||
|
|
bec097f14d | ||
|
|
196449fbd6 | ||
|
|
7f8704ee2c | ||
|
|
d8866172c1 | ||
|
|
5ee5b270ef | ||
|
|
89cb4f4770 | ||
|
|
88eeab410b | ||
|
|
cecc01d216 | ||
|
|
0d4893b13c | ||
|
|
d081b88af2 | ||
|
|
a285313bae | ||
|
|
860d88fe56 | ||
|
|
968aabf893 | ||
|
|
d46ac6206d | ||
|
|
6bb8d26561 | ||
|
|
2b7ace0356 | ||
|
|
ac66606135 | ||
|
|
528986cd4f | ||
|
|
dcaaa4e689 | ||
|
|
d1cdd60ee8 | ||
|
|
2d15da9a39 | ||
|
|
611eceff2b | ||
|
|
00f1fc39fa | ||
|
|
8af9879b08 | ||
|
|
501e980e73 | ||
|
|
8aaa88770f | ||
|
|
ac2b7e769d | ||
|
|
03ba35d65a | ||
|
|
f0c4f33eea | ||
|
|
7c6243cf33 | ||
|
|
5bf5be94bb | ||
|
|
c96e10b33d | ||
|
|
bba73c4f31 | ||
|
|
e90bb63a8f | ||
|
|
3ddba0816e | ||
|
|
5386f81cda | ||
|
|
3c84a814a8 | ||
|
|
bd04cfd898 | ||
|
|
c7c0d80cf0 | ||
|
|
21cc88dfea | ||
|
|
0f40141be7 | ||
|
|
40b065abd9 | ||
|
|
d3b0c1d82c | ||
|
|
c187a27123 | ||
|
|
f6353ca14c | ||
|
|
1bfdf4b232 | ||
|
|
95f57c9f5e | ||
|
|
fdfb0c7928 | ||
|
|
53b0101a12 | ||
|
|
8a21ce98b1 | ||
|
|
3ede565971 | ||
|
|
8611d738b0 | ||
|
|
bc51a91734 | ||
|
|
06988073d8 | ||
|
|
52f66e7982 | ||
|
|
621e40304f | ||
|
|
7fed5b7138 | ||
|
|
801774ab28 | ||
|
|
d8b8dfa299 | ||
|
|
fe5daefd61 | ||
|
|
42247449f8 | ||
|
|
c3d771c8fc | ||
|
|
278b74e78c | ||
|
|
3a5ea3dd9a | ||
|
|
b5b0792c90 | ||
|
|
9361f6bc53 | ||
|
|
5f5c48ea05 | ||
|
|
7c052fedfc | ||
|
|
ec3ea892af | ||
|
|
c7dc7de0c3 | ||
|
|
1c17cd1be5 | ||
|
|
4ec89e2cc8 | ||
|
|
552e6da392 | ||
|
|
501e764c21 | ||
|
|
2b101c2967 | ||
|
|
23cf5febec | ||
|
|
2835c66694 | ||
|
|
5fb84d153e | ||
|
|
5767bfb5a2 | ||
|
|
fd4477bc50 | ||
|
|
e896d52968 | ||
|
|
6bbe1b41c2 | ||
|
|
bd6396d7a8 | ||
|
|
b3435c97c3 | ||
|
|
d7974f057f | ||
|
|
50c5f921f5 | ||
|
|
e996a45bf5 | ||
|
|
b3f784686f | ||
|
|
77566eae0d | ||
|
|
59e2124407 | ||
|
|
f8c9b69f4b | ||
|
|
d14034d38b | ||
|
|
1d3b9a19bc | ||
|
|
7413d7db25 | ||
|
|
5cabeed19d | ||
|
|
40aed1086f | ||
|
|
dd1ea597d4 | ||
|
|
a7d5bef93f | ||
|
|
7bfa5a6cc4 | ||
|
|
b4f0501765 | ||
|
|
767437d9aa | ||
|
|
6c5a62353c | ||
|
|
bfd54e94e7 | ||
|
|
126fd5c0ff | ||
|
|
747c2e1637 | ||
|
|
23e4f87af0 | ||
|
|
9ef5ea8fb6 | ||
|
|
46c6c4dd21 | ||
|
|
9d89a38490 | ||
|
|
d56da74168 | ||
|
|
dff4d15872 | ||
|
|
939eba8e61 | ||
|
|
0e4c0d8fff | ||
|
|
5291a87587 | ||
|
|
5da29227e8 | ||
|
|
70bf00e844 | ||
|
|
cf999896d5 | ||
|
|
2bb511cd99 | ||
|
|
f147d0f3b3 | ||
|
|
1220bad26d | ||
|
|
689c701f34 | ||
|
|
a3d281192d | ||
|
|
f59e809713 | ||
|
|
48f7a25a22 | ||
|
|
0b57ebb10f | ||
|
|
a88c2f0760 | ||
|
|
5e850f12c6 | ||
|
|
66d6322c1d | ||
|
|
d0e61978d6 | ||
|
|
faee5bb613 | ||
|
|
6adfded074 | ||
|
|
6584a46d53 | ||
|
|
5b886b3917 | ||
|
|
c62df2228b | ||
|
|
17b3ba1272 | ||
|
|
eca54c2a09 | ||
|
|
cb51a2a81b | ||
|
|
90fc7a0d9d | ||
|
|
9a6ab35c21 | ||
|
|
74b15aa8e9 | ||
|
|
032d8bee1b | ||
|
|
b7be97fcf9 | ||
| 598e93728f | |||
| c6ca219505 | |||
| 70aeb4b989 | |||
| 6cb96766db | |||
| 1b04348a8b | |||
| 1eab3be7f6 | |||
| 3575d57608 | |||
| 50e5525591 | |||
| 6fd2bc2687 | |||
| a7707b7b28 | |||
| 70c60e93ac | |||
| 51317f3e51 | |||
| 98a9fdc6a7 | |||
| 9c8cfe3bdb | |||
| 8d3bdd8a2b | |||
| 051556936e | |||
| 37d6b747ac | |||
| 1c5958d1d2 | |||
| bd4f48fe04 | |||
| e8c55512e2 | |||
| 4c551cc15f | |||
| 067c4a62ff | |||
| a013d73400 | |||
| 3857eae158 | |||
| f07789a3e1 | |||
| 44a8ced30c | |||
| bae4070621 | |||
| 906ce8759b | |||
| 17d46e804e | |||
| 9e5d3aa139 | |||
| 50154baa40 | |||
| 0fc85e5de2 | |||
| 0fd1edd8fd | |||
| abe7de5701 | |||
| 9b1d38d9ff | |||
| af9c4a36c6 | |||
| f8a32e841c | |||
| e4144409f0 | |||
| f4ef2d4cc2 | |||
| c47fae3a1b | |||
| 48a127409e | |||
| 945c2c45ce | |||
| 749864e922 | |||
| 6e79381937 | |||
| 1f444e58ce | |||
| 53870fda89 | |||
| 240f53047d | |||
| b803afa973 | |||
| 3fba2d8a77 | |||
| ff384fb734 | |||
| 08dc06c810 | |||
| b22fdd841b | |||
| 8f9eb91b13 | |||
| 98c19b560f | |||
| 41d7d2fa34 | |||
| f4029f0ef7 | |||
| 087d349cec | |||
| 4bf4183a2a | |||
| 6d337e7cb2 | |||
| 7d31dd8d52 | |||
| 3462d7af2a | |||
| 0e8b4caac3 | |||
| af56378ee2 | |||
| 4959dfd14f | |||
| f0a89df6ab | |||
| 1907ce1e54 | |||
| 79465d9634 | |||
| 09d849eb9d | |||
| 8abcc7194a | |||
| 9903423caf | |||
| 88a88227c4 | |||
| 86377fec5f | |||
| fe69ec4175 | |||
| b180572d5f | |||
| f4c85b7a67 | |||
| 9843023c1f | |||
| 75d49517b7 | |||
| 2fcd426b49 | |||
| f38be7a706 | |||
| c3de881a2c | |||
| 532a4b679b | |||
| 703b77897d | |||
| 7275fb10e3 | |||
| b1b7cd6d50 | |||
| b6691e1991 | |||
| 057362d011 | |||
| 6973d39cbd | |||
| 52ce2934ac | |||
| c0898b84d6 | |||
| 9883c3d9c2 | |||
| 0c4628677f | |||
| 4826ae8571 | |||
| 003e07491d | |||
| 885aa4f627 | |||
| 8733fdc657 | |||
| af5543190a | |||
| 61965d480c | |||
| f42ca7bd52 | |||
| 7f86399033 | |||
| 985ab9d2dc | |||
| 05ee57378a | |||
| 21d4dd6bfa | |||
| 11eb66f834 | |||
| f1eb29c713 | |||
| dac451bb86 | |||
| 6040a3f41f | |||
| 5a82e769c7 | |||
| 70594156fd | |||
| f31aef6f5d | |||
| de5b729c0b | |||
| 525995615a | |||
| 2b2fb9e3e7 | |||
| c410294022 | |||
| af9d8f7858 | |||
| 79c7847f1c | |||
| f684923a8a | |||
| a223f933ce | |||
| 2ac879d276 | |||
| 2cfbd21560 | |||
| 4b26f8f476 | |||
| 3b2bf921f3 | |||
| e3dfaf4440 | |||
| fc58850e56 | |||
| cb68319ac0 | |||
| 24b297011e | |||
| 0ad1a5e049 | |||
| 5d467622f4 | |||
| c39bfb234d | |||
| baaa701249 | |||
| ad7874649f | |||
| 9a712e7371 | |||
| 4eaacf5697 | |||
| a5d859471d | |||
| 93d4f0ff1a | |||
| 6d9f96c5d5 | |||
| 95d115b585 | |||
| 238ebcbf34 | |||
| fb59bf552a | |||
| 689dbeefbd | |||
| d29e296799 | |||
| bb3f6fc408 | |||
| 1173827a79 | |||
| 15a386ad05 | |||
| d724f96728 | |||
| 4db3da1eaa | |||
| aa488cc6e3 | |||
| 0e1a760eb3 | |||
| d62ddfd0f2 | |||
| 58ead7b426 | |||
| fdb2b132f6 | |||
| d765543b30 | |||
| d53a365fcb | |||
| 05f24f5e77 | |||
| 24fe8a1d8e | |||
| 2f55596b9d | |||
| 2c80e77bd7 | |||
| ba8dfb673f | |||
| d37a7ab7cc | |||
| deadb3498e | |||
| 4b310e9dfa | |||
| e4b269956e | |||
| 0f09fd53eb | |||
| c8ebe7892a | |||
| 4fc0053bd9 | |||
| 74e5322c5e | |||
| 8d484ba728 | |||
| 6db48c37fb | |||
| 8e4573e5ad | |||
| 14c2c00d51 | |||
| 01b59f0a9b | |||
| 370605554b | |||
| 152813730e | |||
| 25795e2cce | |||
| 37e188a0df | |||
| c020d5659e | |||
| 6a950c8b57 | |||
| 1f212e1ad6 | |||
| 0d0acb6f3c | |||
| 9c8d914b75 | |||
| d97b1ab894 | |||
| 791a6c1c72 | |||
| 5441063012 | |||
| 071b09709b | |||
| db63be8f92 | |||
| 6bfc5e4378 | |||
| 5c55750c0c | |||
| 61a795d634 | |||
| 638757b6e4 | |||
| 8ca98b394f | |||
| 6cbb7f9c1e | |||
| 7d71cf3a8c | |||
| 5b4490a845 | |||
| 8211380243 | |||
| 240b203ec0 | |||
| 131100d6e6 | |||
| acad99bdfe | |||
| b45bb0cfbc | |||
| 628c5a2013 | |||
| d6fb9da176 | |||
| cd12b53071 | |||
| 5f2c1f3b5c | |||
| ff3e956fc7 | |||
| 4773a388ac | |||
| ea0e37aa49 | |||
| 2791821239 | |||
| 6820608a09 | |||
| 9d9c741ce9 | |||
| aad91fe4ae | |||
| 495f0bcf3b | |||
| a579e3c88f | |||
| 6be065c0cb | |||
| 5d468b58fd | |||
| 1896cd1472 | |||
| 96eef822d6 | |||
| 19f46d4d86 | |||
| 5c63bd860b | |||
| 3f30f80d34 | |||
| 90172ea19a | |||
| 3c14bb26c2 | |||
| fa7b2a7a23 | |||
| 33e49e1b08 | |||
| db01599569 | |||
| ae5bd374ba | |||
| d90d067659 | |||
| 3889faee26 | |||
| 6719d805d3 | |||
| fd6e90e74c | |||
| 0feac66e94 | |||
| 1407700511 | |||
| f16251dc78 | |||
| 981b121980 | |||
| ff54b323e6 | |||
| f10e4af29b | |||
| 074b281c52 | |||
| 956afa6ae7 | |||
| f3a3ed6369 | |||
| 6c3cba1b87 | |||
| 6733777f63 | |||
| b7073361d7 | |||
| 65ef021e3e | |||
| 15af3e91e0 | |||
| 3586d5c745 | |||
| 46d978fe29 | |||
| 6e43208778 | |||
| e79a6b33fb | |||
| d0d3d150ee | |||
| e57f440665 | |||
| 3290a5f686 | |||
| a8e14f6305 | |||
| 44394ae85d | |||
| 8dd5925af6 | |||
| 945642c511 | |||
| b03775fa07 | |||
| 870d74a1de | |||
| a828fee476 | |||
| 9c4cc24f42 | |||
| f1f33080f6 | |||
| 1f5b935beb | |||
| 3dc8547431 | |||
| 97cf5b2b6b | |||
| bb2fe82c84 | |||
| 3c30e9107a | |||
| 4910f8650f | |||
| 51ffcb5be7 | |||
| c7fac575e4 | |||
| 82d1f0fd38 | |||
| ec70c4f0c7 | |||
| 170cc4244d | |||
| 0b5b9c8aa4 | |||
| 638a71f36c | |||
| 756ab09ce6 | |||
| 646ff064a7 | |||
| 82e8ce432b | |||
| 800a25743d | |||
| f795ccf129 | |||
| 6d1f30f703 | |||
| 73443b77a8 | |||
| 0ae7d2669a | |||
| 70afe75eb4 | |||
| 164681427a | |||
| 21688ab9d0 | |||
| 0d719d88ea | |||
| 5acaa9c6bf | |||
| c76320b078 | |||
| 8359f78e2e | |||
| b9c8950f6d | |||
| 968eb07472 | |||
| 95ed67b1fe | |||
| 0ede7e0f82 | |||
| 5afd86a932 | |||
| 74274f879e | |||
| 06fa428790 | |||
| 915eb75719 | |||
| d977a5e712 | |||
| 5e1a1005f1 | |||
| c8c638d201 | |||
| e5c6ccd252 | |||
| 3c92e828f6 | |||
| 349a5a506e | |||
| b2dfbac9e5 | |||
| b4e69d46ae | |||
| 13a8221b92 | |||
| 111fc8ad00 | |||
| 7ac5a8e08c | |||
| 278246a343 | |||
| acf780635b | |||
| 5d212df929 | |||
| 377970f874 | |||
| 6b358d74db | |||
| 2c490753a4 | |||
| 9c234af747 | |||
| 8889ea5cf3 | |||
| 372ff54261 | |||
| 6c942f5a95 | |||
| 97f5e49187 | |||
| b9e617edfc | |||
| 86a2c8f0f5 | |||
| c69ed23b1c | |||
| 40bf476ea6 | |||
| cabae4989e | |||
| d1532e1bec | |||
| 850509257d | |||
| 30e58fb397 | |||
| d69e1a1231 | |||
| b6fe91b016 | |||
| 958c2b625c | |||
| 880cc64022 | |||
| 8389787c7e | |||
| c5b978dfae | |||
| 8f0f5131c8 | |||
| fe3cc901b0 | |||
| bf5a2096f5 | |||
| 872a529d3e | |||
| 6a0babe986 | |||
| 5e461f1f1a | |||
| 14b8993d10 | |||
| 5b19fdf7f2 | |||
| f33981ef34 | |||
| 1ff8d37d89 | |||
| bbf06a3882 | |||
| c9c7a33585 | |||
| d4a42bff90 | |||
| b7abd805e2 | |||
| d79c6aee7f | |||
| cd2ca65903 | |||
| 2b5b8fb19d | |||
| 98d29c0af0 | |||
| 2b90a7a5c8 | |||
| 48b10d0f49 | |||
| e22b09700d | |||
| b8074205ef | |||
| 2f894edda5 | |||
| fa30366ea4 | |||
| 86eba83fd3 | |||
| 2f70de8e11 | |||
| bec26ee072 | |||
| df4a2b0bb3 | |||
| 4caf0dbe95 | |||
| dd6a1d9bfd | |||
| 114e2e03b2 | |||
| ce448da7e0 | |||
| c411979edb | |||
| 4c7c3c2ff4 | |||
| 2f66e36c28 | |||
| 48b1f15070 | |||
| f5b60ac743 | |||
| 139f0ce7cb | |||
| 739d2de297 | |||
| 0f513266a0 | |||
| f7f4da24ec | |||
| 4eb57b4a91 | |||
| cde290fbd7 | |||
| 9d9eda29be | |||
| 2068202ca6 | |||
| e61ed2bb5f | |||
| 885f51943a | |||
| 8ca458a8ff | |||
| a1b9b7ab38 | |||
| d2cb74a252 | |||
| ff8ab2fe09 | |||
| 1546e0836c | |||
| bfb26b717a | |||
| 5ba308f07a | |||
| 9e501426cb | |||
| 6b56bad21e | |||
| 59a26ea00b | |||
| 30963b94dd | |||
| 49405b953f | |||
| be5dd4acd8 | |||
| 971cda1cb5 | |||
| ea9c2dfe67 | |||
| 5fc56308d2 | |||
| 631ac34573 | |||
| 8a139e1ac0 | |||
| be83505ecf | |||
| 2f9c2332f9 | |||
| 44f0064b42 | |||
| 65f98c6387 | |||
| ee8994582c | |||
| 6e1de52a40 | |||
| db14eaa5f9 | |||
| 054dfba6b5 | |||
| ed1bc6c73f | |||
| d4801bb240 | |||
| 75dbe2548a | |||
| 38bab672d4 | |||
| aa7dca65d1 | |||
| e6dbb60e4a | |||
| a43be4cbdb | |||
| f921bb4ae9 | |||
| ca33704f51 | |||
| ef602a3a15 | |||
| bffa6d2c9e | |||
| 5bae5b6e5f | |||
| dfa0183669 | |||
| 944f091ca5 | |||
| b20cf53645 | |||
| a671190fff | |||
| b075adf424 | |||
| a96680875c | |||
| b4c0b9f8eb | |||
| a45bebddac | |||
| 6f5ba925c1 | |||
| 0ed1b2eff9 | |||
| 204a4d4bdd | |||
| 10d2ba78d0 | |||
| 96e027b4e3 | |||
| a97c6defca | |||
| 14e5695b6e | |||
| 87e2d6bbfd | |||
| 8ba6a82065 | |||
| cde9aba0cb | |||
| 5ff916475a | |||
| 09ea3125df | |||
| 46cadcb8e5 | |||
| abf9893f9d | |||
| 7930fbce49 | |||
| 3541a9c402 | |||
| 5dfaa4c126 | |||
| 94bf207397 | |||
| 37ab07356e | |||
| b7f51a8deb | |||
| c8671afe47 | |||
| 504bcd02e3 | |||
| c882862af6 | |||
| 83164e44d3 | |||
| f8a15be565 | |||
| ffdf0690e7 | |||
| 4ecb386233 | |||
| 2fef4b4314 | |||
| 4012669a3e | |||
| 3f113310e3 | |||
| 54ef0ff807 | |||
| c48d04de0f | |||
| aab304dd20 | |||
| c9200180cf | |||
| e9dfeba11f | |||
| 2035b06888 | |||
| 64249a06ac | |||
| b3f3455237 | |||
| 6ca3ad4c63 | |||
| 42f227c39d | |||
| eb9bc4677e | |||
| 2458b91ac2 | |||
| a91cfe7ddc | |||
| 0b4a1a5232 | |||
| d9623789bd | |||
| c300d4d251 | |||
| 44c44cf9e6 | |||
| ff61094b69 | |||
| 04e9d29010 | |||
| d61ea26eb5 | |||
| 000105b175 | |||
| 8b840f52d1 | |||
| 769e7bb37e | |||
| 81ede3f511 | |||
| 9c00216e62 | |||
| fc675ae144 | |||
| b75780044c | |||
| 76852daf6d | |||
| 601196ddeb | |||
| 6d4fd5fd77 | |||
| 24fe61e064 | |||
| 32a0188b72 | |||
| 77b214f1dc | |||
| 345103d759 | |||
| 89c0776045 | |||
| eca65f6b4d | |||
| 6b493f11d7 | |||
| c83c5513df | |||
| 2c097ca41f | |||
| 4387092f7d | |||
| 72817f7be3 | |||
| 68f9043910 | |||
| 0ad2952563 | |||
| 2389c982bb | |||
| 7d0cf0f100 | |||
| ab82329171 | |||
| 3891a6e86b | |||
| bceded3ebd | |||
| 4d53b84587 | |||
| 37e4259fbd |
@@ -5,7 +5,7 @@ root = true
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
src/scrapers/template.js
|
||||
|
||||
17
.eslintrc
17
.eslintrc
@@ -2,27 +2,26 @@
|
||||
"root": true,
|
||||
"extends": ["airbnb-base", "plugin:vue/recommended"],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", "tab"],
|
||||
"no-tabs": "off",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
|
||||
"no-console": 0,
|
||||
"indent": "off",
|
||||
"template-curly-spacing": "off",
|
||||
"max-len": [2, {
|
||||
"code": 300,
|
||||
"tabWidth": 4,
|
||||
"ignoreUrls": true
|
||||
}],
|
||||
"vue/html-indent": ["error", 4],
|
||||
"max-len": 0,
|
||||
"vue/no-v-html": 0,
|
||||
"vue/html-indent": ["error", "tab"],
|
||||
"vue/multiline-html-element-content-newline": 0,
|
||||
"vue/singleline-html-element-content-newline": 0,
|
||||
"vue/multi-word-component-names": 0,
|
||||
"no-param-reassign": ["error", {
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["state", "acc"]
|
||||
}],
|
||||
}]
|
||||
},
|
||||
"globals": {
|
||||
"CONFIG": true
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,13 @@
|
||||
node_modules/
|
||||
dist/
|
||||
log/
|
||||
media/
|
||||
html/
|
||||
public/js/*
|
||||
public/css/*
|
||||
config/*
|
||||
!config/default.js
|
||||
assets/js/config/
|
||||
!assets/js/config/default.js
|
||||
*.heapprofile
|
||||
*.heapsnapshot
|
||||
|
||||
524
README.md
524
README.md
@@ -2,367 +2,195 @@
|
||||
**NSFW** - The latest releases from your favorite porn studios in one place.
|
||||
|
||||
## Install & run
|
||||
Use [nvm](https://github.com/creationix/nvm) to install a recent version of NodeJS. Download and unpack or clone this repository, and run the following in the root directory:
|
||||
Use [nvm](https://github.com/creationix/nvm) to install NodeJS v14.15.4 or newer. Download and unpack or clone this repository, and run the following in the root directory:
|
||||
|
||||
`npm install`
|
||||
|
||||
### Set up database
|
||||
Install PostgreSQL, make sure password authentication is enabled (scram-sha-256) and create a database with a privileged user.
|
||||
|
||||
### Configuration
|
||||
Do not modify `config/default.js`, but instead create a copy at `config/local.js` containing the properties you wish to change. If you have set `NODE_ENV`, copy `assets/js/config/default.js` to `assets/js/config/[environment].js`. After setting up PostgreSQL and configuring the details, run the following commands to create and populate the tables, and build the project:
|
||||
|
||||
`npm run migrate`
|
||||
|
||||
`npm run seed`
|
||||
|
||||
`npm start`
|
||||
You can also use `npm run flush` to run both steps at once, and wipe the database completely later.
|
||||
|
||||
### Configuration
|
||||
Do not modify `config/default.js`, but instead create a copy at `config/local.js` containing the properties you wish to change.
|
||||
#### Networks and channels
|
||||
To scrape the networks and channels available in the database, you can configure `include` and `exclude` lists. To include all available channels and only use the `exclude` list, leave the `include` parameter unconfigured. The `exclude` lists will exclude channels and child networks from networks on the `include` lists, but not vice versa. That is, if the `include` list includes a network and the `exclude` list excludes one of that network's channels, the channel will not be scraped. However, if the `include` list includes a channel, and the `exclude` list includes its parent network, the channel will be scraped.
|
||||
|
||||
### Options
|
||||
`npm start -- --option value`
|
||||
This configuration will scrape Evil Angel and all XEmpire channels, except for LesbianX.
|
||||
```
|
||||
include: {
|
||||
networks: [
|
||||
'xempire',
|
||||
],
|
||||
channels: [
|
||||
'evilangel',
|
||||
],
|
||||
},
|
||||
exclude: {
|
||||
channels: [
|
||||
'lesbianx',
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Running `npm start` without any arguments will run the web server.
|
||||
This configuration will scrape all channels, except for BAM Visions, and except all channels part of the Vixen network.
|
||||
```
|
||||
exclude: {
|
||||
channels: [
|
||||
'bamvisions',
|
||||
],
|
||||
networks: [
|
||||
'vixen'
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
* `--fetch`: Fetch updates instead of running the webserver. Without further arguments, it will use the networks and sites defined in the configuration file.
|
||||
* `--site [site ID]`: Fetch updates from a specific site. The site ID is typically the site name in lowercase and without cases or special characters. For example, Teens Like It Big is teenslikeitbig.
|
||||
* `--network [network ID]`: Fetch updates from all sites of a specific network. The network ID is composed similarly to the site ID.
|
||||
* `--after "[time]"`: Do not fetch scenes older than this. Example values are: `"1 month"`, `"2 weeks"`, `"3 years"`.
|
||||
* `--scene [URL]`: Try to retrieve scene details from its official site or network URL.
|
||||
* `--deep`: Follow each release link found running `--site` or `--network` and scrape it for more details. Enabled by default at the moment of writing; use `--no-deep` to only save information found on the overview pages.
|
||||
* `--copy`: Try to copy relevant results to the clipboard. When used with `--scene`, it will copy the filename as defined in the config with all the details filled in.
|
||||
### Building
|
||||
To build traxxx, run the following command:
|
||||
|
||||
#### Developer options
|
||||
`npm run build`
|
||||
|
||||
To generate thumbnails for new logos and tag photos, install ImageMagick and run:
|
||||
|
||||
`npm run logos-thumbs`
|
||||
|
||||
`npm run tags-thumbs`
|
||||
|
||||
### Run
|
||||
`./traxxx --option value` or `npm start -- --option value`
|
||||
|
||||
* `--server`: Run the web server
|
||||
|
||||
#### Channels
|
||||
* `--channels [slug] [slug]`: Fetch updates from specific channels. The slug is the channel's name in lowercase and without cases or special characters. For example, Teens Like It Big is teenslikeitbig. Overrides configured included networks and channels.
|
||||
* `--networks [slug] [slug]`: Fetch updates from all sites of a specific network. The network slug is composed similarly to the channel slug. Overrides configured included networks and channels.
|
||||
* `--exclude-channels [slug] [slug]`: Scrape every configured, specified or available channel, except for specified. Overrides configured excluded channels.
|
||||
* `--exclude-networks [slug] [slug]`: Scrape every configured, specified or available network, except for specified. Overrides configured excluded networks.
|
||||
* `--after "[time]"`: Do not fetch scenes older than this period or date. Example values are: `"1 month"`, `"3 years"`, `"2019-01-01"`.
|
||||
* `--scene [URL]`: Try to retrieve scene details from its official channel or network URL.
|
||||
* `--deep`: Follow each release link found running `--channel` or `--network` and scrape it for more details. Enabled by default ; use `--no-deep` to only save information found on the overview pages.
|
||||
|
||||
#### Actors
|
||||
* `--actors "[name]" "[name]"`: Fetch actor profiles. When no names are specified, actors without existing profiles are scraped
|
||||
* `--actors-file [filepath]`: Fetch all scenes for the actors specified in a file using a newline delimiter.
|
||||
* `--actors-sources [slug] [slug]`: Scrapers to use for actor profiles. Defaults to config.
|
||||
* `--actors-update [time]`: Update actors that don't have any profiles newer than period ("1 month") or date (2020-08-01). Using this argument without a value will default to 1900-01-01, practically updating all actors.
|
||||
* `--actors-scenes`: Fetch all scenes for scraped actors. Use with caution, as an actor may have many scenes.
|
||||
* `--scene-actors`: Fetch profiles for actors associated with scraped scenes. Use with caution, as scenes may have many actors, each with many profiles.
|
||||
|
||||
#### Developers
|
||||
* `--no-save`: Do not store retrieved information in local database, forcing re-fetch.
|
||||
* `--debug`: Show full error stack trace.
|
||||
* `--level`: Change log level to `silly`, `verbose`, `info`, `warn` or `error`.
|
||||
* `--delete-scenes`: Delete scenes and assets by ID.
|
||||
* `--delete-movies`: Delete movies and assets by ID.
|
||||
* `--flush-network`: Delete all scenes and movies with assets from all network channels
|
||||
* `--flush-channel`: Delete all scenes and movies with assets from a channel
|
||||
* `--flush-batch`: Delete all scenes and movies with assets from a batch ID
|
||||
* `--flush-media`: Delete temporary media directory and all unassociated media entries and files
|
||||
|
||||
### API
|
||||
A GraphQL API is available at `/graphql`, and a REST API is available at the following `GET` endpoints:
|
||||
|
||||
* `/api/scenes`: Fetch the latest releases. Supports search with `query` or `q` parameter;
|
||||
* `/api/scenes/{ID}`: Fetch scene by ID.
|
||||
* `/api/actors`: Fetch actors. Search `query` or `q` parameter required.
|
||||
* `/api/actors/{ID|slug}`: Fetch detailed actor by ID or slug.
|
||||
* `/api/entities`: Fetch networks and channels. Use the `type` parameter to filter for either `channel`s or `network`s.
|
||||
* `/api/entities/{ID|slug}`: Fetch detailed network or channel by ID. To fetch by slug, the `type` parameter must specify either `channel` or `network`.
|
||||
* `/api/channels`: Fetch channel entities. Supports the `q` or `query` parameter for searching.
|
||||
* `/api/channels/{ID|slug}`: Fetch detailed channel by ID or slug.
|
||||
* `/api/networks`: Fetch networks. Supports a `q` or `query` parameter for searching.
|
||||
* `/api/networks/{ID|slug}`: Fetch detailed network by ID or slug.
|
||||
* `/api/tags`: Fetch tags.
|
||||
* `/api/tags/{ID|slug|name}`: Fetch detailed tag by ID, slug or name.
|
||||
|
||||
## Supported networks & sites
|
||||
285 sites on 15 networks, continuously expanding!
|
||||
* **21Sextury**
|
||||
* Anal Teen Angels
|
||||
* Asshole Fever
|
||||
* Butt Plays
|
||||
* Club Sandy
|
||||
* DP Fanatics
|
||||
* Deepthroat Frenzy
|
||||
* Footsie Babes
|
||||
* Gapeland
|
||||
* Lez Cuties
|
||||
* Pix and Video
|
||||
* **Bang Bros**
|
||||
* Ass Parade
|
||||
* AvaSpice
|
||||
* Back Room Facials
|
||||
* Backroom MILF
|
||||
* Ball Honeys
|
||||
* Bang Bus
|
||||
* Bang Casting
|
||||
* Bang POV
|
||||
* Bang Tryouts
|
||||
* BangBros 18
|
||||
* BangBros Angels
|
||||
* BangBros Remastered
|
||||
* Bangbros Clips
|
||||
* Big Mouthfuls
|
||||
* Big Tit Cream Pie
|
||||
* Big Tits, Round Asses
|
||||
* BlowJob Fridays
|
||||
* Blowjob Ninjas
|
||||
* Boob Squad
|
||||
* Brown Bunnies
|
||||
* Can He Score?
|
||||
* Casting
|
||||
* Chongas
|
||||
* Colombia Fuck Fest
|
||||
* Dirty World Tour
|
||||
* Dorm Invasion
|
||||
* Facial Fest
|
||||
* Fuck Team Five
|
||||
* Glory Hole Loads
|
||||
* Latina Rampage
|
||||
* Living With Anna
|
||||
* MILF Lessons
|
||||
* Magical Feet
|
||||
* Milf Soup
|
||||
* MomIsHorny
|
||||
* Monsters of Cock
|
||||
* Mr CamelToe
|
||||
* Mr. Anal
|
||||
* My Dirty Maid
|
||||
* My Life In Brazil
|
||||
* Newbie Black
|
||||
* Party of 3
|
||||
* Pawg
|
||||
* Penny Show
|
||||
* Porn Star Spa
|
||||
* Power Munch
|
||||
* Public Bang
|
||||
* Slutty White Girls
|
||||
* Stepmom Videos
|
||||
* Street Ranger
|
||||
* Tugjobs
|
||||
* Working Latinas
|
||||
* **Blowpass**
|
||||
* 1000 Facials
|
||||
* Immoral Live
|
||||
* Mommy Blows Best
|
||||
* Only Teen Blowjobs
|
||||
* Throated
|
||||
* **Brazzers**
|
||||
* Asses In Public
|
||||
* Baby Got Boobs
|
||||
* Big Butts Like It Big
|
||||
* Big Tits In Sports
|
||||
* Big Tits In Uniform
|
||||
* Big Tits at School
|
||||
* Big Tits at Work
|
||||
* Big Wet Butts
|
||||
* Brazzers Exxtra
|
||||
* Brazzers Live
|
||||
* Brazzers Vault
|
||||
* Brazzers en Español
|
||||
* Busty & Real
|
||||
* Bustyz
|
||||
* Butts & Blacks
|
||||
* CFNM
|
||||
* Day With A Pornstar
|
||||
* Dirty Masseur
|
||||
* Doctor Adventures
|
||||
* Hot And Mean
|
||||
* Hot Chicks Big Asses
|
||||
* JugFuckers
|
||||
* Milfs Like it Big
|
||||
* Mommy Got Boobs
|
||||
* Moms in Control
|
||||
* Pornstars Like it Big
|
||||
* Racks & Blacks
|
||||
* Real Wife Stories
|
||||
* SexPro Adventures
|
||||
* Shes Gonna Squirt
|
||||
* Teens Like It Big
|
||||
* Teens Like It Black
|
||||
* ZZ Series
|
||||
* **DDF Network**
|
||||
* 1By-Day
|
||||
* DDF Busty
|
||||
* DDF Network VR
|
||||
* Euro Girls on Girls
|
||||
* Euro Teen Erotica
|
||||
* Hands on Hardcore
|
||||
* Hot Legs and Feet
|
||||
* House of Taboo
|
||||
* Only Blowjob
|
||||
* **Dogfart Network**
|
||||
* Barb Cummings
|
||||
* Black Meat White Feet
|
||||
* Blacks On Blondes
|
||||
* Blacks On Boys
|
||||
* Blacks On Cougars
|
||||
* Candy Monroe
|
||||
* Cuckold Sessions
|
||||
* Cumbang
|
||||
* Dogfart Behind The Scenes
|
||||
* Glory Hole
|
||||
* Gloryholes And Handjobs
|
||||
* Gloryholy Initiations
|
||||
* Interracial Blowbang
|
||||
* Interracial Pickups
|
||||
* Katie Thomas
|
||||
* Ruth Blackwell
|
||||
* Spring Thomas
|
||||
* The Minion
|
||||
* Watching My Daughter Go Black
|
||||
* Watching My Mom Go Black
|
||||
* We Fuck Black Girls
|
||||
* Wife Writing
|
||||
* Zebra Girls
|
||||
* **Evil Angel**
|
||||
* **Jules Jordan**
|
||||
* **Kink**
|
||||
* 30 Minutes of Torment
|
||||
* Bound Gangbangs
|
||||
* Bound Gods
|
||||
* Bound in Public
|
||||
* Brutal Sessions
|
||||
* Butt Machine Boys
|
||||
* Device Bondage
|
||||
* Devine Bitches
|
||||
* Electrosluts
|
||||
* Everything Butt
|
||||
* Families Tied
|
||||
* Foot Worship
|
||||
* Fucked and Bound
|
||||
* Fucking Machines
|
||||
* Hardcore Gangbang
|
||||
* Hogtied
|
||||
* Kink University
|
||||
* Men In Pain
|
||||
* Men on Edge
|
||||
* Naked Kombat
|
||||
* Public Disgrace
|
||||
* Sadistic Rope
|
||||
* Sex and Submission
|
||||
* The Training of O
|
||||
* The Upper Floor
|
||||
* TS Pussy Hunters
|
||||
* TS Seduction
|
||||
* Ultimate Surrender
|
||||
* Water Bondage
|
||||
* Whipped Ass
|
||||
* Wired Pussy
|
||||
* **LegalPorno**
|
||||
* **Mike Adriano**
|
||||
* All Anal
|
||||
* True Anal
|
||||
* Nympho
|
||||
* Swallowed
|
||||
* **MOFOS**
|
||||
* Blogs
|
||||
* Don't Break Me
|
||||
* Ebony Sex Tapes
|
||||
* Girls Gone Pink
|
||||
* I Know That Girl
|
||||
* Latina Sex Tapes
|
||||
* Lets Try Anal
|
||||
* MOFOS Lab
|
||||
* Mofos B Sides
|
||||
* Pervs On Patrol
|
||||
* Public Pickups
|
||||
* Real Slut Party
|
||||
* Share My BF
|
||||
* She's A Freak
|
||||
* Stranded Teens
|
||||
* **Naughty America**\*
|
||||
* 2 Chicks Same Time
|
||||
* American Daydreams
|
||||
* Anal College
|
||||
* Asian 1 On 1
|
||||
* Ass Masterpiece
|
||||
* Big Cock Bully
|
||||
* Diary of a Milf
|
||||
* Diary of a Nanny
|
||||
* Dirty Wives Club
|
||||
* Fast Times
|
||||
* Housewife 1 on 1
|
||||
* I Have a Wife
|
||||
* LA Sluts
|
||||
* Latin Adultery
|
||||
* Latina Step Mom
|
||||
* Lesbian Girl on Girl
|
||||
* Live Gym Cam
|
||||
* Live Naughty Milf
|
||||
* Live Naughty Nurse
|
||||
* Live Naughty Secretary
|
||||
* Live Naughty Student
|
||||
* Live Naughty Teacher
|
||||
* Live Party Girl
|
||||
* Milf Sugar Babes Classic
|
||||
* My Dad's Hot Girlfriend
|
||||
* My Daughter's Hot Friend
|
||||
* My First Sex Teacher
|
||||
* My Friend's Hot Girl
|
||||
* My Friend's Hot Mom
|
||||
* My Girl Loves Anal
|
||||
* My Girlfriend's Busty Friend
|
||||
* My Naughty Latin Maid
|
||||
* My Naughty Massage
|
||||
* My Sister's Hot Friend
|
||||
* My Wife Is My Pornstar
|
||||
* My Wife's Hot Friend
|
||||
* Naughty America
|
||||
* Naughty Athletics
|
||||
* Naughty Bookworms
|
||||
* Naughty Country Girls
|
||||
* Naughty Flipside
|
||||
* Naughty Office
|
||||
* Naughty Rich Girls
|
||||
Naughty Weddings
|
||||
* Neighbor Affair
|
||||
* Open Family
|
||||
* Perfect Fucking Strangers Classic
|
||||
* Seduced By A Cougar
|
||||
* Sleazy Stepdad
|
||||
* Slut Step Mom
|
||||
* Slut Step Sister
|
||||
* Socal Coeds
|
||||
* Teens Love Cream
|
||||
* The Passenger
|
||||
* Tonight's Girlfriend
|
||||
* Watch Your Mom
|
||||
* Watch Your Wife
|
||||
* Wives on Vacation
|
||||
* **Perv City**
|
||||
* Anal Overdose
|
||||
* Banging Beauties
|
||||
* Chocolate BJs
|
||||
* Oral Overdose
|
||||
* Up Her Asshole
|
||||
* **Private**
|
||||
* Anal Introductions
|
||||
* Blacks on Sluts
|
||||
* I Confess Files
|
||||
* Mission: Ass Possible
|
||||
* Private Fetish
|
||||
* Private MILFs
|
||||
* Private Stars
|
||||
* Russian Fake Agent
|
||||
* Russian Teen Ass
|
||||
* Sex on the Beach
|
||||
* Tight and Teen
|
||||
* **Reality Kings**\*
|
||||
* 40 Inch Plus\*
|
||||
* 8th Street Latinas
|
||||
* Bad Tow Truck\*
|
||||
* Big Naturals
|
||||
* Big Tits Boss\*
|
||||
* Bikini Crashers\*
|
||||
* CFNM Secret\*
|
||||
* Captain Stabbin\*
|
||||
* Cum Fiesta
|
||||
* Cum Girls\*
|
||||
* Dangerous Dongs\*
|
||||
* Euro Sex Parties\*
|
||||
* Extreme Asses\*
|
||||
* Extreme Naturals\*
|
||||
* First Time Auditions\*
|
||||
* Flower Tucci\*
|
||||
* Girls of Naked\*
|
||||
* HD Love\*
|
||||
* Happy Tugs
|
||||
* Hot Bush\*
|
||||
* In the VIP\*
|
||||
* Mike in Brazil\*
|
||||
* Mike's Apartment\*
|
||||
* Milf Hunter
|
||||
* Milf Next Door\*
|
||||
* Moms Bang Teens
|
||||
* Moms Lick Teens
|
||||
* Money Talks
|
||||
* Monster Curves\*
|
||||
* No Faces\*
|
||||
* Pure 18\*
|
||||
* RK Prime\*
|
||||
* Real Orgasms\*
|
||||
* Round and Brown
|
||||
* Saturday Night Latinas\*
|
||||
* See My Wife\*
|
||||
* Sneaky Sex
|
||||
* Street BlowJobs\*
|
||||
* Team Squirt\*
|
||||
* Teens Love Huge Cocks
|
||||
* Top Shelf Pussy\*
|
||||
* Tranny Surprise
|
||||
* VIP Crew\*
|
||||
* We Live Together
|
||||
* Wives in Pantyhose\*
|
||||
* **Vixen**
|
||||
* Blacked
|
||||
* Blacked Raw
|
||||
* Tushy
|
||||
* Tushy Raw
|
||||
* Vixen
|
||||
* **XEmpire**
|
||||
* DarkX
|
||||
* EroticaX
|
||||
* HardX
|
||||
* LesbianX
|
||||
1121 channels on 83 networks, continuously expanding!
|
||||
|
||||
## Notes
|
||||
* **Naughty America**: Scene titles are not shown in NA's 'latest' overviews. They are derived from a hyperlink and will be stripped of any punctuation and capitalization. Individual scenes fetched by URL with `--scene` are not affected.
|
||||
* **Reality Kings**: Only RK sites without a `*` can be fetched individually by URL using `--scene`, as most RK sites do not show comprehensive details on their scene pages.
|
||||
* 21Naturals
|
||||
* 21Sextreme
|
||||
* 21Sextury
|
||||
* Adult Time
|
||||
* Amateur Allure
|
||||
* Amateur Euro
|
||||
* American Pornstar
|
||||
* Assylum
|
||||
* Aziani (Gangbang Creampies)
|
||||
* Babes
|
||||
* Bang!
|
||||
* Bang Bros
|
||||
* Blowpass
|
||||
* Brazzers
|
||||
* Burning Angel
|
||||
* Cherry Pimps
|
||||
* CzechAV
|
||||
* DDF Network / Porn World
|
||||
* Digital Playground
|
||||
* Dogfart Network
|
||||
* Dorcel
|
||||
* Elegant Angel
|
||||
* Evil Angel
|
||||
* Fake Hub
|
||||
* Fame Digital
|
||||
* Fantasy Massage
|
||||
* FCUK (Exploited College Girls)
|
||||
* First Anal Quest
|
||||
* ForBondage
|
||||
* Full Porn Network (Analized, James Deen)
|
||||
* Gaywire
|
||||
* Girlsway
|
||||
* Hitzefrei
|
||||
* Hookup Hotshot
|
||||
* Hush Pass
|
||||
* Hussie Pass
|
||||
* In The Crack
|
||||
* Insex
|
||||
* Interracial Pass
|
||||
* JayRock Productions
|
||||
* Jesse Loads Monster Facials
|
||||
* Jules Jordan
|
||||
* Karups
|
||||
* Kelly Madison Media (Teen Fidelity)
|
||||
* Killergram
|
||||
* Kink
|
||||
* LegalPorno
|
||||
* LetsDoeIt
|
||||
* Little Caprice Dreams
|
||||
* Mamacitaz
|
||||
* Men
|
||||
* Metro HD
|
||||
* Mike Adriano
|
||||
* Mile High Media
|
||||
* MOFOS
|
||||
* Naughty America
|
||||
* New Sensations
|
||||
* Nubiles
|
||||
* Pascal's Sub Sluts
|
||||
* Perfect Gonzo
|
||||
* Perv City
|
||||
* Pimp.XXX
|
||||
* Pinky XXX
|
||||
* Porn Pros
|
||||
* PornCZ
|
||||
* Private
|
||||
* Pure Taboo
|
||||
* Reality Kings
|
||||
* SCORE
|
||||
* Sexy Hub
|
||||
* Team Skeet
|
||||
* Teen Core Club
|
||||
* TransBella
|
||||
* Twistys
|
||||
* VIP Sex Vault
|
||||
* Vivid
|
||||
* Vixen
|
||||
* VogoV
|
||||
* Whale Member (Holed, POVD)
|
||||
* Wicked
|
||||
* XEmpire
|
||||
* ZTOD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,770 @@
|
||||
<template>
|
||||
<div class="actors">
|
||||
<Actor
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
:actor="actor"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="content"
|
||||
class="actors"
|
||||
>
|
||||
<nav
|
||||
ref="filters"
|
||||
class="filters"
|
||||
>
|
||||
<div class="filters-row">
|
||||
<ul class="genders nolist">
|
||||
<li class="gender">
|
||||
<router-link
|
||||
:to="{ name: 'actors', params: { gender: 'all', pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ selected: gender === 'all' }"
|
||||
class="gender-link all"
|
||||
>all</router-link>
|
||||
</li>
|
||||
<li class="gender">
|
||||
<router-link
|
||||
:to="{ name: 'actors', params: { gender: 'female', pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ selected: gender === 'female' }"
|
||||
class="gender-link female"
|
||||
><Gender gender="female" /></router-link>
|
||||
</li>
|
||||
<li class="gender">
|
||||
<router-link
|
||||
:to="{ name: 'actors', params: { gender: 'male', pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ selected: gender === 'male' }"
|
||||
class="gender-link male"
|
||||
replace
|
||||
><Gender gender="male" /></router-link>
|
||||
</li>
|
||||
<li class="gender">
|
||||
<router-link
|
||||
:to="{ name: 'actors', params: { gender: 'trans', pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ selected: gender === 'trans' }"
|
||||
class="gender-link transsexual"
|
||||
replace
|
||||
><Gender gender="transsexual" /></router-link>
|
||||
</li>
|
||||
<li class="gender">
|
||||
<router-link
|
||||
:to="{ name: 'actors', params: { gender: 'other', pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ selected: gender === 'other' }"
|
||||
class="gender-link other"
|
||||
replace
|
||||
><Icon icon="question5" /></router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="filters-attributes nolist">
|
||||
<li>
|
||||
<Tooltip class="filter boobs">
|
||||
<span
|
||||
class="filter-trigger"
|
||||
:class="{ enabled: ageRequired }"
|
||||
><Icon icon="vcard" />Age</span>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<RangeFilter
|
||||
label="age"
|
||||
:min="18"
|
||||
:max="100"
|
||||
:value="age"
|
||||
:disabled="!ageRequired"
|
||||
@enable="(checked) => updateValue('ageRequired', checked, true)"
|
||||
@input="(range) => updateValue('age', range, false)"
|
||||
@change="(range) => updateValue('age', range, true)"
|
||||
>
|
||||
<template v-slot:start><Icon icon="leaf" /></template>
|
||||
<template v-slot:end><Icon icon="tree3" /></template>
|
||||
</RangeFilter>
|
||||
|
||||
<div class="filter-section">
|
||||
<label class="filter-label">
|
||||
<span class="label">
|
||||
<Checkbox
|
||||
:checked="dobRequired"
|
||||
class="checkbox"
|
||||
@change="(checked) => updateValue('dobRequired', checked, true)"
|
||||
/>Date of birth
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="input-container"
|
||||
@click="() => updateValue('dobRequired', true, true)"
|
||||
>
|
||||
<input
|
||||
v-model="dob"
|
||||
:disabled="!dobRequired"
|
||||
type="date"
|
||||
class="input"
|
||||
@change="updateFilters"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Tooltip class="filter">
|
||||
<span
|
||||
class="filter-trigger boobs"
|
||||
:class="{ enabled: boobSizeRequired || naturalBoobs !== 1 }"
|
||||
><Icon icon="boobs" />Boobs</span>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<RangeFilter
|
||||
label="size"
|
||||
:min="0"
|
||||
:max="boobSizes.length - 1"
|
||||
:value="boobSize"
|
||||
:values="boobSizes"
|
||||
:disabled="!boobSizeRequired"
|
||||
@enable="(checked) => updateValue('boobSizeRequired', checked, true)"
|
||||
@input="(range) => updateValue('boobSize', range, false)"
|
||||
@change="(range) => updateValue('boobSize', range, true)"
|
||||
>
|
||||
<template v-slot:start><Icon icon="boobs-small" /></template>
|
||||
<template v-slot:end><Icon icon="boobs-big" /></template>
|
||||
</RangeFilter>
|
||||
|
||||
<div class="filter-section">
|
||||
<span class="filter-label">Enhanced</span>
|
||||
|
||||
<span
|
||||
:class="{ [['off', 'default', 'on'][naturalBoobs]]: true }"
|
||||
class="toggle-container noclick"
|
||||
>
|
||||
<span
|
||||
class="toggle-label off"
|
||||
@click="updateValue('naturalBoobs', 0)"
|
||||
><Icon icon="leaf" /></span>
|
||||
|
||||
<input
|
||||
v-model.number="naturalBoobs"
|
||||
class="toggle"
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
@change="updateFilters"
|
||||
>
|
||||
|
||||
<span
|
||||
class="toggle-label on"
|
||||
@click="updateValue('naturalBoobs', 2)"
|
||||
><Icon icon="magic-wand2" /></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Tooltip class="filter boobs">
|
||||
<span
|
||||
class="filter-trigger"
|
||||
:class="{ enabled: heightRequired || weightRequired }"
|
||||
><Icon icon="rulers" />Physique</span>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<RangeFilter
|
||||
label="height"
|
||||
:min="50"
|
||||
:max="220"
|
||||
:value="height"
|
||||
:disabled="!heightRequired"
|
||||
unit="cm"
|
||||
@enable="(checked) => updateValue('heightRequired', checked, true)"
|
||||
@input="(range) => updateValue('height', range, false)"
|
||||
@change="(range) => updateValue('height', range, true)"
|
||||
>
|
||||
<template v-slot:start><Icon icon="height-short" /></template>
|
||||
<template v-slot:end><Icon icon="height" /></template>
|
||||
</RangeFilter>
|
||||
|
||||
<RangeFilter
|
||||
label="weight"
|
||||
:min="30"
|
||||
:max="200"
|
||||
:value="weight"
|
||||
:disabled="!weightRequired"
|
||||
unit="kg"
|
||||
@enable="(checked) => updateValue('weightRequired', checked, true)"
|
||||
@input="(range) => updateValue('weight', range, false)"
|
||||
@change="(range) => updateValue('weight', range, true)"
|
||||
>
|
||||
<template v-slot:start><Icon icon="meter-slow" /></template>
|
||||
<template v-slot:end><Icon icon="meter-fast" /></template>
|
||||
</RangeFilter>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Tooltip class="filter">
|
||||
<span
|
||||
:class="{ enabled: country }"
|
||||
class="filter-trigger"
|
||||
><img
|
||||
v-if="$route.query.c"
|
||||
:src="`/img/flags/${$route.query.c.toLowerCase()}.svg`"
|
||||
class="flag"
|
||||
><Icon
|
||||
v-else
|
||||
icon="earth2"
|
||||
/>Country</span>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<input
|
||||
v-model="countryQuery"
|
||||
placeholder="Search"
|
||||
class="input"
|
||||
>
|
||||
|
||||
<Countries
|
||||
v-if="!countryQuery"
|
||||
:countries="topCountries"
|
||||
:selected-country="country"
|
||||
:update-value="updateValue"
|
||||
/>
|
||||
|
||||
<Countries
|
||||
:countries="filteredCountries"
|
||||
:selected-country="country"
|
||||
:update-value="updateValue"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<SearchBar :placeholder="`Search ${totalCount} actors`" />
|
||||
</nav>
|
||||
|
||||
<div
|
||||
ref="tiles"
|
||||
class="tiles"
|
||||
>
|
||||
<Actor
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
:actor="actor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
v-if="totalCount > 0"
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-bottom"
|
||||
/>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Actor from '../tile/actor.vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import Actor from './tile.vue';
|
||||
import Gender from './gender.vue';
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
import Countries from './countries.vue';
|
||||
import RangeFilter from './filter-range.vue';
|
||||
import SearchBar from '../search/bar.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
const toggleValues = [true, null, false];
|
||||
const boobSizes = 'ABCDEFGHIJKZ'.split('');
|
||||
const topCountries = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
||||
|
||||
function updateFilters() {
|
||||
this.$router.push({
|
||||
name: 'actors',
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
gender: this.gender,
|
||||
},
|
||||
query: {
|
||||
nb: this.naturalBoobs !== 1 ? this.naturalBoobs : undefined,
|
||||
bs: this.boobSizeRequired ? this.boobSize.join(',') : undefined,
|
||||
h: this.heightRequired ? this.height.join(',') : undefined,
|
||||
w: this.weightRequired ? this.weight.join(',') : undefined,
|
||||
c: this.country ? this.country : undefined,
|
||||
age: this.ageRequired ? this.age.join(',') : undefined,
|
||||
dob: this.dobRequired ? this.dob : undefined,
|
||||
query: this.$route.query.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateValue(prop, value, load = true) {
|
||||
this[prop] = value;
|
||||
|
||||
if (load) {
|
||||
this.updateFilters();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchActors(scroll) {
|
||||
const curatedGender = this.gender.replace('trans', 'transsexual');
|
||||
|
||||
const { actors, countries, totalCount } = await this.$store.dispatch('fetchActors', {
|
||||
limit: this.limit,
|
||||
pageNumber: Number(this.$route.params.pageNumber) || 1,
|
||||
query: this.$route.query.query,
|
||||
gender: curatedGender === 'other' ? null : curatedGender,
|
||||
age: this.ageRequired && this.age,
|
||||
dob: this.dobRequired && this.dob,
|
||||
boobSize: this.boobSizeRequired && this.boobSize,
|
||||
country: this.country,
|
||||
naturalBoobs: toggleValues[this.naturalBoobs] ?? null,
|
||||
height: this.heightRequired && this.height,
|
||||
weight: this.weightRequired && this.weight,
|
||||
});
|
||||
|
||||
const countriesByAlpha2 = countries.reduce((acc, country) => ({ ...acc, [country.alpha2]: country }), {});
|
||||
|
||||
this.actors = actors;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.countries = countries;
|
||||
this.topCountries = [...(this.country && !topCountries.includes(this.country) ? [this.country] : []), ...topCountries].map(alpha2 => countriesByAlpha2[alpha2]);
|
||||
|
||||
if (scroll) {
|
||||
this.$refs.tiles?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function filteredCountries() {
|
||||
const countryQueryExpression = new RegExp(this.countryQuery, 'i');
|
||||
|
||||
return this.countryQuery?.length > 0
|
||||
? this.countries.filter(country => countryQueryExpression.test(country.name) || countryQueryExpression.test(country.alpha2))
|
||||
: this.countries;
|
||||
}
|
||||
|
||||
function gender() {
|
||||
return this.$route.params.gender || 'all';
|
||||
}
|
||||
|
||||
async function route(to, from) {
|
||||
const scroll = to.params.pageNumber !== from.params.pageNumber
|
||||
|| to.params.gender !== from.params.gender;
|
||||
|
||||
await this.fetchActors(scroll);
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.pageTitle = 'Actors';
|
||||
this.actors = await this.$store.dispatch('fetchActors', { limit: 1000 });
|
||||
this.pageTitle = 'Actors';
|
||||
|
||||
await this.fetchActors();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actors: [],
|
||||
pageTitle: null,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
components: {
|
||||
Actor,
|
||||
Checkbox,
|
||||
Countries,
|
||||
Gender,
|
||||
RangeFilter,
|
||||
SearchBar,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actors: [],
|
||||
countries: [],
|
||||
topCountries: [],
|
||||
countryQuery: null,
|
||||
pageTitle: null,
|
||||
totalCount: 0,
|
||||
limit: 50,
|
||||
age: this.$route.query.age?.split(',') || [18, 100],
|
||||
ageRequired: !!this.$route.query.age,
|
||||
dob: this.$route.query.dob || dayjs().subtract(21, 'years').format('YYYY-MM-DD'),
|
||||
dobRequired: !!this.$route.query.dob,
|
||||
boobSizes,
|
||||
boobSize: this.$route.query.bs?.split(',') || ['A', 'Z'],
|
||||
boobSizeRequired: !!this.$route.query.bs,
|
||||
country: this.$route.query.c || null,
|
||||
naturalBoobs: Number(this.$route.query.nb) || 1,
|
||||
height: this.$route.query.h?.split(',').map(Number) || [50, 220],
|
||||
heightRequired: !!this.$route.query.h,
|
||||
weight: this.$route.query.w?.split(',').map(Number) || [30, 200],
|
||||
weightRequired: !!this.$route.query.w,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
gender,
|
||||
filteredCountries,
|
||||
},
|
||||
watch: {
|
||||
$route: route,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchActors,
|
||||
updateFilters,
|
||||
updateValue,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
<style lang="scss">
|
||||
.gender-link {
|
||||
&.selected .gender .icon {
|
||||
fill: var(--text-light);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.actors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, .5fr));
|
||||
grid-gap: 0 .5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
&:hover:not(.selected) {
|
||||
.gender .icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.actors {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||
.male .icon {
|
||||
filter: drop-shadow(0 0 1px var(--male));
|
||||
}
|
||||
|
||||
.female .icon {
|
||||
filter: drop-shadow(0 0 1px var(--female));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.selected) .transsexual .icon {
|
||||
fill: var(--female);
|
||||
filter: drop-shadow(1px 0 0 var(--text-light)) drop-shadow(-1px 0 0 var(--text-light)) drop-shadow(0 1px 0 var(--text-light)) drop-shadow(0 -1px 0 var(--text-light)) drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male)) drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.actors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
grid-template-rows: min-content;
|
||||
grid-gap: .5rem;
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 0;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.filters,
|
||||
.filters-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 1rem 0 .5rem 0;
|
||||
}
|
||||
|
||||
.filters-row,
|
||||
.filter {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.genders {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem 0 0;
|
||||
}
|
||||
|
||||
.gender {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.gender-link {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
margin: .25rem .5rem .25rem 0;
|
||||
color: var(--shadow);
|
||||
background: var(--background);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
|
||||
.male,
|
||||
.female,
|
||||
.transsexual {
|
||||
padding: .2rem 0 0 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--primary);
|
||||
color: var(--text-light);
|
||||
|
||||
&.other .icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
|
||||
.icon,
|
||||
.flag {
|
||||
fill: var(--shadow);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: -.1rem .75rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--shadow-strong);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-values {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.filter-split {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .75rem .5rem .5rem .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
.checkbox {
|
||||
margin: 0 .75rem 0 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.input-container {
|
||||
box-sizing: border-box;
|
||||
padding: 0 .5rem .5rem .5rem;
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-container,
|
||||
.range-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
&.on {
|
||||
.toggle-label.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--enabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
.toggle-label.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--disabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
min-width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
&.on {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
height: 1.25rem;
|
||||
appearance: none;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--shadow-hint);
|
||||
background-image: radial-gradient(circle, var(--shadow-weak) .3rem, transparent calc(.3rem + 1px));
|
||||
cursor: pointer;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
background: var(--disabled-handle);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: .625rem;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
appearance: none;
|
||||
background: var(--disabled-handle);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: none;
|
||||
border-radius: .625rem;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-mega) {
|
||||
.filters {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
::v-deep(.search) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.filters {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.filters-row {
|
||||
flex-direction: column;
|
||||
|
||||
.filter {
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-attributes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.genders {
|
||||
padding: 0;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
padding: .5rem 1rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
88
assets/components/actors/countries.vue
Normal file
88
assets/components/actors/countries.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<ul class="countries nolist">
|
||||
<li
|
||||
v-for="country in countries"
|
||||
:key="country.alpha2"
|
||||
:title="country.name"
|
||||
:class="{ selected: selectedCountry === country.alpha2 }"
|
||||
class="country"
|
||||
@click="updateValue('country', country.alpha2, true)"
|
||||
>
|
||||
<img
|
||||
:src="`/img/flags/${country.alpha2.toLowerCase()}.svg`"
|
||||
class="flag"
|
||||
>
|
||||
|
||||
<span class="country-name">{{ country.alias || country.name }}</span>
|
||||
|
||||
<Icon
|
||||
v-if="selectedCountry === country.alpha2"
|
||||
icon="cross2"
|
||||
@click.native.stop="updateValue('country', null, true)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
countries: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedCountry: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
updateValue: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.countries:not(:last-child) {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.country {
|
||||
width: 15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.flag {
|
||||
width: 1.25rem;
|
||||
padding: .25rem .5rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: .25rem .5rem;;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.country-name {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: .25rem .5rem;
|
||||
}
|
||||
</style>
|
||||
215
assets/components/actors/filter-range.vue
Normal file
215
assets/components/actors/filter-range.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<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 v-slot:start><slot name="start" /></template>
|
||||
<template v-slot:end><slot name="end" /></template>
|
||||
</Range>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
import Range from '../form/range.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
Range,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [0, 10],
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowEnable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['change', 'input', 'enable'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.filter-section {
|
||||
width: 15rem;
|
||||
max-width: 100%;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .75rem .5rem .5rem .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin: 0 .75rem 0 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label-values {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.filter-split {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-container,
|
||||
.range-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
&.on {
|
||||
.toggle-label.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--enabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
.toggle-label.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background-color: var(--disabled-background);
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
min-width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
|
||||
&.on {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&.on {
|
||||
color: var(--enabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--enabled);
|
||||
}
|
||||
}
|
||||
|
||||
&.off {
|
||||
color: var(--disabled);
|
||||
|
||||
.icon {
|
||||
fill: var(--disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
assets/components/actors/gender.vue
Normal file
41
assets/components/actors/gender.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="gender"
|
||||
class="gender"
|
||||
:class="{ [gender]: true }"
|
||||
><Icon :icon="gender" /></span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.gender {
|
||||
&.female .icon {
|
||||
fill: var(--female);
|
||||
filter: drop-shadow(0 0 1px var(--darken));
|
||||
}
|
||||
|
||||
&.male .icon {
|
||||
fill: var(--male);
|
||||
filter: drop-shadow(0 0 1px var(--darken));
|
||||
}
|
||||
|
||||
&.transsexual .icon {
|
||||
fill: var(--text-light);
|
||||
filter: drop-shadow(1px 0 0 var(--female)) drop-shadow(-1px 0 0 var(--female)) drop-shadow(0 1px 0 var(--female)) drop-shadow(0 -1px 0 var(--female))
|
||||
drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male))
|
||||
drop-shadow(0 0 1px var(--darken))
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +1,73 @@
|
||||
<template>
|
||||
<div
|
||||
class="photos"
|
||||
:class="{ wide: actor.photos.length > 2 }"
|
||||
>
|
||||
<a
|
||||
v-if="actor.avatar"
|
||||
:href="`/media/${actor.avatar.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="avatar-link photo-link"
|
||||
>
|
||||
<img
|
||||
:src="`/media/${actor.avatar.thumbnail}`"
|
||||
class="avatar photo"
|
||||
>
|
||||
</a>
|
||||
<div
|
||||
class="photos"
|
||||
:class="{
|
||||
avatar: !!actor.avatar,
|
||||
empty: actor.photos.length === 0,
|
||||
}"
|
||||
>
|
||||
<a
|
||||
v-if="actor.avatar"
|
||||
:href="getPath(actor.avatar)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="avatar-link photo-link"
|
||||
>
|
||||
<img
|
||||
:src="getPath(actor.avatar, 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(actor.avatar, 'lazy') }"
|
||||
:title="actor.avatar.credit && `© ${actor.avatar.credit}`"
|
||||
:width="actor.avatar.thumbnailWidth"
|
||||
:height="actor.avatar.thumbnailHeight"
|
||||
loading="lazy"
|
||||
class="avatar photo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-for="photo in actor.photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
:href="`/media/${photo.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="photo-link"
|
||||
>
|
||||
<img
|
||||
:src="`/media/${photo.thumbnail}`"
|
||||
class="photo"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
v-for="photo in photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
:href="getPath(photo)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="photo-link"
|
||||
>
|
||||
<img
|
||||
:src="getPath(photo, 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(photo, 'lazy') }"
|
||||
:title="`© ${photo.credit || photo.entity.name}`"
|
||||
:width="photo.thumbnailWidth"
|
||||
:height="photo.thumbnailHeight"
|
||||
loading="lazy"
|
||||
class="photo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function photos() {
|
||||
return this.actor.photos.filter(photo => !photo.entropy || photo.entropy > 5.5);
|
||||
}
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['load'],
|
||||
computed: {
|
||||
sfw,
|
||||
photos,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -47,59 +75,70 @@ export default {
|
||||
@import 'theme';
|
||||
|
||||
.photos {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(auto-fit, 12rem);
|
||||
grid-gap: .5rem;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 1rem;
|
||||
font-size: 0;
|
||||
|
||||
&.expanded {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
|
||||
.photo-link {
|
||||
margin: 0 .5rem .5rem 0;
|
||||
}
|
||||
|
||||
.photo {
|
||||
height: 18rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
.photo {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
padding: 0 .5rem 0 0;
|
||||
|
||||
&:last-child {
|
||||
padding: 0 1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 3px $shadow-weak;
|
||||
}
|
||||
|
||||
@media(min-width: $breakpoint3) {
|
||||
.photos.wide {
|
||||
max-width: 30vw;
|
||||
}
|
||||
height: 15rem;
|
||||
width: auto;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
object-fit: cover;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint3) {
|
||||
.photos {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
scrollbar-width: none;
|
||||
.photos {
|
||||
&.empty.avatar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
display: inline-block;
|
||||
}
|
||||
.avatar-link {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&.expanded {
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
height: 15rem;
|
||||
flex-shrink: 0;
|
||||
margin: 0 .5rem 0 0;
|
||||
@media(max-width: $breakpoint0) {
|
||||
.photos.expanded {
|
||||
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
86
assets/components/actors/preview.vue
Normal file
86
assets/components/actors/preview.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="actor nolink"
|
||||
>
|
||||
<div class="avatar">
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
:src="getPath(actor.avatar, 'lazy')"
|
||||
class="avatar-image"
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-else-if="actor.gender"
|
||||
:icon="actor.gender"
|
||||
class="avatar-fallback"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="name">{{ actor.name }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function unstashActor(actorId, stashId) {
|
||||
await this.$store.dispatch('unstashActor', { actorId, stashId });
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
unstashActor,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actor {
|
||||
height: 2.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: solid 1px var(--shadow-hint);
|
||||
|
||||
&:hover {
|
||||
border: solid 1px var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 100%;
|
||||
border-right: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 0;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 .5rem;
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="actor"
|
||||
class="social"
|
||||
>
|
||||
<a
|
||||
v-for="social in actor.social"
|
||||
:key="`social-${social.id}`"
|
||||
v-tooltip.bottom="social.url"
|
||||
:href="social.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="social-link"
|
||||
>
|
||||
<Icon
|
||||
v-if="social.platform"
|
||||
:icon="social.platform"
|
||||
/>
|
||||
<div
|
||||
v-if="actor"
|
||||
class="social"
|
||||
>
|
||||
<a
|
||||
v-for="social in actor.social"
|
||||
:key="`social-${social.id}`"
|
||||
v-tooltip.bottom="social.url"
|
||||
:href="social.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="social-link"
|
||||
>
|
||||
<Icon
|
||||
v-if="social.platform"
|
||||
:icon="social.platform"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
icon="link"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<Icon
|
||||
v-else
|
||||
icon="link"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -48,14 +48,14 @@ export default {
|
||||
padding: 0 0 0 1rem;
|
||||
|
||||
.icon {
|
||||
color: $highlight;
|
||||
fill: $highlight;
|
||||
color: var(--highlight);
|
||||
fill: var(--highlight);
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
fill: $primary;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
386
assets/components/actors/tile.vue
Normal file
386
assets/components/actors/tile.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="actor"
|
||||
class="actor"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{ name: 'actor', params: { actorId: actor.id, actorSlug: actor.slug } }"
|
||||
class="link"
|
||||
>
|
||||
<span
|
||||
class="handle"
|
||||
>
|
||||
<span
|
||||
v-tooltip.top="actor.name"
|
||||
class="name"
|
||||
>{{ actor.name }}</span>
|
||||
|
||||
<RouterLink
|
||||
v-if="actor.entity"
|
||||
v-tooltip="actor.entity.name"
|
||||
:to="{ name: actor.entity.type, params: { entitySlug: actor.entity.slug, range: 'new', pageNumber: 1 } }"
|
||||
class="favicon"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${actor.entity.slug}/favicon_dark.png`"
|
||||
class="favicon-icon"
|
||||
>
|
||||
</RouterLink>
|
||||
|
||||
<Icon
|
||||
v-if="alias"
|
||||
v-tooltip="`Alias of ${alias.name}`"
|
||||
icon="users3"
|
||||
class="favicon alias"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="actor.dateOfDeath"
|
||||
v-tooltip="`Died ${formatDate(actor.dateOfDeath, 'MMMM D, YYYY')}`"
|
||||
icon="tombstone"
|
||||
class="favicon died"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div class="avatar-container">
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
:src="getPath(actor.avatar, 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(actor.avatar, 'lazy') }"
|
||||
loading="lazy"
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="avatar"
|
||||
><img
|
||||
:src="`/img/avatar_${actor.gender || 'female'}.svg`"
|
||||
class="avatar-fallback"
|
||||
></span>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited"
|
||||
icon="heart7"
|
||||
class="stash stashed"
|
||||
@click.prevent.native="unstashActor"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited === false"
|
||||
icon="heart8"
|
||||
class="stash unstashed"
|
||||
@click.prevent.native="stashActor"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="stash && !stash.primary"
|
||||
icon="cross2"
|
||||
class="stash unstash"
|
||||
@click.prevent.native="unstashActor"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="details"
|
||||
>
|
||||
<span class="gender-age">
|
||||
<Gender :gender="actor.gender" />
|
||||
|
||||
<span
|
||||
v-if="actor.ageAtDeath"
|
||||
v-tooltip="`Born ${formatDate(actor.dateOfBirth, 'MMMM D, YYYY')}<br>Died ${formatDate(actor.dateOfDeath, 'MMMM D, YYYY')}`"
|
||||
class="age-death"
|
||||
>{{ actor.ageAtDeath }}</span>
|
||||
|
||||
<span
|
||||
v-else-if="actor.ageFromBirth"
|
||||
v-tooltip="`Born on ${formatDate(actor.dateOfBirth, 'MMMM D, YYYY')}`"
|
||||
class="age-now"
|
||||
>{{ actor.ageFromBirth }}</span>
|
||||
|
||||
<span
|
||||
v-else-if="actor.age"
|
||||
v-tooltip="`At least ${actor.age}`"
|
||||
class="age-now"
|
||||
>{{ actor.age }}</span>
|
||||
|
||||
<span
|
||||
v-if="actor.ageThen && actor.ageThen < actor.ageFromBirth"
|
||||
v-tooltip="`${actor.ageThen} years old on release date`"
|
||||
class="age-then"
|
||||
>{{ actor.ageThen }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="actor.origin"
|
||||
v-tooltip="`Born in ${actor.origin.country.alias || actor.origin.country.name}`"
|
||||
class="country"
|
||||
>
|
||||
{{ actor.origin.country.alpha2 }}
|
||||
<img
|
||||
class="flag"
|
||||
:src="`/img/flags/${actor.origin.country.alpha2.toLowerCase()}.svg`"
|
||||
>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="country"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Gender from './gender.vue';
|
||||
|
||||
async function stashActor() {
|
||||
this.favorited = true;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('stashActor', {
|
||||
actorId: this.actor.id,
|
||||
stashId: this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', true);
|
||||
} catch (error) {
|
||||
this.favorited = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unstashActor() {
|
||||
if (!this.stash || this.stash.primary) {
|
||||
this.favorited = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('unstashActor', {
|
||||
actorId: this.actor.id,
|
||||
stashId: this.stash?.id || this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', false);
|
||||
} catch (error) {
|
||||
this.favorited = true;
|
||||
}
|
||||
}
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Gender,
|
||||
},
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
alias: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['stash'],
|
||||
data() {
|
||||
return {
|
||||
favorited: this.actor.isFavorited,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sfw,
|
||||
},
|
||||
methods: {
|
||||
stashActor,
|
||||
unstashActor,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.actor {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
background: var(--info);
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
padding-bottom: 150%;
|
||||
}
|
||||
|
||||
&:hover .unstashed,
|
||||
&:hover .unstash {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--text-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 0 3px var(--shadow);
|
||||
|
||||
.name {
|
||||
padding: .25rem .5rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.alias {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.favicon {
|
||||
font-size: 0;
|
||||
padding: .5rem .25rem;
|
||||
|
||||
&:last-child {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
&.died {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.favicon-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--profile);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
color: var(--darken-weak);
|
||||
background: var(--darken-hint);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
object-position: center 0;
|
||||
background-size: cover;
|
||||
background-position: center 0;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
opacity: .1;
|
||||
}
|
||||
|
||||
.stash {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: .25rem .25rem .5rem .5rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
fill: var(--lighten-weak);
|
||||
filter: drop-shadow(0 0 2px var(--darken));
|
||||
|
||||
&:hover.unstashed,
|
||||
&.stashed {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
&:hover.unstash {
|
||||
fill: var(--text-light);
|
||||
filter: drop-shadow(0 0 2px var(--darken-weak));
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
background: var(--darken);
|
||||
color: var(--text-light);
|
||||
width: 100%;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gender-age {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gender {
|
||||
margin: .25rem .25rem 0 0;
|
||||
}
|
||||
|
||||
.country {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flag {
|
||||
height: .75rem;
|
||||
margin: 0 0 0 .5rem;
|
||||
}
|
||||
|
||||
.age-now,
|
||||
.age-death {
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
.age-then {
|
||||
color: var(--lighten);
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.name {
|
||||
font-size: .9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
295
assets/components/album/album.vue
Normal file
295
assets/components/album/album.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div class="album">
|
||||
<div class="album-header">
|
||||
<h3 class="album-title">{{ title }}</h3>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="close"
|
||||
@click.native="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="album-body">
|
||||
<div
|
||||
v-if="entity || tag"
|
||||
class="campaign-container"
|
||||
>
|
||||
<Campaign
|
||||
:entity="entity"
|
||||
:tag="tag"
|
||||
:min-ratio="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="album-items"
|
||||
:class="{ portrait }"
|
||||
>
|
||||
<div
|
||||
v-for="item in albumItems"
|
||||
:key="item.id"
|
||||
class="item-container"
|
||||
>
|
||||
<a
|
||||
:href="getPath(item, null, { local })"
|
||||
class="item-link"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
:src="getPath(item, 'thumbnail', { local })"
|
||||
:style="{ 'background-image': getBgPath(item, 'lazy', { local }) }"
|
||||
:width="item.thumbnailWidth"
|
||||
:height="item.thumbnailHeight"
|
||||
:title="item.title"
|
||||
loading="lazy"
|
||||
class="item image"
|
||||
>
|
||||
|
||||
<Logo :photo="item" />
|
||||
|
||||
<router-link
|
||||
v-if="comments && item.title && item.entity"
|
||||
:to="`/${item.entity.type}/${item.entity.slug}`"
|
||||
class="item-comment"
|
||||
>{{ item.title }} for {{ item.entity.name }}</router-link>
|
||||
|
||||
<span
|
||||
v-else-if="comments && item.title"
|
||||
class="item-comment"
|
||||
>{{ item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logo from './logo.vue';
|
||||
import Campaign from '../campaigns/campaign.vue';
|
||||
|
||||
function albumItems() {
|
||||
return this.items
|
||||
.filter(Boolean)
|
||||
.map(item => ({
|
||||
...item,
|
||||
title: item.comment || (item.credit && `© ${item.credit}`) || (item.entity && `© ${item.entity.name}`),
|
||||
}));
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
Campaign,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
local: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portrait: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
comments: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
albumItems,
|
||||
},
|
||||
emits: ['close'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.album {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--darken-extreme);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.album-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.album-title {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
color: var(--text-light);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.close {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 1rem;
|
||||
fill: var(--lighten);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.album-body {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.campaign-container {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.album-items {
|
||||
height: 0;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
||||
grid-gap: 0 1rem;
|
||||
padding: 1rem;
|
||||
margin: auto 0;
|
||||
|
||||
&.portrait {
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-link {
|
||||
position: relative;
|
||||
margin: 0 0 .5rem 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
.item-comment {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
::v-deep(.album-logo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.item-comment {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
color: var(--text-light);
|
||||
background: var(--darken);
|
||||
font-size: .9rem;
|
||||
text-shadow: 0 0 3px var(--darken);
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
line-height: 1.25;
|
||||
transform: translateY(100%);
|
||||
transition: transform .25s ease;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-giga) {
|
||||
.album-items {
|
||||
grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));
|
||||
|
||||
.portrait {
|
||||
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-mega) {
|
||||
.album-items.portrait {
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.album-items {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
grid-gap: 0 .5rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.item-link {
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.album-items {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.album-items {
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-pico) {
|
||||
.album-items {
|
||||
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
assets/components/album/logo.vue
Normal file
66
assets/components/album/logo.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<router-link
|
||||
v-if="photo.entity"
|
||||
v-tooltip="photo.entity.name"
|
||||
:to="`/${photo.entity.type}/${photo.entity.slug}`"
|
||||
>
|
||||
<img
|
||||
v-if="favicon && photo.entity.type !== 'network' && !photo.entity.independent && photo.entity.parent"
|
||||
:src="`/img/logos/${photo.entity.parent.slug}/favicon.png`"
|
||||
class="album-logo favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="favicon"
|
||||
:src="`/img/logos/${photo.entity.slug}/favicon.png`"
|
||||
class="album-logo favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="photo.entity.type !== 'network' && !photo.entity.independent && photo.entity.parent"
|
||||
:src="`/img/logos/${photo.entity.parent.slug}/${photo.entity.slug}.png`"
|
||||
class="album-logo"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="`/img/logos/${photo.entity.slug}/network.png`"
|
||||
class="album-logo"
|
||||
>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: {
|
||||
photo: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
favicon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.album-logo {
|
||||
max-height: .75rem;
|
||||
max-width: 5rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: .5rem;
|
||||
transition: transform .25s ease, opacity .25s ease;
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak));
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
.album-logo:not(.favicon) {
|
||||
max-height: .5rem;
|
||||
max-width: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
441
assets/components/alerts/add.vue
Normal file
441
assets/components/alerts/add.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="Add alert"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<form
|
||||
class="dialog-body"
|
||||
@submit.prevent="addAlert"
|
||||
>
|
||||
<div class="dialog-section">
|
||||
<h3 class="dialog-heading">
|
||||
When<span class="dialog-description">All to appear in the same scene</span>
|
||||
</h3>
|
||||
|
||||
<div class="alert-section">
|
||||
<h4
|
||||
v-if="actors.length > 1"
|
||||
class="alert-heading"
|
||||
>Actors</h4>
|
||||
|
||||
<h4
|
||||
v-else
|
||||
class="alert-heading"
|
||||
>Actor</h4>
|
||||
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="actor"
|
||||
>
|
||||
<ActorPreview :actor="actor" />
|
||||
|
||||
<Icon
|
||||
icon="cross3"
|
||||
class="remove"
|
||||
@click.native="removeActor(actor)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<Tooltip>
|
||||
<li class="actor placeholder"><template v-if="actors.length === 0">Any actor</template>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<template #tooltip>
|
||||
<Search
|
||||
content="actors"
|
||||
@select="actor => addActor(actor)"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert-section">
|
||||
<h4
|
||||
v-if="actors.length > 1"
|
||||
class="alert-heading"
|
||||
>Do</h4>
|
||||
|
||||
<h4
|
||||
v-else
|
||||
class="alert-heading"
|
||||
>Does</h4>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="tag in tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="tag"
|
||||
>{{ tag.name }}
|
||||
<Icon
|
||||
icon="cross3"
|
||||
class="remove"
|
||||
@click.native="removeTag(tag)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<Tooltip>
|
||||
<li class="tag placeholder"><template v-if="tags.length === 0">Any type of scene</template>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<template #tooltip>
|
||||
<Search
|
||||
content="tags"
|
||||
:defaults="['anal', 'blowbang', 'mfm', 'dp', 'gangbang', 'airtight']"
|
||||
@select="tag => addTag(tag)"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert-section">
|
||||
<h4 class="alert-heading">For</h4>
|
||||
|
||||
<div class="entities">
|
||||
<div
|
||||
v-if="entity"
|
||||
class="entity"
|
||||
>
|
||||
<Entity :entity="entity" />
|
||||
|
||||
<Icon
|
||||
icon="cross3"
|
||||
class="remove"
|
||||
@click.native="removeEntity(entity)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tooltip v-if="!entity">
|
||||
<div class="entity placeholder">
|
||||
Any channel
|
||||
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #tooltip>
|
||||
<Search
|
||||
label="Search channels"
|
||||
content="entities"
|
||||
@select="entity => addEntity(entity)"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section">
|
||||
<h3 class="dialog-heading">Then</h3>
|
||||
|
||||
<label class="alert-label">
|
||||
<Checkbox
|
||||
:checked="notify"
|
||||
@change="checked => notify = checked"
|
||||
/>Notify me in traxxx
|
||||
</label>
|
||||
|
||||
<!--
|
||||
<label class="alert-label">
|
||||
<Checkbox
|
||||
:checked="email"
|
||||
@change="checked => email = checked"
|
||||
/>Send me an e-mail
|
||||
</label>
|
||||
-->
|
||||
|
||||
<div class="stashes-container">
|
||||
<ul class="stashes nolist">
|
||||
<li
|
||||
v-for="stash in stashes"
|
||||
:key="`stash-${stash.id}`"
|
||||
class="stash"
|
||||
>{{ stash.name }}
|
||||
|
||||
<Icon
|
||||
icon="cross3"
|
||||
class="remove"
|
||||
@click.native="removeStash(stash)"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<Tooltip>
|
||||
<li class="stash placeholder">
|
||||
Add to stash
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<template #tooltip>
|
||||
<Search
|
||||
content="stashes"
|
||||
@select="stash => addStash(stash)"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions right">
|
||||
<button
|
||||
:disabled="actors.length === 0 && tags.length === 0 && !entity"
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Add alert</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActorPreview from '../actors/preview.vue';
|
||||
import Entity from '../entities/tile.vue';
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
import Search from './search.vue';
|
||||
|
||||
async function addAlert() {
|
||||
await this.$store.dispatch('addAlert', {
|
||||
actors: this.actors.map((actor) => actor.id),
|
||||
tags: this.tags.map((tag) => tag.id),
|
||||
entity: this.entity?.id,
|
||||
notify: this.notify,
|
||||
email: this.email,
|
||||
stashes: this.stashes.map((stash) => stash.id),
|
||||
});
|
||||
|
||||
this.$emit('close', true);
|
||||
}
|
||||
|
||||
function addActor(actor) {
|
||||
if (!this.actors.some((selectedActor) => selectedActor.id === actor.id)) {
|
||||
this.actors = this.actors.concat(actor);
|
||||
}
|
||||
|
||||
this.events.emit('blur');
|
||||
}
|
||||
|
||||
function addEntity(entity) {
|
||||
this.entity = entity;
|
||||
this.events.emit('blur');
|
||||
}
|
||||
|
||||
function addTag(tag) {
|
||||
if (!this.tags.some((selectedTag) => selectedTag.id === tag.id)) {
|
||||
this.tags = this.tags.concat(tag);
|
||||
}
|
||||
|
||||
this.events.emit('blur');
|
||||
}
|
||||
|
||||
function removeActor(actor) {
|
||||
this.actors = this.actors.filter((listedActor) => listedActor.id !== actor.id);
|
||||
}
|
||||
|
||||
function removeEntity() {
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
function removeTag(tag) {
|
||||
this.tags = this.tags.filter((listedTag) => listedTag.id !== tag.id);
|
||||
}
|
||||
|
||||
function addStash(stash) {
|
||||
if (!this.stashes.some((selectedStash) => selectedStash.id === stash.id)) {
|
||||
this.stashes = this.stashes.concat(stash);
|
||||
}
|
||||
|
||||
this.events.emit('blur');
|
||||
}
|
||||
|
||||
function removeStash(stash) {
|
||||
this.stashes = this.stashes.filter((listedStash) => listedStash.id !== stash.id);
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActorPreview,
|
||||
Checkbox,
|
||||
Entity,
|
||||
Search,
|
||||
},
|
||||
emits: ['close'],
|
||||
data() {
|
||||
return {
|
||||
actors: [],
|
||||
tags: [],
|
||||
entity: null,
|
||||
notify: true,
|
||||
email: false,
|
||||
stashes: [],
|
||||
availableStashes: this.$store.state.auth.user.stashes,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addActor,
|
||||
addAlert,
|
||||
addEntity,
|
||||
addTag,
|
||||
addStash,
|
||||
removeActor,
|
||||
removeEntity,
|
||||
removeTag,
|
||||
removeStash,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-section {
|
||||
width: 30rem;
|
||||
max-width: 100%;
|
||||
|
||||
&:first-child {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 .25rem 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.dialog-description {
|
||||
color: var(--shadow);
|
||||
font-size: .9rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
margin: .75rem 0 .25rem 0;
|
||||
}
|
||||
|
||||
.actors,
|
||||
.entities,
|
||||
.tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.actors > .actor,
|
||||
.entity,
|
||||
.tag,
|
||||
.stash {
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
margin: 0 .5rem .5rem 0;
|
||||
}
|
||||
|
||||
.entity .tile {
|
||||
width: 10rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.tag:not(.placeholder),
|
||||
.stash:not(.placeholder) {
|
||||
padding: .5rem .75rem;
|
||||
border: solid 1px var(--shadow-hint);
|
||||
margin: 0 .75rem 0 0;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stashes {
|
||||
margin: 0 0 0 .25rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.remove {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
top: -.35rem;
|
||||
right: -.35rem;
|
||||
z-index: 1;
|
||||
border: solid 1px var(--darken-hint);
|
||||
border-radius: 50%;
|
||||
background: var(--background);
|
||||
fill: var(--shadow-weak);
|
||||
box-shadow: 0 0 1px var(--shadow);
|
||||
|
||||
&:hover {
|
||||
fill: var(--text-light);
|
||||
background: var(--primary);
|
||||
border: solid 1px var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--shadow-strong);
|
||||
padding: .75rem 0;
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
|
||||
.add {
|
||||
fill: var(--shadow);
|
||||
margin: 0 0 0 .5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
.add {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
margin: 0 0 .25rem 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stashes-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
|
||||
.alert-label {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.check-container {
|
||||
display: inline-block;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
</style>
|
||||
41
assets/components/alerts/remove.vue
Normal file
41
assets/components/alerts/remove.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="Remove alert"
|
||||
@close="$emit('close', false)"
|
||||
>
|
||||
<form
|
||||
class="dialog-body"
|
||||
@submit.prevent="removeAlert"
|
||||
>
|
||||
Are you sure you want to remove alert?
|
||||
|
||||
<div class="dialog-actions right">
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function removeAlert() {
|
||||
await this.$store.dispatch('removeAlert', this.alert.id);
|
||||
|
||||
this.$emit('close', true);
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
alert: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
removeAlert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
147
assets/components/alerts/search.vue
Normal file
147
assets/components/alerts/search.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
ref="input"
|
||||
v-model="query"
|
||||
:placeholder="label || `Search ${content}`"
|
||||
class="input"
|
||||
@input="search"
|
||||
>
|
||||
|
||||
<ul
|
||||
v-if="results.length > 0"
|
||||
class="nolist"
|
||||
>
|
||||
<li
|
||||
v-for="result in results"
|
||||
:key="`result-${result.id}`"
|
||||
class="result"
|
||||
@click="selectResult(result)"
|
||||
>
|
||||
<Icon
|
||||
v-if="result.type === 'network'"
|
||||
v-tooltip="'Network'"
|
||||
icon="device_hub"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="result.type === 'channel'"
|
||||
v-tooltip="'Channel'"
|
||||
icon="tv"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-if="result.avatar"
|
||||
:src="getPath(result.avatar)"
|
||||
class="avatar"
|
||||
>{{ result.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function search() {
|
||||
if (this.content === 'actors') {
|
||||
const { actors } = await this.$store.dispatch('fetchActors', {
|
||||
query: this.query,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
this.results = actors;
|
||||
}
|
||||
|
||||
if (this.content === 'entities') {
|
||||
this.results = await this.$store.dispatch('searchEntities', {
|
||||
query: this.query,
|
||||
limit: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.content === 'tags') {
|
||||
this.results = await this.$store.dispatch('searchTags', {
|
||||
query: this.query,
|
||||
minLength: 1,
|
||||
limit: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.content === 'stashes') {
|
||||
this.results = this.$store.state.auth.user.stashes.filter(stash => new RegExp(this.query).test(stash.name));
|
||||
}
|
||||
}
|
||||
|
||||
function selectResult(item) {
|
||||
this.query = null;
|
||||
this.results = [];
|
||||
|
||||
this.$emit('select', item);
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.$refs.input.focus();
|
||||
|
||||
if (this.defaults.length > 0 && this.content === 'tags') {
|
||||
this.results = await this.$store.dispatch('fetchTags', {
|
||||
slugs: this.defaults,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.content === 'stashes') {
|
||||
this.results = this.$store.state.auth.user.stashes;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
defaults: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
results: [],
|
||||
};
|
||||
},
|
||||
emits: ['select'],
|
||||
mounted,
|
||||
methods: {
|
||||
search,
|
||||
selectResult,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.result {
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 2rem;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: -.1rem .75rem 0 0;
|
||||
}
|
||||
</style>
|
||||
124
assets/components/auth/login.vue
Normal file
124
assets/components/auth/login.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<form
|
||||
class="login"
|
||||
@submit.prevent="login"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
class="feedback error"
|
||||
>{{ error }}</div>
|
||||
|
||||
<div
|
||||
v-if="success"
|
||||
class="feedback success"
|
||||
>Login successful, redirecting</div>
|
||||
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="username"
|
||||
placeholder="Username or e-mail"
|
||||
type="text"
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
|
||||
<input
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Log in</button>
|
||||
|
||||
<RouterLink
|
||||
v-if="$store.state.auth.signup"
|
||||
:to="{ name: 'signup', query: { ref: $route.query.ref } }"
|
||||
class="link link-external signup"
|
||||
>Sign up</RouterLink>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function login() {
|
||||
this.error = null;
|
||||
this.success = false;
|
||||
|
||||
try {
|
||||
const user = await this.$store.dispatch('login', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
this.success = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.$router.replace(this.$route.query.ref || { name: 'user', params: { username: user.username } });
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
if (!this.$store.state.auth.login) {
|
||||
this.$router.replace({ name: 'not-found' });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: null,
|
||||
password: null,
|
||||
success: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
login,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
|
||||
.signup {
|
||||
padding: .5rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
134
assets/components/auth/signup.vue
Normal file
134
assets/components/auth/signup.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<form
|
||||
class="signup"
|
||||
@submit.prevent="signup"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
class="feedback error"
|
||||
>{{ error }}</div>
|
||||
|
||||
<div
|
||||
v-if="success"
|
||||
class="feedback success"
|
||||
>Signup successful, redirecting</div>
|
||||
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="username"
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
|
||||
<input
|
||||
v-model="email"
|
||||
placeholder="E-mail"
|
||||
type="email"
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
|
||||
<input
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
class="input"
|
||||
required
|
||||
>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Sign up</button>
|
||||
|
||||
<RouterLink
|
||||
v-if="$store.state.auth.login"
|
||||
:to="{ name: 'login', query: { ref: $route.query.ref } }"
|
||||
class="link link-external login"
|
||||
>Log in</RouterLink>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function signup() {
|
||||
this.error = null;
|
||||
this.success = false;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('signup', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
this.success = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.$router.replace(this.$route.query.ref || { name: 'home' });
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
if (!this.$store.state.auth.signup) {
|
||||
this.$router.replace({ name: 'not-found' });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: null,
|
||||
email: null,
|
||||
password: null,
|
||||
success: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
signup,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.signup {
|
||||
width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
|
||||
.login {
|
||||
padding: .5rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
152
assets/components/campaigns/campaign.vue
Normal file
152
assets/components/campaigns/campaign.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<a
|
||||
v-if="campaign"
|
||||
:href="campaign.url || campaign.affiliate?.url"
|
||||
target="_blank"
|
||||
class="campaign"
|
||||
>
|
||||
<img
|
||||
v-if="campaign.banner.entity.type === 'network' || !campaign.banner.entity.parent"
|
||||
:src="`/img/banners/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="campaign.banner.entity.type === 'channel' && campaign.banner.entity.parent?.type === 'network'"
|
||||
:src="`/img/banners/${campaign.banner.entity.parent.slug}/${campaign.banner.entity.slug}/${campaign.banner.id}.${campaign.banner.type || 'jpg'}`"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
class="campaign-banner"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function ratioFilter(banner) {
|
||||
if (!banner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.minHeight && banner.height < this.minHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.maxHeight && banner.height > this.minHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.minRatio && banner.ratio < this.minRatio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.maxRatio && banner.ratio > this.maxRatio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function entityCampaign() {
|
||||
const bannerCampaigns = this.entity.campaigns
|
||||
.concat(this.entity.children?.flatMap(child => child.campaigns))
|
||||
.concat(this.entity.parent?.campaigns)
|
||||
.filter(campaignX => campaignX && this.ratioFilter(campaignX.banner));
|
||||
|
||||
if (bannerCampaigns.length > 0) {
|
||||
const randomCampaign = bannerCampaigns[Math.floor(Math.random() * bannerCampaigns.length)];
|
||||
|
||||
this.$emit('campaign', randomCampaign);
|
||||
return randomCampaign;
|
||||
}
|
||||
|
||||
this.$emit('campaign', null);
|
||||
return null;
|
||||
}
|
||||
|
||||
function tagCampaign() {
|
||||
const campaignBanners = this.tag.banners.filter(banner => banner.campaigns.length > 0 && this.ratioFilter(banner));
|
||||
const banner = campaignBanners[Math.floor(Math.random() * campaignBanners.length)];
|
||||
|
||||
if (banner?.campaigns.length > 0) {
|
||||
const randomCampaign = {
|
||||
...banner.campaigns[Math.floor(Math.random() * banner.campaigns.length)],
|
||||
banner,
|
||||
};
|
||||
|
||||
this.$emit('campaign', randomCampaign);
|
||||
return randomCampaign;
|
||||
}
|
||||
|
||||
this.$emit('campaign', null);
|
||||
return null;
|
||||
}
|
||||
|
||||
function campaign() {
|
||||
if (this.entity) {
|
||||
return this.entityCampaign();
|
||||
}
|
||||
|
||||
if (this.tag) {
|
||||
return this.tagCampaign();
|
||||
}
|
||||
|
||||
this.$emit('campaign', null); // allow parent to toggle campaigns depending on availability
|
||||
return null;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
minHeight: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
maxHeight: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
minRatio: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
maxRatio: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['campaign'],
|
||||
computed: {
|
||||
campaign,
|
||||
},
|
||||
methods: {
|
||||
entityCampaign,
|
||||
ratioFilter,
|
||||
tagCampaign,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.campaign {
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,137 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Header />
|
||||
<div class="container">
|
||||
<Warning
|
||||
v-if="showWarning"
|
||||
class="warning-container"
|
||||
@enter="(includeQueer) => setConsent(true, includeQueer)"
|
||||
@leave="setConsent(false)"
|
||||
/>
|
||||
|
||||
<div class="content">
|
||||
<!-- key forces rerender when new and old path use same component -->
|
||||
<router-view :key="$route.fullPath" />
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<Sidebar
|
||||
v-if="showSidebar"
|
||||
@toggle-sidebar="(state) => toggleSidebar(state)"
|
||||
@show-filters="(state) => toggleFilters(state)"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
<Header
|
||||
@toggle-sidebar="(state) => toggleSidebar(state)"
|
||||
@show-filters="(state) => toggleFilters(state)"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="config.showDisclaimer"
|
||||
class="disclaimer"
|
||||
>{{ config.disclaimer }}</p>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="content"
|
||||
@scroll="scroll"
|
||||
>
|
||||
<router-view @scroll="scrollToTop" />
|
||||
</div>
|
||||
|
||||
<Filters
|
||||
v-if="showFilters"
|
||||
@close="toggleFilters(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Warning from './warning.vue';
|
||||
import Header from '../header/header.vue';
|
||||
import Sidebar from '../sidebar/sidebar.vue';
|
||||
import Filters from '../filters/filters.vue';
|
||||
|
||||
function toggleSidebar(state) {
|
||||
this.showSidebar = typeof state === 'boolean' ? state : !this.showSidebar;
|
||||
}
|
||||
|
||||
function toggleFilters(state) {
|
||||
this.showFilters = state;
|
||||
this.showSidebar = false;
|
||||
}
|
||||
|
||||
async function setConsent(consent, includeQueer) {
|
||||
if (consent) {
|
||||
this.showWarning = false;
|
||||
localStorage.setItem('consent', window.env.sessionId);
|
||||
}
|
||||
|
||||
if (includeQueer) {
|
||||
this.$store.dispatch('setTagFilter', this.$store.state.ui.tagFilter.filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch('setTagFilter', this.$store.state.ui.tagFilter.concat(['gay', 'bisexual', 'transsexual']));
|
||||
}
|
||||
|
||||
function blur(event) {
|
||||
this.events.emit('blur', event);
|
||||
}
|
||||
|
||||
function resize(event) {
|
||||
this.events.emit('resize', event);
|
||||
}
|
||||
|
||||
function scroll(event) {
|
||||
this.events.emit('scroll', event);
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
this.$refs.content.scrollTop = 0;
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
document.addEventListener('click', this.blur);
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
function beforeUnmount() {
|
||||
document.removeEventListener('click', this.blur);
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Header,
|
||||
},
|
||||
components: {
|
||||
Header,
|
||||
Sidebar,
|
||||
Warning,
|
||||
Filters,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
showWarning: localStorage.getItem('consent') !== window.env.sessionId,
|
||||
showFilters: false,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
beforeUnmount,
|
||||
methods: {
|
||||
toggleSidebar,
|
||||
toggleFilters,
|
||||
setConsent,
|
||||
blur,
|
||||
resize,
|
||||
scroll,
|
||||
scrollToTop,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'theme';
|
||||
|
||||
.container {
|
||||
background: $background-dim;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--background-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -35,11 +139,57 @@ export default {
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
&.sidebar-container {
|
||||
transition: background .15s ease-in-out;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transition: transform .15s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
&.sidebar-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
padding: 0 1rem;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.disclaimer {
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
color: var(--text-light);
|
||||
background: var(--warn);
|
||||
font-weight: bold;
|
||||
box-shadow: inset 0 0 3px var(--darken-weak);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
268
assets/components/container/warning.vue
Normal file
268
assets/components/container/warning.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="warning-container">
|
||||
<div class="warning">
|
||||
<strong class="title">
|
||||
<span
|
||||
class="logo"
|
||||
v-html="logo"
|
||||
/>contains sexually explicit content
|
||||
</strong>
|
||||
|
||||
<ul class="rules">
|
||||
<li class="rule">You are at least 18 years old, and legally permitted to view adult material in your jurisdiction.</li>
|
||||
<li class="rule">You do not regard erotic and frankly pornographic media as obscene or offensive.</li>
|
||||
<li class="rule">You understand that most sexual scenarios depicted on this website are fictional, performed by professional actors for the purpose of entertainment, and not representative of real-life interactions.</li>
|
||||
</ul>
|
||||
|
||||
<span class="preferences">You can adjust your content preferences later</span>
|
||||
|
||||
<div class="actions">
|
||||
<a
|
||||
href="https://www.google.com"
|
||||
class="button leave"
|
||||
@click="$emit('leave')"
|
||||
><Icon icon="arrow-left16" />Leave</a>
|
||||
|
||||
<button
|
||||
class="button enter queer"
|
||||
@click="$emit('enter', true)"
|
||||
>
|
||||
<span class="button-title">Enter</span>
|
||||
<span class="button-sub">I want to see gay, bi and trans content</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="button enter straight"
|
||||
@click="$emit('enter', false)"
|
||||
>
|
||||
<span class="button-title">Enter</span>
|
||||
<span class="button-sub">I prefer straight content</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logo from '../../img/logo.svg';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
logo,
|
||||
};
|
||||
},
|
||||
emits: ['enter'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.warning-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: var(--background-censor);
|
||||
backdrop-filter: blur(.25rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--text);
|
||||
width: 50rem;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.copy,
|
||||
.rules {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 6.5rem;
|
||||
display: inline-block;
|
||||
fill: var(--primary);
|
||||
margin: 0 .6rem 0 0;
|
||||
|
||||
::v-deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
display: block;
|
||||
padding: 0 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rules {
|
||||
margin: 0 0 0 1rem;
|
||||
text-align: left;
|
||||
text-shadow: 0 0 3px var(--highlight-strong);
|
||||
}
|
||||
|
||||
.rule {
|
||||
padding: .5rem 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
flex-basis: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: .25rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
text-decoration: none;
|
||||
transition: border-radius .2s ease;
|
||||
|
||||
&.leave {
|
||||
color: var(--shadow-strong);
|
||||
flex-direction: row;
|
||||
padding: 1rem;
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin: 0 1rem 0 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.enter {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.straight,
|
||||
&.queer {
|
||||
color: var(--lighten-strong);
|
||||
background: var(--darken-censor);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: calc(100% + .3rem);
|
||||
height: calc(100% + .25rem);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
filter: blur(.25rem);
|
||||
transition: filter .2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text-light);
|
||||
border-radius: 0;
|
||||
|
||||
.button-sub {
|
||||
color: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.straight:before {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&.queer:before {
|
||||
background: linear-gradient(90deg, #f00, #f80, #ff0, #0f0, #00f, #a0f, #fff, #f8f, #0ff);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
fill: var(--text);
|
||||
}
|
||||
|
||||
&.straight:before,
|
||||
&.queer:before {
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-title,
|
||||
.button-sub {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button-title {
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 0 .15rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.button-sub {
|
||||
display: block;
|
||||
font-size: .8rem;
|
||||
padding: .15rem .5rem .75rem .5rem;
|
||||
color: var(--lighten);
|
||||
}
|
||||
|
||||
.preferences {
|
||||
color: var(--shadow);
|
||||
display: block;
|
||||
padding: .5rem 0 1rem 0;
|
||||
text-align: center;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 5.75rem;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
.actions {
|
||||
flex-direction: column-reverse;
|
||||
padding: 0;
|
||||
|
||||
.button {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.button:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
assets/components/dialog/dialog.vue
Normal file
139
assets/components/dialog/dialog.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div
|
||||
class="container"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
@click.stop="events.emit('blur')"
|
||||
>
|
||||
<div
|
||||
v-if="title || $slots.header"
|
||||
class="header"
|
||||
>
|
||||
<slot name="header">
|
||||
<h2 class="header-title">{{ title }}</h2>
|
||||
</slot>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function mounted() {
|
||||
this.events.emit('blur');
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
mounted,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
background: var(--darken-strong);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--primary);
|
||||
color: var(--text-light);
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
padding: .5rem .5rem .5rem 1rem;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
fill: var(--lighten);
|
||||
|
||||
&:hover {
|
||||
fill: var(--lighten-strong);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep(.section) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.dialog-body) {
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::v-deep(.dialog-section:not(:last-child)) {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
::v-deep(.dialog-actions) {
|
||||
display: flex;
|
||||
margin: 1rem 0 0 0;
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
assets/components/entities/children.vue
Normal file
74
assets/components/entities/children.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="children">
|
||||
<div
|
||||
v-for="child in entity.children"
|
||||
:key="`child-${child.id}`"
|
||||
class="tile-container"
|
||||
>
|
||||
<EntityTile
|
||||
:entity="child"
|
||||
@load="(event) => $emit('load', event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EntityTile from './tile.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EntityTile,
|
||||
},
|
||||
emits: ['load'],
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.children {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
margin: 0 1rem 0 0;
|
||||
border-bottom: solid 1px var(--darken-hint);
|
||||
|
||||
.tile-container {
|
||||
display: inline-block;
|
||||
padding: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 15rem;
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
margin: 0;
|
||||
|
||||
.tile-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint0) {
|
||||
.children.expanded {
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
333
assets/components/entities/entity.vue
Normal file
333
assets/components/entities/entity.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="entity"
|
||||
class="entity content"
|
||||
>
|
||||
<div class="info">
|
||||
<a
|
||||
:href="entityUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link link-child"
|
||||
>
|
||||
<template v-if="entity.hasLogo">
|
||||
<img
|
||||
v-if="$route.name === 'network'"
|
||||
class="logo logo-child"
|
||||
:src="`/img/logos/${entity.slug}/thumbs/network.png`"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="entity.parent && !entity.independent"
|
||||
class="logo logo-child"
|
||||
:src="`/img/logos/${entity.parent.slug}/thumbs/${entity.slug}.png`"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
class="logo logo-child"
|
||||
:src="`/img/logos/${entity.slug}/thumbs/${entity.slug}.png`"
|
||||
>
|
||||
</template>
|
||||
|
||||
<h2
|
||||
v-else
|
||||
class="name"
|
||||
>{{ entity.name }}</h2>
|
||||
|
||||
<Icon
|
||||
v-if="entity.url"
|
||||
icon="share2"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<ul
|
||||
v-if="entity.tags.length > 0"
|
||||
class="tags"
|
||||
>
|
||||
<li
|
||||
v-for="tag in entity.tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
>{{ tag.name }}</li>
|
||||
</ul>
|
||||
|
||||
<RouterLink
|
||||
v-if="entity.parent"
|
||||
:to="`/${entity.parent.type}/${entity.parent.slug}`"
|
||||
class="link link-parent"
|
||||
>
|
||||
<img
|
||||
v-if="entity.parent.hasLogo"
|
||||
class="logo logo-parent"
|
||||
:src="`/img/logos/${entity.parent.slug}/thumbs/network.png`"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="entity.parent.hasLogo"
|
||||
class="favicon"
|
||||
:src="`/img/logos/${entity.parent.slug}/favicon.png`"
|
||||
>
|
||||
|
||||
<h3
|
||||
v-else
|
||||
class="name parent-name"
|
||||
>{{ entity.parent.name }}</h3>
|
||||
|
||||
<Icon icon="device_hub" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="content-inner"
|
||||
@scroll="events.emit('scroll', $event)"
|
||||
>
|
||||
<Scroll
|
||||
v-if="entity.children.length > 0"
|
||||
v-slot="scroll"
|
||||
:expandable="true"
|
||||
:expanded="expanded"
|
||||
class="scroll-light"
|
||||
@expand="(state) => expanded = state"
|
||||
>
|
||||
<Children
|
||||
:entity="entity"
|
||||
:class="{ expanded }"
|
||||
@load="scroll.loaded"
|
||||
/>
|
||||
</Scroll>
|
||||
|
||||
<div class="campaign-container">
|
||||
<Campaign
|
||||
:entity="entity"
|
||||
:min-ratio="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FilterBar
|
||||
ref="filter"
|
||||
:fetch-releases="fetchEntity"
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
:available-tags="entity.tags"
|
||||
/>
|
||||
|
||||
<div class="releases">
|
||||
<Releases
|
||||
:releases="entity.releases"
|
||||
:done="done"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-top"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterBar from '../filters/filter-bar.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
import Children from './children.vue';
|
||||
import Scroll from '../scroll/scroll.vue';
|
||||
import Campaign from '../campaigns/campaign.vue';
|
||||
|
||||
async function fetchEntity(scroll = true) {
|
||||
this.done = false;
|
||||
this.entityUrl = null;
|
||||
|
||||
const { entity, totalCount } = await this.$store.dispatch('fetchEntityBySlugAndType', {
|
||||
entitySlug: this.$route.params.entitySlug,
|
||||
entityType: this.$route.name,
|
||||
limit: this.limit,
|
||||
range: this.$route.params.range,
|
||||
pageNumber: Number(this.$route.params.pageNumber),
|
||||
});
|
||||
|
||||
this.entity = entity;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.pageTitle = entity.name;
|
||||
|
||||
const campaign = entity.campaigns.find((campaignX) => !campaignX.banner)
|
||||
|| entity.parent?.campaigns.find((campaignX) => !campaignX.banner);
|
||||
|
||||
const affiliateParams = new URLSearchParams({
|
||||
...(entity.url && Object.fromEntries(new URL(entity.url).searchParams)), // preserve any query in entity URL, e.g. ?siteId=5
|
||||
...(campaign?.affiliate?.parameters && Object.fromEntries(new URLSearchParams(campaign.affiliate.parameters))), // append affiliate parameters
|
||||
}).toString();
|
||||
|
||||
this.entityUrl = campaign?.url || campaign?.affiliate?.url || `${entity.url}${campaign?.affiliate?.parameters ? `?${affiliateParams}` : ''}`;
|
||||
this.done = true;
|
||||
|
||||
if (scroll && this.$refs.filter?.$el) {
|
||||
this.$refs.filter.$el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchEntity();
|
||||
}
|
||||
|
||||
async function route(to) {
|
||||
if (to.name === 'channel' || to.name === 'network' || to.name === 'studio') {
|
||||
await this.fetchEntity();
|
||||
this.expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
Pagination,
|
||||
Children,
|
||||
Releases,
|
||||
Scroll,
|
||||
Campaign,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entity: null,
|
||||
pageTitle: null,
|
||||
totalCount: null,
|
||||
done: false,
|
||||
limit: Number(this.$route.query.limit) || 20,
|
||||
expanded: false,
|
||||
entityUrl: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: route,
|
||||
'$store.state.ui.tagFilter': fetchEntity,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchEntity,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: var(--profile);
|
||||
border-bottom: solid 1px var(--lighten-hint);
|
||||
}
|
||||
|
||||
.link {
|
||||
max-width: 15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-child {
|
||||
flex-shrink: 0;
|
||||
|
||||
.icon {
|
||||
fill: var(--lighten);
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.link-parent {
|
||||
flex-direction: row-reverse;
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 0 3rem;
|
||||
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
fill: var(--lighten);
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-child {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.logo-parent {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
background: var(--profile);
|
||||
}
|
||||
|
||||
.campaign-container {
|
||||
background: var(--background-dim);
|
||||
text-align: center;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.releases {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.logo-parent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-nano) {
|
||||
.link-child .icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
assets/components/entities/tile.vue
Normal file
127
assets/components/entities/tile.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="`/${entity.type}/${entity.slug}`"
|
||||
:title="entity.name"
|
||||
class="tile"
|
||||
>
|
||||
<div class="tile-logo">
|
||||
<template v-if="entity.hasLogo">
|
||||
<img
|
||||
v-if="entity.type === 'network' || entity.independent"
|
||||
:src="`/img/logos/${entity.slug}/thumbs/network.png`"
|
||||
:alt="entity.name"
|
||||
loading="lazy"
|
||||
class="logo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="entity.parent"
|
||||
:src="`/img/logos/${entity.parent.slug}/thumbs/${entity.slug}.png`"
|
||||
:alt="entity.name"
|
||||
loading="lazy"
|
||||
class="logo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="`/img/logos/${entity.slug}/thumbs/${entity.slug}.png`"
|
||||
:alt="entity.name"
|
||||
loading="lazy"
|
||||
class="logo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
</template>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="name"
|
||||
>{{ entity.name }}</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="entity.childrenTotal > 0 || typeof entity.sceneTotal !== 'undefined'"
|
||||
class="count"
|
||||
>
|
||||
<span v-if="typeof entity.sceneTotal !== 'undefined'">{{ entity.sceneTotal }} scenes</span>
|
||||
<span v-if="entity.type === 'network'">{{ entity.childrenTotal }} channels</span>
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['load'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tile {
|
||||
height: 100%;
|
||||
background: var(--tile);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: .25rem;
|
||||
position: relative;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .25);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover .count {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-logo {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: .5rem 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-light);
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: -1.75rem;
|
||||
box-sizing: border-box;
|
||||
padding: .25rem .5rem;
|
||||
background: var(--darken-strong);
|
||||
box-shadow: 0 0 3px var(--darken);
|
||||
color: var(--text-light);
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px var(--darken);
|
||||
transition: bottom .1s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="errorpage">
|
||||
<h1 class="error">404 - Not Found</h1>
|
||||
<div class="errorpage">
|
||||
<h1 class="error">404 - Not Found</h1>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
class="home"
|
||||
>Take me home</a>
|
||||
</div>
|
||||
<a
|
||||
href="/"
|
||||
class="home"
|
||||
>Take me home</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.errorpage {
|
||||
background: $background;
|
||||
color: $primary;
|
||||
background: var(--background);
|
||||
color: var(--primary);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
.home {
|
||||
color: $shadow;
|
||||
color: var(--shadow);
|
||||
margin: 3rem 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
63
assets/components/expand/expand.vue
Normal file
63
assets/components/expand/expand.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-show="expanded"
|
||||
class="expand-button expanded noselect"
|
||||
@click="$emit('expand', false)"
|
||||
><Icon icon="arrow-up3" /></div>
|
||||
|
||||
<div
|
||||
v-show="!expanded"
|
||||
class="expand-button noselect"
|
||||
@click="$emit('expand', true)"
|
||||
><Icon icon="arrow-down3" /></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.expand-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .5rem;
|
||||
|
||||
.icon {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--lighten-hint);
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-dark {
|
||||
.icon {
|
||||
fill: var(--darken-weak);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--darken-hint);
|
||||
|
||||
.icon {
|
||||
fill: var(--darken);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
assets/components/filters/actor-filter.vue
Normal file
88
assets/components/filters/actor-filter.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<Tooltip class="filter-container">
|
||||
<div class="filter">
|
||||
<Icon icon="users" />
|
||||
|
||||
<div
|
||||
v-if="selectedActors.length > 0"
|
||||
class="filter-applied"
|
||||
>{{ selectedActors.length }} {{ selectedActors.length > 1 ? 'actors' : 'actor' }}</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="filter-applied empty"
|
||||
>Actors</div>
|
||||
</div>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<div @click.stop>
|
||||
<router-link
|
||||
class="filter-clear"
|
||||
:to="{ query: { ...$route.query, actors: undefined } }"
|
||||
:class="{ active: selectedActors.length > 0 }"
|
||||
>clear all<Icon icon="cross2" /></router-link>
|
||||
|
||||
<ul class="filter-items nolist">
|
||||
<li
|
||||
v-for="actor in availableActors"
|
||||
:key="actor.id"
|
||||
class="filter-item"
|
||||
:class="{ selected: selectedActors.includes(actor.slug) }"
|
||||
>
|
||||
<router-link
|
||||
:to="{ query: {
|
||||
...$route.query,
|
||||
actors: actor.slug,
|
||||
}, params: { pageNumber: 1 } }"
|
||||
class="filter-name"
|
||||
>{{ actor.name }}</router-link>
|
||||
|
||||
<router-link
|
||||
:to="{ query: { ...$route.query, ...getNewRange(actor) }, params: { pageNumber: 1 } }"
|
||||
class="filter-include"
|
||||
>
|
||||
<Icon
|
||||
icon="checkmark"
|
||||
class="filter-add"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function getNewRange(actor) {
|
||||
if (this.selectedActors.includes(actor.slug)) {
|
||||
return { actors: this.selectedActors.filter(selectedActor => selectedActor !== actor.slug).join(',') || undefined };
|
||||
}
|
||||
|
||||
return { actors: this.selectedActors.concat(actor.slug).join(',') };
|
||||
}
|
||||
|
||||
function selectedActors() {
|
||||
return this.$route.query.actors ? this.$route.query.actors.split(',') : [];
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
availableActors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedActors,
|
||||
},
|
||||
methods: {
|
||||
getNewRange,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
179
assets/components/filters/channel-filter.vue
Normal file
179
assets/components/filters/channel-filter.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Tooltip class="filter-container">
|
||||
<div class="filter">
|
||||
<Icon icon="antenna" />
|
||||
|
||||
<div
|
||||
v-if="selectedLength > 0"
|
||||
class="filter-applied"
|
||||
>{{ selectedLength }} {{ selectedLength > 1 ? 'channels' : 'channel' }}</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="filter-applied empty"
|
||||
>Channels</div>
|
||||
</div>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<div @click.stop>
|
||||
<router-link
|
||||
class="filter-clear"
|
||||
:to="{ query: { ...$route.query, channels: undefined, networks: undefined } }"
|
||||
:class="{ active: selectedLength > 0 }"
|
||||
>clear all<Icon icon="cross2" /></router-link>
|
||||
|
||||
<ul class="filter-items nolist">
|
||||
<li
|
||||
v-for="channel in channelsPerNetwork"
|
||||
:key="`channel-${channel.id}`"
|
||||
class="filter-item"
|
||||
:class="{
|
||||
[channel.type]: true,
|
||||
independent: channel.independent,
|
||||
selected: channel.type === 'network' ? selectedNetworks.includes(channel.slug) : selectedChannels.includes(channel.slug),
|
||||
disabled: channel.parent && selectedNetworks.includes(channel.parent.slug),
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
:to="{ query: {
|
||||
...$route.query,
|
||||
[channel.type === 'network' ? 'networks' : 'channels']: channel.slug,
|
||||
[channel.type === 'network' ? 'channels' : 'networks']: undefined,
|
||||
}, params: { pageNumber: 1 } }"
|
||||
class="filter-name"
|
||||
>
|
||||
<img
|
||||
v-if="channel.type === 'network' || channel.independent || !channel.parent "
|
||||
:src="`/img/logos/${channel.slug}/favicon.png`"
|
||||
class="favicon"
|
||||
>
|
||||
|
||||
{{ channel.name }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
:to="{ query: { ...$route.query, ...getNewRange(channel) }, params: { pageNumber: 1 } }"
|
||||
class="filter-include"
|
||||
>
|
||||
<Icon
|
||||
icon="checkmark"
|
||||
class="filter-add"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function getNewRange(entity) {
|
||||
if (entity.type === 'network' && this.selectedNetworks.includes(entity.slug)) {
|
||||
return {
|
||||
channels: this.selectedChannels.join(',') || undefined,
|
||||
networks: this.selectedNetworks.filter(selectedTag => selectedTag !== entity.slug).join(',') || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (entity.type === 'network') {
|
||||
return {
|
||||
channels: this.selectedChannels.join(',') || undefined,
|
||||
networks: this.selectedNetworks.concat(entity.slug).join(','),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.selectedChannels.includes(entity.slug)) {
|
||||
return {
|
||||
channels: this.selectedChannels.filter(selectedTag => selectedTag !== entity.slug).join(',') || undefined,
|
||||
networks: this.selectedNetworks.join(',') || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
channels: this.selectedChannels.concat(entity.slug).join(','),
|
||||
networks: this.selectedNetworks.join(',') || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function selectedChannels() {
|
||||
return this.$route.query.channels ? this.$route.query.channels.split(',') : [];
|
||||
}
|
||||
|
||||
function selectedNetworks() {
|
||||
return this.$route.query.networks ? this.$route.query.networks.split(',') : [];
|
||||
}
|
||||
|
||||
function channelsPerNetwork() {
|
||||
const networks = this.availableChannels.reduce((acc, channel) => {
|
||||
if (channel.type === 'network' || channel.independent || !channel.parent) {
|
||||
acc[channel.slug] = { ...channel, children: [] };
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[channel.parent.slug]) {
|
||||
acc[channel.parent.slug] = { ...channel.parent, children: [] };
|
||||
}
|
||||
|
||||
acc[channel.parent.slug].children.push(channel);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.values(networks).reduce((acc, network) => [...acc, network, ...(network.children || [])], []);
|
||||
}
|
||||
|
||||
function selectedLength() {
|
||||
return this.selectedChannels.length + this.selectedNetworks.length;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filter: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableChannels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
channelsPerNetwork,
|
||||
selectedChannels,
|
||||
selectedNetworks,
|
||||
selectedLength,
|
||||
},
|
||||
methods: {
|
||||
getNewRange,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.favicon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: 0 .75rem 0 0;
|
||||
filter: drop-shadow(0 0 1px var(--darken));
|
||||
}
|
||||
|
||||
.network .filter-name,
|
||||
.independent .filter-name {
|
||||
font-weight: bold;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.channel:not(.independent) .filter-name {
|
||||
padding: .5rem .5rem .5rem 2.25rem;
|
||||
}
|
||||
</style>
|
||||
366
assets/components/filters/filter-bar.vue
Normal file
366
assets/components/filters/filter-bar.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="filter-bar noselect">
|
||||
<div class="sort">
|
||||
<RouterLink
|
||||
v-for="section in ranges"
|
||||
:key="section"
|
||||
:to="{ params: { range: section, pageNumber: 1 }, query: $route.query }"
|
||||
:class="{ active: $route.name === section || range === section }"
|
||||
class="range range-button"
|
||||
>{{ section }}</RouterLink>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="itemsTotal && showTotal"
|
||||
class="total"
|
||||
>{{ itemsTotal }} <template v-if="itemsTotal === 1">scene</template><template v-else>scenes</template></span>
|
||||
|
||||
<div class="filters">
|
||||
<ActorFilter
|
||||
class="filters-filter"
|
||||
:available-actors="availableActors"
|
||||
/>
|
||||
|
||||
<ChannelFilter
|
||||
class="filters-filter"
|
||||
:available-channels="availableChannels"
|
||||
/>
|
||||
|
||||
<TagFilter
|
||||
class="filters-filter"
|
||||
:available-tags="availableTags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import ActorFilter from './actor-filter.vue';
|
||||
import ChannelFilter from './channel-filter.vue';
|
||||
import TagFilter from './tag-filter.vue';
|
||||
|
||||
function range() {
|
||||
return this.$route.params.range;
|
||||
}
|
||||
|
||||
function batch(state) {
|
||||
return state.ui.batch;
|
||||
}
|
||||
|
||||
async function setRange(newRange) {
|
||||
this.$store.dispatch('setRange', newRange);
|
||||
|
||||
await this.fetchReleases();
|
||||
}
|
||||
|
||||
async function setBatch(newBatch) {
|
||||
this.$store.dispatch('setBatch', newBatch);
|
||||
|
||||
await this.fetchReleases();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActorFilter,
|
||||
ChannelFilter,
|
||||
TagFilter,
|
||||
},
|
||||
props: {
|
||||
fetchReleases: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
isHome: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showTotal: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
itemsTotal: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
ranges: {
|
||||
type: Array,
|
||||
default: () => ['latest', 'upcoming', 'new'],
|
||||
},
|
||||
availableTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
availableChannels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
availableActors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
range,
|
||||
batch,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
setRange,
|
||||
setBatch,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'breakpoints';
|
||||
|
||||
.filter {
|
||||
color: var(--shadow);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.filter-applied {
|
||||
flex-grow: 1;
|
||||
padding: .75rem .5rem;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
|
||||
&.empty {
|
||||
color: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: -.1rem 0 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.applied {
|
||||
color: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-mode {
|
||||
width: 100%;
|
||||
color: var(--shadow);
|
||||
background: none;
|
||||
padding: .75rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
|
||||
option {
|
||||
color: var(--text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1rem;
|
||||
color: var(--shadow-weak);
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-hint);
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--shadow);
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-items .filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.filter-include {
|
||||
.filter-add {
|
||||
fill: var(--success);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.filter-add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-remove {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-include {
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: .5rem 1rem;
|
||||
fill: var(--shadow-hint);
|
||||
}
|
||||
|
||||
.filter-remove {
|
||||
display: none;
|
||||
fill: var(--alert);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
min-width: 8rem;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding: .5rem .75rem .5rem 1rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.filter-include:hover,
|
||||
.filter-name:hover {
|
||||
background: var(--shadow-hint);
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
.filter-applied {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filters-filter:not(:last-child) .filter {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.filters-filter:last-child .filter {
|
||||
padding: .5rem 0 .5rem .5rem;
|
||||
}
|
||||
|
||||
.total {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .5rem 1rem 0 1rem;
|
||||
z-index: 1;
|
||||
background: var(--background-dim);
|
||||
font-size: 0;
|
||||
|
||||
.icon {
|
||||
margin: 0 .5rem 0 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.range-button {
|
||||
color: var(--shadow);
|
||||
display: inline-block;
|
||||
padding: .8rem 1rem;
|
||||
border: none;
|
||||
position: relative;
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
border: solid 1px transparent;
|
||||
border-bottom: none;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: var(--shadow-strong);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
background: var(--background-soft);
|
||||
border-color: var(--crease);
|
||||
|
||||
&::after {
|
||||
/* hide grey border for tab effect, negative margin shows grey crease on mobile */
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--background-soft);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-filter {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
flex-shrink: 0;
|
||||
padding: 0 1rem;
|
||||
color: var(--shadow);
|
||||
font-size: .8rem;
|
||||
}
|
||||
</style>
|
||||
84
assets/components/filters/filters.vue
Normal file
84
assets/components/filters/filters.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="Filters"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<div class="filters dialog-body">
|
||||
<h3 class="form-heading">Show me</h3>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
class="tags-item"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="!tagFilter.includes(tag)"
|
||||
:label="tag"
|
||||
class="tag"
|
||||
@change="(state) => filterTag(tag, state)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="disclaimer">You may still incidentally see filtered out content</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
|
||||
function tagFilter() {
|
||||
return this.$store.state.ui.tagFilter;
|
||||
}
|
||||
|
||||
function filterTag(tag, isChecked) {
|
||||
if (isChecked) {
|
||||
this.$store.dispatch('setTagFilter', this.tagFilter.filter(filteredTag => filteredTag !== tag));
|
||||
} else {
|
||||
this.$store.dispatch('setTagFilter', this.tagFilter.concat(tag));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: ['anal', 'gay', 'transsexual', 'bisexual', 'pissing', 'anal prolapse'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tagFilter,
|
||||
},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
filterTag,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tags-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin: 1rem 0 0 0;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
font-size: .9rem;
|
||||
color: var(--shadow);
|
||||
}
|
||||
</style>
|
||||
112
assets/components/filters/tag-filter.vue
Normal file
112
assets/components/filters/tag-filter.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<Tooltip class="filter-container">
|
||||
<div class="filter">
|
||||
<Icon icon="price-tag4" />
|
||||
|
||||
<div
|
||||
v-if="selectedTags.length > 0"
|
||||
class="filter-applied"
|
||||
>{{ selectedTags.length }} {{ selectedTags.length > 1 ? 'tags' : 'tag' }}</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="filter-applied empty"
|
||||
>Tags</div>
|
||||
</div>
|
||||
|
||||
<template #tooltip>
|
||||
<div
|
||||
class="filter-options"
|
||||
@click.stop
|
||||
>
|
||||
<select
|
||||
v-model="mode"
|
||||
class="filter-mode"
|
||||
@change="$router.push({ query: { ...$route.query, mode }, params: { pageNumber: 1 } })"
|
||||
>
|
||||
<option value="all">match all selected</option>
|
||||
<option value="any">match any selected</option>
|
||||
</select>
|
||||
|
||||
<router-link
|
||||
class="filter-clear"
|
||||
:to="{ query: { ...$route.query, tags: undefined, mode: undefined } }"
|
||||
:class="{ active: selectedTags.length > 0 }"
|
||||
>clear all<Icon icon="cross2" /></router-link>
|
||||
|
||||
<ul class="filter-items nolist">
|
||||
<li
|
||||
v-for="tag in availableTags"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="filter-item"
|
||||
:class="{ selected: selectedTags.includes(tag.slug) }"
|
||||
>
|
||||
<router-link
|
||||
:to="{ query: { ...$route.query, tags: tag.slug, mode }, params: { pageNumber: 1 } }"
|
||||
class="filter-name"
|
||||
>{{ tag.name }}</router-link>
|
||||
|
||||
<router-link
|
||||
:to="{ query: { ...$route.query, ...getNewRange(tag.slug), mode }, params: { pageNumber: 1 } }"
|
||||
class="filter-include"
|
||||
>
|
||||
<Icon
|
||||
icon="checkmark"
|
||||
class="filter-add"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function getNewRange(tag) {
|
||||
if (this.selectedTags.includes(tag)) {
|
||||
return { tags: this.selectedTags.filter((selectedTag) => selectedTag !== tag).join(',') || undefined };
|
||||
}
|
||||
|
||||
return { tags: this.selectedTags.concat(tag).join(',') };
|
||||
}
|
||||
|
||||
function selectedTags() {
|
||||
return this.$route.query.tags ? this.$route.query.tags.split(',') : [];
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mode: this.$route.query.mode || 'all',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedTags,
|
||||
},
|
||||
methods: {
|
||||
getNewRange,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-options {
|
||||
width: 15rem;
|
||||
}
|
||||
</style>
|
||||
60
assets/components/footer/footer.vue
Normal file
60
assets/components/footer/footer.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<span class="segment">© traxxx</span>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'stats' }"
|
||||
class="segment footer-link nolink"
|
||||
>stats</router-link>
|
||||
|
||||
<a
|
||||
v-if="config.discord"
|
||||
:href="config.discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="segment footer-link nolink discord"
|
||||
><Icon icon="discord" /></a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--background-dim);
|
||||
color: var(--shadow);
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
box-shadow: inset -3px 0 3px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.segment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px var(--shadow-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discord {
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
width: 4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
102
assets/components/form/checkbox.vue
Normal file
102
assets/components/form/checkbox.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<label class="check-container noselect">
|
||||
<span
|
||||
v-if="label"
|
||||
class="check-label"
|
||||
>{{ label }}</span>
|
||||
|
||||
<input
|
||||
v-show="false"
|
||||
:id="`checkbox-${uid}`"
|
||||
:checked="checked"
|
||||
type="checkbox"
|
||||
class="check-checkbox"
|
||||
@change="$emit('change', $event.target.checked)"
|
||||
>
|
||||
|
||||
<label
|
||||
:for="`checkbox-${uid}`"
|
||||
class="check"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.check-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
background-color: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
transition: background .15s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: .5rem;
|
||||
height: .3rem;
|
||||
border: solid 2px var(--text-light);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
margin: -.2rem 0 0 0;
|
||||
transform: rotateZ(-45deg) scaleX(0);
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.check-cross .check::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: url('/img/icons/cross3.svg') no-repeat center/80%;
|
||||
opacity: .15;
|
||||
transition: transform .1s ease;
|
||||
}
|
||||
|
||||
.check-checkbox:checked + .check {
|
||||
background: var(--primary);
|
||||
|
||||
&::after {
|
||||
transform: rotateZ(-45deg) scaleX(1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.check-label {
|
||||
overflow: hidden;
|
||||
text-transform: capitalize;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
</style>
|
||||
245
assets/components/form/range.vue
Normal file
245
assets/components/form/range.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="range-container">
|
||||
<div
|
||||
class="label label-start"
|
||||
:class="{ disabled }"
|
||||
@click="setValue('valueA', 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="valueA"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="valueA"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="emit('input')"
|
||||
@change="emit('change')"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
<input
|
||||
v-model.number="valueB"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="valueB"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="emit('input')"
|
||||
@change="emit('change')"
|
||||
@click.stop
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="label label-end"
|
||||
:class="{ disabled }"
|
||||
@click="setValue('valueB', max)"
|
||||
>
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
function minValue() {
|
||||
return Math.min(this.valueA, this.valueB);
|
||||
}
|
||||
|
||||
function maxValue() {
|
||||
return Math.max(this.valueA, this.valueB);
|
||||
}
|
||||
|
||||
function minPercentage() {
|
||||
return ((this.minValue - this.min) / (this.max - this.min)) * 100;
|
||||
}
|
||||
|
||||
function maxPercentage() {
|
||||
return ((this.maxValue - this.min) / (this.max - this.min)) * 100;
|
||||
}
|
||||
|
||||
function emit(type = 'change') {
|
||||
if (this.values) {
|
||||
this.$emit(type, [this.values[this.minValue], this.values[this.maxValue]]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit(type, [this.minValue, this.maxValue]);
|
||||
}
|
||||
|
||||
function setNearest(event) {
|
||||
if (this.allowEnable) {
|
||||
this.emit('enable');
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (!this.disabled) {
|
||||
const closestValue = Math.round((event.offsetX / event.target.getBoundingClientRect().width) * (this.max - this.min)) + this.min;
|
||||
const closestSlider = Math.abs(this.valueA - closestValue) < Math.abs(this.valueB - closestValue) ? 'valueA' : 'valueB';
|
||||
|
||||
this[closestSlider] = closestValue;
|
||||
this.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setValue(prop, value) {
|
||||
if (this.allowEnable) {
|
||||
this.emit('enable');
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (!this.disabled) {
|
||||
this[prop] = value;
|
||||
this.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [3, 7],
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowEnable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['change', 'input', 'enable'],
|
||||
data() {
|
||||
if (this.values) {
|
||||
return {
|
||||
valueA: this.values.indexOf(this.value[0]),
|
||||
valueB: this.values.indexOf(this.value[1]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valueA: this.value[0],
|
||||
valueB: this.value[1],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
minValue,
|
||||
maxValue,
|
||||
minPercentage,
|
||||
maxPercentage,
|
||||
},
|
||||
methods: {
|
||||
emit,
|
||||
setNearest,
|
||||
setValue,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dark .range-container .range {
|
||||
--slider-range: var(--lighten-weak);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@mixin thumb {
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--slider-thumb);
|
||||
pointer-events: visible;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.range-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.range {
|
||||
--slider-track: var(--shadow-hint);
|
||||
--slider-range: var(--primary-faded);
|
||||
--slider-thumb: var(--primary);
|
||||
|
||||
position: relative;
|
||||
height: 1.25rem;
|
||||
flex-grow: 1;
|
||||
border-radius: .625rem;
|
||||
|
||||
&.disabled {
|
||||
--slider-range: var(--shadow-weak);
|
||||
--slider-thumb: var(--disabled-handle);
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
background: none;
|
||||
outline: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
@include thumb;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
@include thumb;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 .5rem;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
::v-deep(.icon) {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.icon) {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
</style>
|
||||
115
assets/components/form/toggle.vue
Normal file
115
assets/components/form/toggle.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<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>
|
||||
export default {
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
trueValue: {
|
||||
type: null,
|
||||
default: true,
|
||||
},
|
||||
falseValue: {
|
||||
type: null,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
id: Math.floor(new Date().getTime() * Math.random()),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.toggle-container {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.light {
|
||||
.toggle {
|
||||
background: var(--lighten-weak);
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle {
|
||||
background: var(--lighten);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
width: 2rem;
|
||||
height: .9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background: var(--shadow-hint);
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: var(--background-light);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px var(--darken-strong);
|
||||
transition: background-color .2s ease, left .2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
display: none;
|
||||
|
||||
&:checked + .toggle {
|
||||
background: var(--primary-faded);
|
||||
|
||||
&::after {
|
||||
background: var(--primary);
|
||||
left: calc(100% - 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] + .toggle {
|
||||
background: var(--shadow-weak);
|
||||
|
||||
&::after {
|
||||
background: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="filter-bar noselect">
|
||||
<span>
|
||||
<label class="range">
|
||||
<input
|
||||
:id="`${_uid}-new`"
|
||||
:checked="range === 'new'"
|
||||
type="radio"
|
||||
class="range-input"
|
||||
@click="setRange('new')"
|
||||
>
|
||||
<label
|
||||
:for="`${_uid}-new`"
|
||||
class="range-button"
|
||||
>New</label>
|
||||
</label>
|
||||
|
||||
<label class="range">
|
||||
<input
|
||||
:id="`${_uid}-upcoming`"
|
||||
:checked="range === 'upcoming'"
|
||||
type="radio"
|
||||
class="range-input"
|
||||
@click="setRange('upcoming')"
|
||||
>
|
||||
<label
|
||||
:for="`${_uid}-upcoming`"
|
||||
class="range-button"
|
||||
>Upcoming</label>
|
||||
</label>
|
||||
|
||||
<label class="range">
|
||||
<input
|
||||
:id="`${_uid}-all`"
|
||||
:checked="range === 'all'"
|
||||
type="radio"
|
||||
class="range-input"
|
||||
@click="setRange('all')"
|
||||
>
|
||||
<label
|
||||
:for="`${_uid}-all`"
|
||||
class="range-button"
|
||||
>All</label>
|
||||
</label>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span class="filters-container">
|
||||
<Icon icon="filter" />
|
||||
|
||||
<Filters
|
||||
class="filters-block"
|
||||
:filter="filter"
|
||||
@set-filter="setFilter"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<v-popover class="filters-compact">
|
||||
<Icon icon="filter" />
|
||||
|
||||
<div slot="popover">
|
||||
<Filters
|
||||
:compact="true"
|
||||
:filter="filter"
|
||||
@set-filter="setFilter"
|
||||
/>
|
||||
</div>
|
||||
</v-popover>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import Filters from './filters.vue';
|
||||
|
||||
function filter(state) {
|
||||
return state.ui.filter;
|
||||
}
|
||||
|
||||
function range(state) {
|
||||
return state.ui.range;
|
||||
}
|
||||
|
||||
async function setFilter(newFilter) {
|
||||
this.$store.dispatch('setFilter', newFilter);
|
||||
|
||||
await this.fetchReleases();
|
||||
}
|
||||
|
||||
async function setRange(newRange) {
|
||||
this.$store.dispatch('setRange', newRange);
|
||||
|
||||
await this.fetchReleases();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Filters,
|
||||
},
|
||||
props: {
|
||||
fetchReleases: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
filter,
|
||||
range,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
setFilter,
|
||||
setRange,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.filter-bar {
|
||||
background: $background;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .5rem 1rem;
|
||||
z-index: 1;
|
||||
font-size: 0;
|
||||
box-shadow: 0 0 3px $shadow;
|
||||
|
||||
.icon {
|
||||
fill: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filters-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filters-compact {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
display: none;
|
||||
margin: 0 0 0 .5rem;
|
||||
}
|
||||
|
||||
.range-button {
|
||||
color: $shadow;
|
||||
background: $background;
|
||||
display: inline-block;
|
||||
padding: .5rem 1rem;
|
||||
border: none;
|
||||
box-shadow: 0 0 2px $shadow-weak;
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
color: $text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.range-input {
|
||||
display: none;
|
||||
|
||||
&:checked + .range-button {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.filters-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filters-compact {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div :class="{ compact }">
|
||||
<ul class="filters">
|
||||
<li class="filter">
|
||||
<label
|
||||
class="toggle"
|
||||
:class="{ active: !localFilter.includes('lesbian') }"
|
||||
>
|
||||
<input
|
||||
v-model="localFilter"
|
||||
value="lesbian"
|
||||
type="checkbox"
|
||||
class="check"
|
||||
@change="$emit('set-filter', localFilter)"
|
||||
>lesbian
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li class="filter">
|
||||
<label
|
||||
class="toggle"
|
||||
:class="{ active: !localFilter.includes('gay') }"
|
||||
>
|
||||
<input
|
||||
v-model="localFilter"
|
||||
value="gay"
|
||||
type="checkbox"
|
||||
class="check"
|
||||
@change="$emit('set-filter', localFilter)"
|
||||
>gay
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li class="filter">
|
||||
<label
|
||||
class="toggle"
|
||||
:class="{ active: !localFilter.includes('transsexual') }"
|
||||
>
|
||||
<input
|
||||
v-model="localFilter"
|
||||
value="transsexual"
|
||||
type="checkbox"
|
||||
class="check"
|
||||
@change="$emit('set-filter', localFilter)"
|
||||
>trans
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="filters">
|
||||
<li class="filter">
|
||||
<label
|
||||
class="toggle"
|
||||
:class="{ active: !localFilter.includes('anal') }"
|
||||
>
|
||||
<input
|
||||
v-model="localFilter"
|
||||
value="anal"
|
||||
type="checkbox"
|
||||
class="check"
|
||||
@change="$emit('set-filter', localFilter)"
|
||||
>anal
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
filter: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localFilter: this.filter,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.filters {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
padding: .5rem;
|
||||
margin: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px $shadow-hint;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.compact {
|
||||
.filters {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter {
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
color: $shadow-weak;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
margin: 0 .25rem;
|
||||
border: solid 1px transparent;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
|
||||
.check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $shadow;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $primary;
|
||||
box-shadow: 0 0 2px $shadow-weak;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,143 +1,441 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<router-link
|
||||
to="/"
|
||||
class="logo-link"
|
||||
><h1 class="logo"><Icon icon="logo" /></h1></router-link>
|
||||
<header class="header">
|
||||
<div class="header-nav">
|
||||
<router-link
|
||||
to="/"
|
||||
class="logo-link"
|
||||
><h1 class="header-logo">
|
||||
<div
|
||||
class="logo"
|
||||
v-html="logo"
|
||||
/>
|
||||
</h1></router-link>
|
||||
|
||||
<nav class="nav">
|
||||
<ul class="nolist">
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
to="/actors"
|
||||
class="nav-link"
|
||||
:class="{ active: active === 'actors' }"
|
||||
>
|
||||
<Icon icon="stars" /><span class="nav-label">Actors</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<nav class="nav">
|
||||
<ul class="nav-list nolist">
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="{ name: 'actors', params: { pageNumber: 1 } }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Actors</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
to="/networks"
|
||||
class="nav-link"
|
||||
:class="{ active: active === 'networks' }"
|
||||
>
|
||||
<Icon icon="earth2" /><span class="nav-label">Networks</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="{ name: 'channels' }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Channels</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
to="/tags"
|
||||
class="nav-link"
|
||||
:class="{ active: active === 'tags' }"
|
||||
>
|
||||
<Icon icon="price-tags" /><span class="nav-label">Tags</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="{ name: 'tags' }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Tags</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
:to="{ name: 'movies', params: { range: 'latest', pageNumber: 1 } }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Movies</a>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="header-section">
|
||||
<div
|
||||
class="sidebar-toggle noselect"
|
||||
@click.stop="$emit('toggleSidebar')"
|
||||
><Icon icon="menu" /></div>
|
||||
|
||||
<Tooltip v-if="me">
|
||||
<div
|
||||
class="header-button header-notifications"
|
||||
:class="{ unseen: unseenNotificationsCount > 0 }"
|
||||
>
|
||||
<Icon icon="bell2" />
|
||||
|
||||
<span
|
||||
v-if="unseenNotificationsCount > 0"
|
||||
class="notifications-count"
|
||||
>{{ unseenNotificationsCount }}</span>
|
||||
</div>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<Notifications
|
||||
:notifications="notifications"
|
||||
:unseen-count="unseenNotificationsCount"
|
||||
@check="fetchNotifications"
|
||||
@add-alert="showAddAlert = true"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<div class="header-button header-account">
|
||||
<div class="account">
|
||||
<Icon
|
||||
icon="user3-long"
|
||||
class="avatar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<Menu @show-filters="state => $emit('showFilters', state)" />
|
||||
</template>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
class="search-compact"
|
||||
:open="searching"
|
||||
@open="searching = true"
|
||||
@close="searching = false"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="search-button"
|
||||
><Icon
|
||||
icon="search"
|
||||
/></button>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<Search
|
||||
:searching="searching"
|
||||
class="compact"
|
||||
@search="searching = false"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
|
||||
<Search class="search-full" />
|
||||
</div>
|
||||
|
||||
<AddAlert
|
||||
v-if="showAddAlert"
|
||||
@close="showAddAlert = false"
|
||||
>Alert</AddAlert>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function active() {
|
||||
return this.$route.name;
|
||||
import Menu from './menu.vue';
|
||||
import Notifications from './notifications.vue';
|
||||
import AddAlert from '../alerts/add.vue';
|
||||
|
||||
import Search from './search.vue';
|
||||
|
||||
import logo from '../../img/logo.svg';
|
||||
|
||||
function me() {
|
||||
return this.$store.state.auth.user;
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
const { notifications, unseenCount } = await this.$store.dispatch('fetchNotifications');
|
||||
|
||||
this.notifications = notifications;
|
||||
this.unseenNotificationsCount = unseenCount;
|
||||
}
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
active,
|
||||
},
|
||||
components: {
|
||||
AddAlert,
|
||||
Menu,
|
||||
Notifications,
|
||||
Search,
|
||||
},
|
||||
emits: ['toggleSidebar', 'showFilters'],
|
||||
data() {
|
||||
return {
|
||||
logo,
|
||||
searching: false,
|
||||
showFilters: false,
|
||||
showAddAlert: false,
|
||||
notifications: [],
|
||||
unseenNotificationsCount: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
me,
|
||||
},
|
||||
watch: {
|
||||
me: fetchNotifications,
|
||||
},
|
||||
mounted: fetchNotifications,
|
||||
methods: {
|
||||
fetchNotifications,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
@import 'breakpoints';
|
||||
|
||||
.header {
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $background;
|
||||
color: $primary;
|
||||
border-bottom: solid 1px $shadow-hint;
|
||||
z-index: 2;
|
||||
justify-content: space-between;
|
||||
background: var(--background);
|
||||
color: var(--primary);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
fill: var(--shadow-modest);
|
||||
padding: 0 1rem;
|
||||
width: 1.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
margin: -.25rem 1rem 0 0;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0 0 1rem;
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
padding: .5rem 1rem;
|
||||
margin: 0 1rem 0 0;
|
||||
font-size: 2rem;
|
||||
|
||||
.icon {
|
||||
width: 6rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav {
|
||||
.nav,
|
||||
.nav-list {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1rem calc(1rem - 5px) 1rem;
|
||||
border-bottom: solid 5px transparent;
|
||||
color: $shadow;
|
||||
padding: 0 1rem;
|
||||
color: var(--shadow);
|
||||
text-decoration: none;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: $shadow;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: $primary;
|
||||
border-bottom: solid 5px $primary;
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: $primary;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: $primary;
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: $primary;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint0) {
|
||||
.nav-label {
|
||||
.header-button {
|
||||
padding: 1rem .75rem;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.account {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-account {
|
||||
padding: 1rem 1.25rem 1rem .75rem;
|
||||
}
|
||||
|
||||
.header-notifications {
|
||||
position: relative;
|
||||
padding: 1rem .75rem;
|
||||
|
||||
&.unseen .icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-count {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: .3rem;
|
||||
left: 0;
|
||||
color: var(--primary);
|
||||
font-size: .6rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.account {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: solid 2px var(--shadow);
|
||||
border-radius: 1.5rem;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: .3rem 0 0 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.search-compact {
|
||||
display: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
height: 100%;
|
||||
padding: .25rem 1rem 0 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.search-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav .nolist {
|
||||
.search-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav,
|
||||
.nav-item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
|
||||
|
||||
.header-account {
|
||||
padding: 1rem .5rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
margin: -.25rem 1.25rem 0 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0.05rem .75rem 0 .75rem;
|
||||
}
|
||||
|
||||
.search-compact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-account,
|
||||
.header-notifications {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
190
assets/components/header/menu.vue
Normal file
190
assets/components/header/menu.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="menu">
|
||||
<ul class="menu-items noselect">
|
||||
<RouterLink
|
||||
v-if="login && me"
|
||||
:to="{ name: 'user', params: { username: me.username } }"
|
||||
class="menu-username"
|
||||
>{{ me.username }}</RouterLink>
|
||||
|
||||
<RouterLink
|
||||
v-if="me && favorites"
|
||||
:to="{ name: 'stash', params: { stashId: favorites.id, range: 'scenes', pageNumber: 1 } }"
|
||||
class="menu-item"
|
||||
><Icon icon="heart7" />Favorites</RouterLink>
|
||||
|
||||
<RouterLink
|
||||
v-else-if="login"
|
||||
:to="{ name: 'login', query: { ref: $route.path } }"
|
||||
class="menu-item"
|
||||
@click.stop
|
||||
>
|
||||
<Icon icon="enter2" />Log in
|
||||
</RouterLink>
|
||||
|
||||
<li
|
||||
v-show="!sfw"
|
||||
class="menu-item"
|
||||
@click.stop="setSfw(true)"
|
||||
>
|
||||
<Icon
|
||||
icon="flower"
|
||||
class="toggle noselect"
|
||||
/>Safe mode
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-show="sfw"
|
||||
class="menu-item"
|
||||
@click.stop="setSfw(false)"
|
||||
>
|
||||
<Icon
|
||||
icon="fire"
|
||||
class="toggle noselect"
|
||||
/>Filth mode
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-show="theme === 'light'"
|
||||
class="menu-item"
|
||||
@click.stop="setTheme('dark')"
|
||||
>
|
||||
<Icon
|
||||
icon="moon"
|
||||
class="toggle noselect"
|
||||
/>Dark theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-show="theme === 'dark'"
|
||||
class="menu-item"
|
||||
@click.stop="setTheme('light')"
|
||||
>
|
||||
<Icon
|
||||
icon="sun"
|
||||
class="toggle noselect"
|
||||
/>Light theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="menu-item"
|
||||
@click="$emit('showFilters', true)"
|
||||
>
|
||||
<Icon icon="filter" />Filters
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="login && me"
|
||||
class="menu-item"
|
||||
@click.stop="$store.dispatch('logout')"
|
||||
>
|
||||
<Icon icon="exit2" />Log out
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
function sfw(state) {
|
||||
return state.ui.sfw;
|
||||
}
|
||||
|
||||
function theme(state) {
|
||||
return state.ui.theme;
|
||||
}
|
||||
|
||||
function login(state) {
|
||||
return state.auth.login;
|
||||
}
|
||||
|
||||
function signup(state) {
|
||||
return state.auth.signup;
|
||||
}
|
||||
|
||||
function favorites() {
|
||||
return this.me?.stashes.find(stash => stash.primary);
|
||||
}
|
||||
|
||||
function me(state) {
|
||||
return state.auth.user;
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
this.$store.dispatch('setTheme', newTheme);
|
||||
}
|
||||
|
||||
function setSfw(enabled) {
|
||||
this.$store.dispatch('setSfw', enabled);
|
||||
}
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
login,
|
||||
signup,
|
||||
sfw,
|
||||
theme,
|
||||
me,
|
||||
}),
|
||||
favorites,
|
||||
},
|
||||
emits: ['showFilters'],
|
||||
methods: {
|
||||
setSfw,
|
||||
setTheme,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.menu-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
padding: .75rem 1rem .75rem .75rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--shadow-weak);
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-username {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: var(--shadow-strong);
|
||||
font-size: .9rem;
|
||||
padding: .75rem 1rem;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
341
assets/components/header/notifications.vue
Normal file
341
assets/components/header/notifications.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div class="notifications">
|
||||
<div class="notifications-header">
|
||||
<h4 class="notifications-title">Notifications</h4>
|
||||
|
||||
<div class="notifications-actions">
|
||||
<Icon
|
||||
v-if="unseenCount > 0"
|
||||
v-tooltip="'Mark all as seen'"
|
||||
icon="checkmark"
|
||||
@click="checkNotifications"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Add alert'"
|
||||
icon="plus3"
|
||||
@click="$emit('addAlert')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notifications-body">
|
||||
<div
|
||||
v-if="notifications.length === 0"
|
||||
class="notifications-empty"
|
||||
>No notifications</div>
|
||||
|
||||
<ul
|
||||
v-else
|
||||
class="nolist"
|
||||
>
|
||||
<li
|
||||
v-for="notification in notifications"
|
||||
:key="`notification-${notification.id}`"
|
||||
:class="{ unseen: !notification.seen }"
|
||||
class="notification"
|
||||
@click="checkNotification(notification.id, true)"
|
||||
>
|
||||
<router-link
|
||||
:to="`/scene/${notification.scene.id}/${notification.scene.slug}`"
|
||||
class="notification-link"
|
||||
>
|
||||
<img
|
||||
:src="getPath(notification.scene.poster, 'thumbnail')"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<div class="notification-body">
|
||||
<div class="notification-row notification-title">
|
||||
<img
|
||||
v-if="notification.scene.entity.type === 'network' || notification.scene.entity.independent"
|
||||
v-tooltip="notification.scene.entity.name"
|
||||
:src="`/img/logos/${notification.scene.entity.slug}/favicon_${theme === 'dark' ? 'light' : 'dark'}.png`"
|
||||
class="notification-favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
v-tooltip="notification.scene.entity.name"
|
||||
:src="`/img/logos/${notification.scene.entity.parent.slug}/favicon_${theme === 'dark' ? 'light' : 'dark'}.png`"
|
||||
class="notification-favicon"
|
||||
>
|
||||
|
||||
New <ul
|
||||
v-if="notification.alert?.tags.length > 0"
|
||||
class="nolist notification-tags"
|
||||
>
|
||||
<li
|
||||
v-for="tag in notification.alert.tags"
|
||||
:key="`notification-tag-${tag.slug}`"
|
||||
class="notification-tag"
|
||||
>{{ tag.name }}</li>
|
||||
</ul>scene
|
||||
</div>
|
||||
|
||||
<div class="notification-row notification-details">
|
||||
<span class="notification-date">{{ formatDate(notification.scene.date, 'MMM D') }}</span>
|
||||
|
||||
<ul
|
||||
v-if="notification.scene.actors.length > 0"
|
||||
class="nolist notification-actors"
|
||||
>
|
||||
<li
|
||||
v-for="actor in notification.scene.actors"
|
||||
:key="`notification-actor-${actor.slug}`"
|
||||
class="notification-actor"
|
||||
>{{ actor.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
v-if="!notification.seen"
|
||||
v-tooltip="'Mark as seen'"
|
||||
icon="checkmark"
|
||||
class="notification-check"
|
||||
@click.prevent.stop="checkNotification(notification.id)"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="notification.alert"
|
||||
v-tooltip="`You set an alert for <strong>${notification.alert.tags.map(tag => tag.name).join(', ') || 'all'}</strong> scenes with <strong>${notification.alert.actors.map(actor => actor.name).join(', ') || 'any actor'}</strong> for <strong>${notification.alert.entity?.name || 'any channel'}</strong>`"
|
||||
icon="question5"
|
||||
@click.prevent.stop
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div @click="events.emit('blur')">
|
||||
<router-link
|
||||
to="/notifications"
|
||||
class="notification-link notification-more"
|
||||
>See all</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function checkNotifications() {
|
||||
await this.$store.dispatch('checkNotifications');
|
||||
|
||||
this.$emit('check');
|
||||
}
|
||||
|
||||
async function checkNotification(notificationId, blur) {
|
||||
await this.$store.dispatch('checkNotification', notificationId);
|
||||
|
||||
this.$emit('check');
|
||||
|
||||
if (blur) {
|
||||
this.events.emit('blur');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
unseenCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAddAlert: false,
|
||||
};
|
||||
},
|
||||
emits: ['addAlert'],
|
||||
methods: {
|
||||
checkNotifications,
|
||||
checkNotification,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notifications {
|
||||
width: 30rem;
|
||||
max-height: calc(100vh - 5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.notifications-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon {
|
||||
padding: .5rem;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:first-child {
|
||||
padding: .5rem .5rem .5rem 1.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: .5rem 1.5rem .5rem .5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-title {
|
||||
display: inline-block;
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
color: var(--shadow);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notifications-body {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
}
|
||||
|
||||
.notifications-empty {
|
||||
padding: .5rem 1rem;
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
.notification {
|
||||
display: block;
|
||||
border-right: solid .5rem var(--shadow-touch);
|
||||
color: var(--text);
|
||||
|
||||
&.unseen {
|
||||
border-right: solid .5rem var(--primary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 1.3rem .5rem;
|
||||
fill: var(--shadow-weak);
|
||||
|
||||
&.notification-check {
|
||||
padding: 1.3rem .5rem 1.3rem 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: 1.3rem 1rem 1.3rem .5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
margin: 0 0 -1px 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-touch);
|
||||
|
||||
&:not(.unseen) {
|
||||
border-right: solid .5rem var(--shadow-weak);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-link {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notification-body {
|
||||
flex-grow: 1;
|
||||
padding: .4rem 0 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-row {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
margin: .15rem .5rem .3rem .5rem;
|
||||
}
|
||||
|
||||
.notification-favicon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
|
||||
.notification-tags {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notification-actors {
|
||||
padding: 0 .5rem;
|
||||
height: 1.25rem;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-date {
|
||||
width: 3rem;
|
||||
flex-shrink: 0;
|
||||
padding: .25rem .25rem .35rem .25rem;
|
||||
border-right: solid 1px var(--shadow-hint);
|
||||
border-top: solid 1px var(--shadow-hint);
|
||||
color: var(--shadow-strong);
|
||||
font-size: .8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notification-actor,
|
||||
.notification-tag {
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: ',';
|
||||
padding: 0 .1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-actor {
|
||||
padding: .25rem .15rem .35rem 0;
|
||||
color: var(--shadow-strong);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.notification-tag {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notification-more {
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
color: var(--shadow);
|
||||
text-align: center;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.poster {
|
||||
width: 6rem;
|
||||
height: 3.6rem;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
</style>
|
||||
154
assets/components/header/search.vue
Normal file
154
assets/components/header/search.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<form
|
||||
class="search"
|
||||
@submit.prevent="search"
|
||||
>
|
||||
<input
|
||||
ref="search"
|
||||
v-model="query"
|
||||
type="search"
|
||||
class="search-input"
|
||||
placeholder="Search..."
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="search-button"
|
||||
><Icon
|
||||
icon="search"
|
||||
/></button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function search() {
|
||||
if (this.query) {
|
||||
this.$router.push({ name: 'search', query: { q: this.query } });
|
||||
this.$emit('search');
|
||||
}
|
||||
}
|
||||
|
||||
function searching(to) {
|
||||
if (to) {
|
||||
this.$refs.search.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function route(to) {
|
||||
if (to.name !== 'search') {
|
||||
this.query = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
searching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: this.$route.query ? this.$route.query.q : null,
|
||||
};
|
||||
},
|
||||
emits: ['search'],
|
||||
watch: {
|
||||
$route: route,
|
||||
searching,
|
||||
},
|
||||
methods: {
|
||||
search,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.search {
|
||||
height: 100%;
|
||||
max-width: 20rem;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border-left: solid 1px var(--shadow-hint);
|
||||
margin: 0 .25rem 0 0;
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
.search-input {
|
||||
padding: .75rem .5rem .75rem .75rem;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: 1rem 1rem .75rem .25rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: .5rem 0 .5rem .75rem;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
background: var(--background);
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
padding: .5rem;
|
||||
position: relative;
|
||||
right: 0;
|
||||
color: var(--text);
|
||||
background: url('/img/cancel-circle2.svg');
|
||||
opacity: .25;
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&::placeholder {
|
||||
color: var(--shadow-weak);
|
||||
}
|
||||
|
||||
& + .search-button:not(:hover) .icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
height: 100%;
|
||||
padding: 0 1.25rem 0 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: .3rem 0 0 0;
|
||||
outline: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +1,94 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<FilterBar :fetch-releases="fetchReleases" />
|
||||
<div class="home">
|
||||
<div class="content-inner">
|
||||
<FilterBar
|
||||
ref="filter"
|
||||
:fetch-releases="fetchReleases"
|
||||
:is-home="true"
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
:content="$refs.content"
|
||||
:show-total="false"
|
||||
/>
|
||||
|
||||
<div class="content-inner">
|
||||
<Releases
|
||||
:releases="releases"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Releases
|
||||
:releases="releases"
|
||||
:done="done"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
v-if="totalCount > 0"
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterBar from '../header/filter-bar.vue';
|
||||
import FilterBar from '../filters/filter-bar.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
async function fetchReleases() {
|
||||
this.releases = await this.$store.dispatch('fetchReleases', { limit: 100 });
|
||||
async function fetchReleases(scroll = true) {
|
||||
this.done = false;
|
||||
|
||||
const { releases, totalCount } = await this.$store.dispatch('fetchReleases', {
|
||||
limit: this.limit,
|
||||
range: this.$route.params.range,
|
||||
pageNumber: Number(this.$route.params.pageNumber) || 1,
|
||||
});
|
||||
|
||||
this.totalCount = totalCount;
|
||||
this.releases = releases;
|
||||
this.done = true;
|
||||
|
||||
if (scroll) {
|
||||
this.$refs.filter?.$el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.pageTitle = '';
|
||||
this.pageTitle = '';
|
||||
|
||||
await this.fetchReleases();
|
||||
await this.fetchReleases();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
Releases,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
networks: [],
|
||||
pageTitle: null,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchReleases,
|
||||
},
|
||||
components: {
|
||||
FilterBar,
|
||||
Releases,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
releases: [],
|
||||
networks: [],
|
||||
pageTitle: null,
|
||||
limit: 30,
|
||||
totalCount: 0,
|
||||
from: null,
|
||||
done: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: fetchReleases,
|
||||
'$store.state.ui.tagFilter': fetchReleases,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchReleases,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
<template>
|
||||
<div
|
||||
:title="title"
|
||||
:class="{ active }"
|
||||
class="icon"
|
||||
v-html="svg"
|
||||
/>
|
||||
<div
|
||||
:class="{ active }"
|
||||
class="icon"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
svg: null,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.svg = require(`../../img/${this.icon}.svg`).default;
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
svg: null,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.svg = require(`../../img/icons/${this.icon}.svg`).default; // eslint-disable-line global-require, import/no-dynamic-require
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -38,7 +37,7 @@ export default {
|
||||
@import '../../css/theme';
|
||||
|
||||
.icon {
|
||||
fill: $text;
|
||||
fill: var(--text);
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
@@ -50,10 +49,10 @@ export default {
|
||||
}
|
||||
|
||||
&.active {
|
||||
fill: $shadow;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
fill: $text;
|
||||
fill: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
90
assets/components/loading/ellipsis.vue
Normal file
90
assets/components/loading/ellipsis.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="load-container">
|
||||
<div class="load-ellipsis">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.load-container {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.load-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 5rem;
|
||||
height: .75rem;
|
||||
}
|
||||
|
||||
.load-ellipsis div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.load-ellipsis div:nth-child(1) {
|
||||
left: .5rem;
|
||||
animation: load-ellipsis1 0.5s infinite linear;
|
||||
}
|
||||
|
||||
.load-ellipsis div:nth-child(2) {
|
||||
left: .5rem;
|
||||
animation: load-ellipsis2 0.5s infinite linear;
|
||||
}
|
||||
|
||||
.load-ellipsis div:nth-child(3) {
|
||||
left: 2rem;
|
||||
animation: load-ellipsis3 0.5s infinite linear;
|
||||
}
|
||||
|
||||
.load-ellipsis div:nth-child(4) {
|
||||
left: 3.5rem;
|
||||
animation: load-ellipsis4 0.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes load-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(0.5);
|
||||
}
|
||||
100% {
|
||||
transform: translate(1.5rem, 0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load-ellipsis3 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: translate(1.5rem, 0) scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load-ellipsis4 {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,282 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="network"
|
||||
class="content"
|
||||
>
|
||||
<FilterBar :fetch-releases="fetchNetwork" />
|
||||
|
||||
<div
|
||||
class="network"
|
||||
:class="{ nosites: sites.length === 0 }"
|
||||
>
|
||||
<div
|
||||
v-show="sites.length > 0"
|
||||
class="sidebar"
|
||||
:class="{ expanded }"
|
||||
>
|
||||
<a
|
||||
v-tooltip.bottom="`Go to ${network.url}`"
|
||||
:href="network.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="title"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${network.slug}/network.png`"
|
||||
class="logo"
|
||||
>
|
||||
</a>
|
||||
|
||||
<p
|
||||
v-if="network.description"
|
||||
class="description"
|
||||
>{{ network.description }}</p>
|
||||
|
||||
<Sites
|
||||
v-if="sites.length"
|
||||
:sites="sites"
|
||||
:class="{ expanded }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="sites.length > 0">
|
||||
<span
|
||||
v-show="!expanded"
|
||||
class="expand expand-sidebar noselect"
|
||||
@click="expanded = true"
|
||||
><Icon icon="arrow-right3" /></span>
|
||||
|
||||
<span
|
||||
v-show="expanded"
|
||||
class="expand expand-sidebar noselect"
|
||||
@click="expanded = false"
|
||||
><Icon icon="arrow-left3" /></span>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="header"
|
||||
:class="{ hideable: sites.length > 0 }"
|
||||
>
|
||||
<a
|
||||
v-tooltip.bottom="`Go to ${network.url}`"
|
||||
:href="network.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="title"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${network.slug}/network.png`"
|
||||
class="logo"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content-inner">
|
||||
<template v-if="sites.length > 0">
|
||||
<span
|
||||
v-show="expanded"
|
||||
class="expand collapse-header noselect"
|
||||
@click="expanded = false"
|
||||
><Icon icon="arrow-up3" /></span>
|
||||
|
||||
<Sites
|
||||
:sites="sites"
|
||||
:class="{ expanded }"
|
||||
class="compact"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-show="!expanded"
|
||||
class="expand expand-header noselect"
|
||||
@click="expanded = true"
|
||||
><Icon icon="arrow-down3" /></span>
|
||||
|
||||
<span
|
||||
v-show="expanded"
|
||||
class="expand expand-header noselect"
|
||||
@click="expanded = false"
|
||||
><Icon icon="arrow-up3" /></span>
|
||||
</template>
|
||||
|
||||
<Releases :releases="releases" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterBar from '../header/filter-bar.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
import Sites from '../sites/sites.vue';
|
||||
|
||||
async function fetchNetwork() {
|
||||
this.network = await this.$store.dispatch('fetchNetworks', this.$route.params.networkSlug);
|
||||
|
||||
if (this.network.studios) {
|
||||
this.studios = this.network.studios.map(studio => ({
|
||||
...studio,
|
||||
network: this.network,
|
||||
}));
|
||||
}
|
||||
|
||||
this.sites = this.network.sites
|
||||
.filter(site => !site.independent)
|
||||
// .concat(this.studios)
|
||||
.sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB));
|
||||
|
||||
this.releases = this.network.releases;
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchNetwork();
|
||||
this.pageTitle = this.network.name;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
Releases,
|
||||
Sites,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
network: null,
|
||||
sites: [],
|
||||
studios: [],
|
||||
releases: [],
|
||||
pageTitle: null,
|
||||
expanded: false,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchNetwork,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.network {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
justify-content: stretch;
|
||||
overflow-y: auto;
|
||||
|
||||
&.nosites {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.releases {
|
||||
padding: 1rem 1rem 1rem .5rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: $profile;
|
||||
height: 100%;
|
||||
width: 18rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
color: $text-contrast;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-bottom: solid 1px $highlight-hint;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
width: calc(100% - 25rem);
|
||||
|
||||
.logo {
|
||||
max-width: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
max-height: 8rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
filter: $logo-highlight;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-bottom: solid 1px $shadow-hint;
|
||||
background: $profile;
|
||||
|
||||
&.hideable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 20rem;
|
||||
max-height: 3rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sites.compact {
|
||||
display: none;
|
||||
background: $profile;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint3) {
|
||||
.header,
|
||||
.header.hideable {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sites.compact {
|
||||
display: flex;
|
||||
|
||||
&.expanded {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-header,
|
||||
.collapse-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.expand-sidebar,
|
||||
.collapse-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.network {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +1,202 @@
|
||||
<template>
|
||||
<div class="networks">
|
||||
<input
|
||||
:placeholder="`Find ${siteCount} sites in ${networks.length} networks`"
|
||||
class="search"
|
||||
>
|
||||
<div class="networks">
|
||||
<div class="content-inner">
|
||||
<SearchBar
|
||||
:placeholder="`Search ${channelCount} channels in ${entities.length} networks`"
|
||||
:eager="true"
|
||||
/>
|
||||
|
||||
<div class="network-tiles">
|
||||
<Network
|
||||
v-for="network in networks"
|
||||
:key="`network-tile-${network.slug}`"
|
||||
:network="network"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="done && entities.length === 0"
|
||||
class="empty"
|
||||
>No results for "{{ $route.query.query }}"</span>
|
||||
|
||||
<template v-else>
|
||||
<h2 class="heading">Popular</h2>
|
||||
|
||||
<div
|
||||
class="entity-tiles"
|
||||
>
|
||||
<Entity
|
||||
v-for="entity in popularEntities"
|
||||
:key="entity.parent ? `entity-tile-${entity.parent.slug}-${entity.slug}` : `entity-tile-${entity.slug}`"
|
||||
:entity="entity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 class="heading">All networks</h2>
|
||||
|
||||
<div
|
||||
class="entity-tiles"
|
||||
>
|
||||
<Entity
|
||||
v-for="entity in entities"
|
||||
:key="entity.parent ? `entity-tile-${entity.parent.slug}-${entity.slug}` : `entity-tile-${entity.slug}`"
|
||||
:entity="entity"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Network from '../tile/network.vue';
|
||||
import Entity from '../entities/tile.vue';
|
||||
import SearchBar from '../search/bar.vue';
|
||||
|
||||
async function mounted() {
|
||||
this.networks = await this.$store.dispatch('fetchNetworks');
|
||||
async function fetchEntities() {
|
||||
if (this.$route.query.query) {
|
||||
await this.searchEntities();
|
||||
return;
|
||||
}
|
||||
|
||||
this.done = false;
|
||||
|
||||
this.entities = await this.$store.dispatch('fetchEntities', {
|
||||
type: 'network',
|
||||
entitySlugs: [],
|
||||
});
|
||||
|
||||
this.done = true;
|
||||
}
|
||||
|
||||
function siteCount() {
|
||||
return this.networks.map(network => network.sites).flat().length;
|
||||
async function searchEntities() {
|
||||
this.done = false;
|
||||
|
||||
this.entities = await this.$store.dispatch('searchEntities', {
|
||||
query: this.$route.query.query,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
this.done = true;
|
||||
}
|
||||
|
||||
function popularEntities() {
|
||||
const entitiesBySlug = Object.fromEntries(this.entities.map((entity) => [entity.slug, entity]));
|
||||
|
||||
return [
|
||||
'21sextury',
|
||||
'amateurallure',
|
||||
'analvids',
|
||||
'bamvisions',
|
||||
'bang',
|
||||
'bangbros',
|
||||
'blowpass',
|
||||
'brazzers',
|
||||
'burningangel',
|
||||
'digitalplayground',
|
||||
'dogfartnetwork',
|
||||
'dorcel',
|
||||
'elegantangel',
|
||||
'evilangel',
|
||||
'fakehub',
|
||||
'girlsway',
|
||||
'hookuphotshot',
|
||||
'hussiepass',
|
||||
'insex',
|
||||
'julesjordan',
|
||||
'kellymadison',
|
||||
'kink',
|
||||
'mofos',
|
||||
'naughtyamerica',
|
||||
'newsensations',
|
||||
'pervcity',
|
||||
'pornpros',
|
||||
'private',
|
||||
'realitykings',
|
||||
'twistys',
|
||||
'vixen',
|
||||
'xempire',
|
||||
].map((slug) => entitiesBySlug[slug]).filter(Boolean);
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.pageTitle = 'Channels';
|
||||
|
||||
await this.fetchEntities();
|
||||
}
|
||||
|
||||
function channelCount() {
|
||||
return this.entities.reduce((acc, entity) => acc + entity.childrenTotal, 0);
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Network,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
networks: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
siteCount,
|
||||
},
|
||||
mounted,
|
||||
components: {
|
||||
Entity,
|
||||
SearchBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
done: false,
|
||||
pageTitle: null,
|
||||
entities: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
channelCount,
|
||||
popularEntities,
|
||||
},
|
||||
watch: {
|
||||
$route: fetchEntities,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchEntities,
|
||||
searchEntities,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.search {
|
||||
width: 40rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
box-shadow: 0 0 3px $shadow-weak;
|
||||
margin: 1rem;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
.networks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 3px $primary;
|
||||
.content-inner {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.entity-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
padding: 1rem 0;
|
||||
|
||||
.tile {
|
||||
height: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: block;
|
||||
margin: 1rem 0;
|
||||
color: var(--shadow);
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint2) {
|
||||
.entity-tiles {
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.network-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.networks {
|
||||
@media(max-width: $breakpoint0) {
|
||||
.entity-tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
76
assets/components/notifications/notifications.vue
Normal file
76
assets/components/notifications/notifications.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div
|
||||
ref="page"
|
||||
class="notifications-container"
|
||||
>
|
||||
<h1 class="heading">Notifications</h1>
|
||||
|
||||
<ul class="notifications nolist">
|
||||
<li
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
>
|
||||
<SceneTile :release="notification.scene" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Pagination
|
||||
:items-total="totalCount"
|
||||
:items-per-page="10"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
import SceneTile from '../releases/scene-tile.vue';
|
||||
|
||||
async function fetchNotifications() {
|
||||
const { notifications, totalCount, unseenCount } = await this.$store.dispatch('fetchNotifications', {
|
||||
page: this.$route.params.pageNumber,
|
||||
limit: this.limit,
|
||||
});
|
||||
|
||||
this.notifications = notifications;
|
||||
this.unseenNotificationsCount = unseenCount;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.$emit('scroll');
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Pagination,
|
||||
SceneTile,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
limit: 10,
|
||||
totalCount: 0,
|
||||
unseenNotificationsCount: 0,
|
||||
};
|
||||
},
|
||||
emits: ['scroll'],
|
||||
watch: {
|
||||
$route: fetchNotifications,
|
||||
},
|
||||
mounted: fetchNotifications,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notifications-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
</style>
|
||||
197
assets/components/pagination/pagination.vue
Normal file
197
assets/components/pagination/pagination.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="itemsTotal > 0 || !hideEmpty"
|
||||
class="pagination"
|
||||
>
|
||||
<span
|
||||
v-show="pageNumber > 1"
|
||||
class="cursors"
|
||||
>
|
||||
<router-link
|
||||
class="pagination-button cursor"
|
||||
:to="{ name, params: { ...params, pageNumber: 1 }, query: $route.query }"
|
||||
><Icon icon="first2" /></router-link>
|
||||
|
||||
<router-link
|
||||
class="pagination-button cursor"
|
||||
:to="{ name, params: { ...params, pageNumber: pageNumber - 1 }, query: $route.query }"
|
||||
><Icon icon="arrow-left" /></router-link>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-show="pageNumber === 1"
|
||||
class="cursors"
|
||||
>
|
||||
<span class="pagination-button cursor disabled"><Icon icon="first2" /></span>
|
||||
<span class="pagination-button cursor disabled"><Icon icon="arrow-left" /></span>
|
||||
</span>
|
||||
|
||||
<span class="pages pages-before">
|
||||
<router-link
|
||||
v-for="pageX in pageNumber - 1"
|
||||
:key="`page-${pageX}`"
|
||||
:to="{ name, params: { ...params, pageNumber: pageNumber - pageX }, query: $route.query }"
|
||||
class="pagination-button page"
|
||||
> {{ pageNumber - pageX }} </router-link>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
:key="`page-${pageNumber}`"
|
||||
:to="{ name, params: { ...params, pageNumber }, query: $route.query }"
|
||||
class="pagination-button page active"
|
||||
> {{ pageNumber }} </router-link>
|
||||
|
||||
<span class="pages pages-after">
|
||||
<router-link
|
||||
v-for="pageX in (pageCount - pageNumber)"
|
||||
:key="`page-${pageX + pageNumber}`"
|
||||
:to="{ name, params: { ...params, pageNumber: pageX + pageNumber }, query: $route.query }"
|
||||
class="pagination-button page"
|
||||
> {{ pageX + pageNumber }} </router-link>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-show="pageNumber < pageCount"
|
||||
class="cursors"
|
||||
>
|
||||
<router-link
|
||||
class="pagination-button cursor"
|
||||
:to="{ name, params: { ...params, pageNumber: pageNumber + 1 }, query: $route.query }"
|
||||
><Icon icon="arrow-right" /></router-link>
|
||||
|
||||
<router-link
|
||||
class="pagination-button cursor"
|
||||
:to="{ name, params: { ...params, pageNumber: pageCount }, query: $route.query }"
|
||||
><Icon icon="last2" /></router-link>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-show="pageNumber === pageCount"
|
||||
class="cursors"
|
||||
>
|
||||
<span class="pagination-button cursor disabled"><Icon icon="arrow-right" /></span>
|
||||
<span class="pagination-button cursor disabled"><Icon icon="last2" /></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function pageNumber() {
|
||||
return Number(this.$route.params.pageNumber) || 1;
|
||||
}
|
||||
|
||||
function pageCount() {
|
||||
const count = Math.max(Math.ceil(this.itemsTotal / this.itemsPerPage), 1);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
itemsTotal: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
hideEmpty: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pageNumber,
|
||||
pageCount,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.pagination-top {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.pagination-bottom {
|
||||
margin: .5rem 0 1rem 0;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: .1rem 0 2rem 0;
|
||||
background: var(--background);
|
||||
color: var(--shadow);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 3px var(--shadow-hint);
|
||||
|
||||
.icon {
|
||||
width: .8rem;
|
||||
height: .8rem;
|
||||
margin: 0 0 .125rem 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover:not(.active):not(.disabled) {
|
||||
color: var(--shadow-strong);
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--shadow-weak);
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pages {
|
||||
max-width: 10rem;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pages-before {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.cursors {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cursors {
|
||||
margin: 0 .75rem;
|
||||
font-size: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +1,385 @@
|
||||
<template>
|
||||
<div
|
||||
class="banner"
|
||||
@wheel.prevent="scrollBanner"
|
||||
>
|
||||
<template v-if="release.covers && release.covers.length > 0">
|
||||
<a
|
||||
v-for="cover in release.covers"
|
||||
:key="`cover-${cover.id}`"
|
||||
:href="`/media/${cover.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="`/media/${cover.thumbnail}`"
|
||||
class="cover"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
<div class="media-container">
|
||||
<div
|
||||
class="media"
|
||||
:class="{ center: (release.photos?.length || 0) + (release.scenesPhotos?.length || 0) < 2, preview: !me }"
|
||||
>
|
||||
<div
|
||||
v-if="release.trailer || release.teaser"
|
||||
class="trailer-container"
|
||||
>
|
||||
<Player
|
||||
v-if="release.trailer"
|
||||
:video="release.trailer"
|
||||
:poster="poster"
|
||||
class="item trailer"
|
||||
@play="playing = true; paused = false;"
|
||||
@pause="playing = false; paused = true;"
|
||||
/>
|
||||
|
||||
<div class="trailer">
|
||||
<video
|
||||
v-if="release.trailer"
|
||||
:src="`/media/${release.trailer.path}`"
|
||||
:poster="`/media/${(release.poster && release.poster.thumbnail)}`"
|
||||
:alt="release.title"
|
||||
class="item trailer-video"
|
||||
controls
|
||||
>Sorry, the tailer cannot be played in your browser</video>
|
||||
</div>
|
||||
<Player
|
||||
v-else-if="release.teaser && /^video\//.test(release.teaser.mime)"
|
||||
:video="release.teaser"
|
||||
:poster="poster"
|
||||
:alt="release.title"
|
||||
:class="{ sfw: sfw && paused }"
|
||||
class="item trailer"
|
||||
@play="playing = true; paused = false;"
|
||||
@pause="playing = false; paused = true;"
|
||||
/>
|
||||
|
||||
<a
|
||||
v-for="photo in photos"
|
||||
:key="`banner-${photo.index}`"
|
||||
:href="`/media/${photo.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="`/media/${photo.thumbnail}`"
|
||||
:alt="`Photo ${photo.index + 1}`"
|
||||
class="item"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
v-else-if="release.teaser && /^image\//.test(release.teaser.mime)"
|
||||
:src="getPath(release.teaser, 'thumbnail', { original: true })"
|
||||
:alt="release.title"
|
||||
:width="release.teaser.thumbnailWidth"
|
||||
:height="release.teaser.thumbnailHeight"
|
||||
loading="lazy"
|
||||
class="item trailer"
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="release.poster || release.teaser"
|
||||
class="poster-links"
|
||||
>
|
||||
<a
|
||||
v-if="release.teaser"
|
||||
v-tooltip="'View teaser'"
|
||||
:href="getPath(release.teaser)"
|
||||
:class="{ playing }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="poster-link"
|
||||
><Icon icon="film4" /></a>
|
||||
|
||||
<a
|
||||
v-if="release.poster"
|
||||
v-tooltip="'View poster'"
|
||||
:href="getPath(release.poster)"
|
||||
:class="{ playing }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="poster-link"
|
||||
><Icon icon="image" /></a>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="sfw && !playing"
|
||||
class="warning"
|
||||
>
|
||||
<Icon icon="warning2" />NSFW
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="release.covers?.length > 0">
|
||||
<div
|
||||
v-for="cover in release.covers"
|
||||
:key="`cover-${cover.id}`"
|
||||
class="item-container"
|
||||
>
|
||||
<a
|
||||
:href="getPath(cover)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="getPath(cover, 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(cover, 'lazy') }"
|
||||
:width="cover.thumbnailWidth"
|
||||
:height="cover.thumbnailHeight"
|
||||
class="item cover"
|
||||
loading="lazy"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-for="photo in photos"
|
||||
:key="`media-${photo.id}`"
|
||||
class="item-container"
|
||||
>
|
||||
<a
|
||||
:href="getPath(photo)"
|
||||
:class="{ sfw }"
|
||||
class="item-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="getPath(photo, 'thumbnail')"
|
||||
:style="{ 'background-image': `url('${getPath(photo, 'lazy')}` }"
|
||||
:alt="`Photo ${photo.index + 1}`"
|
||||
:width="photo.thumbnailWidth"
|
||||
:height="photo.thumbnailHeight"
|
||||
loading="lazy"
|
||||
class="item"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
|
||||
<span
|
||||
v-if="sfw"
|
||||
class="warning"
|
||||
>
|
||||
<Icon icon="warning2" />NSFW
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!me"
|
||||
class="item-container item-more"
|
||||
><router-link
|
||||
:to="{ name: 'login', query: { ref: $route.path } }"
|
||||
class="link"
|
||||
>Log in</router-link> for more photos, trailers and features!</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function photos() {
|
||||
if (this.release.trailer) {
|
||||
// poster will be on trailer video
|
||||
return this.release.photos;
|
||||
}
|
||||
import Player from '../video/player.vue';
|
||||
|
||||
if (this.release.poster) {
|
||||
return [this.release.poster].concat(this.release.photos);
|
||||
}
|
||||
|
||||
return this.release.photos;
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
function scrollBanner(event) {
|
||||
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
|
||||
function me() {
|
||||
return this.$store.state.auth.user;
|
||||
}
|
||||
|
||||
function poster() {
|
||||
if (this.release.poster) {
|
||||
return this.getPath(this.release.poster, 'thumbnail');
|
||||
}
|
||||
|
||||
if (this.release.covers?.length > 0) {
|
||||
return this.getPath(this.release.covers[0], 'thumbnail');
|
||||
}
|
||||
|
||||
if (this.photos?.length > 0) {
|
||||
return this.getPath(this.release.photos[0], 'thumbnail');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function photos() {
|
||||
const clips = this.release.clips || [];
|
||||
const clipPostersById = clips.reduce((acc, clip) => ({ ...acc, [clip.poster.id]: clip.poster }), {});
|
||||
const uniqueClipPosters = Array.from(new Set(clips.map((clip) => clip.poster.id) || [])).map((posterId) => clipPostersById[posterId]);
|
||||
const photosWithClipPosters = (this.release.photos || []).concat(this.release.scenesPhotos || []).concat(uniqueClipPosters);
|
||||
|
||||
if (this.release.trailer || (this.release.teaser && this.release.teaser.mime !== 'image/gif')) {
|
||||
// poster will be on trailer video
|
||||
return photosWithClipPosters;
|
||||
}
|
||||
|
||||
if (this.release.poster) {
|
||||
// no trailer, add poster to photos
|
||||
return [this.release.poster].concat(photosWithClipPosters);
|
||||
}
|
||||
|
||||
// no poster available
|
||||
return photosWithClipPosters;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
photos,
|
||||
},
|
||||
methods: {
|
||||
scrollBanner,
|
||||
},
|
||||
components: {
|
||||
Player,
|
||||
},
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
test: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['load'],
|
||||
data() {
|
||||
return {
|
||||
player: null,
|
||||
playing: false,
|
||||
paused: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
me,
|
||||
photos,
|
||||
poster,
|
||||
sfw,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
@import 'breakpoints';
|
||||
|
||||
.banner {
|
||||
background: $empty;
|
||||
.media {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
box-shadow: 0 0 3px $shadow;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
.media.center {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.poster-links {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.poster-link {
|
||||
transition: opacity .1s ease;
|
||||
padding: .5rem;
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: var(--lighten-strong);
|
||||
filter: drop-shadow(0 0 1px var(--darken-weak));
|
||||
}
|
||||
|
||||
&.playing {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-left: .75rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: .75rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trailer {
|
||||
display: inline-block;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.item-container,
|
||||
.trailer-container {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.trailer-video {
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
.warning {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
background: var(--darken-weak);
|
||||
color: var(--text-light);
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 3px var(--darken-strong);
|
||||
pointer-events: none;
|
||||
animation: alert .5s ease infinite .1s;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
fill: var(--text-light);
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 0 .25rem 0;
|
||||
filter: drop-shadow(0 0 3px var(--darken));
|
||||
animation: alert .5s ease infinite .1s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .warning {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 18rem;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
height: 18rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.item-more {
|
||||
height: auto;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: .5rem 2rem;
|
||||
color: var(--text-light);
|
||||
text-shadow: 0 0 3px var(--darken);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
|
||||
.link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.trailer-container {
|
||||
width: 32rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.trailer {
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
max-height: 100%;
|
||||
|
||||
&.sfw {
|
||||
filter: blur(2rem);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes alert {
|
||||
0% {
|
||||
color: var(--text-light);
|
||||
fill: var(--text-light);
|
||||
}
|
||||
50% {
|
||||
color: var(--alert);
|
||||
fill: var(--alert);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.media.center.preview {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-more {
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.media:not(.expanded) .item,
|
||||
.trailer-container {
|
||||
height: 56vw; /* 16:9 ratio for full-width video */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
235
assets/components/releases/chapters.vue
Normal file
235
assets/components/releases/chapters.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="timeline"
|
||||
class="timeline"
|
||||
>
|
||||
<ul class="timeline-items nolist">
|
||||
<li
|
||||
v-for="chapter in timeline"
|
||||
:key="`chapter-${chapter.id}`"
|
||||
:style="{ left: `${(chapter.time / duration) * 100}%` }"
|
||||
:title="formatDuration(chapter.time)"
|
||||
class="timeline-item"
|
||||
><router-link
|
||||
:to="`/tag/${chapter.tags[0].slug}`"
|
||||
class="link"
|
||||
>{{ chapter.tags[0]?.name || ' ' }}</router-link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-else
|
||||
class="chapters nolist"
|
||||
>
|
||||
<li
|
||||
v-for="chapter in chapters"
|
||||
:key="`chapter-${chapter.id}`"
|
||||
class="chapter"
|
||||
>
|
||||
<a
|
||||
v-if="chapter.poster"
|
||||
:href="getPath(chapter.poster)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="getPath(chapter.poster, 'thumbnail')"
|
||||
class="chapter-poster"
|
||||
>
|
||||
</a>
|
||||
|
||||
<span class="chapter-details">
|
||||
<span
|
||||
v-if="chapter.time"
|
||||
v-tooltip="'Time in video'"
|
||||
class="chapter-time"
|
||||
><Icon icon="film3" /> {{ formatDuration(chapter.time) }}</span>
|
||||
|
||||
<span
|
||||
v-if="chapter.duration"
|
||||
v-tooltip="'Duration'"
|
||||
class="chapter-duration"
|
||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||
</span>
|
||||
|
||||
<div class="chapter-info">
|
||||
<h3
|
||||
v-if="chapter.title"
|
||||
class="chapter-row chapter-title"
|
||||
:title="chapter.title"
|
||||
>{{ chapter.title }}</h3>
|
||||
|
||||
<p
|
||||
v-if="chapter.description"
|
||||
class="chapter-row chapter-description"
|
||||
>{{ chapter.description }}</p>
|
||||
|
||||
<Tags
|
||||
:tags="chapter.tags"
|
||||
class="chapter-row chapter-tags"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tags from './tags.vue';
|
||||
|
||||
function timeline() {
|
||||
if (this.chapters.every(chapter => chapter.time)) {
|
||||
return this.chapters.filter(chapter => chapter.tags?.length > 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tags,
|
||||
},
|
||||
props: {
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const lastChapter = this.chapters[this.chapters.length - 1];
|
||||
|
||||
return {
|
||||
duration: lastChapter.time + lastChapter.duration,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timeline,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.chapter-tags.tags-container {
|
||||
margin: 0 0 .5rem 0;
|
||||
|
||||
.tags {
|
||||
padding: 2px 0 0 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.chapters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.chapter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.chapter-poster {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.chapter-details {
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
margin: 0 0 .75rem 0;
|
||||
color: var(--text-light);
|
||||
background: var(--profile);
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
margin: -.1rem .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-duration,
|
||||
.chapter-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chapter-duration .icon {
|
||||
/* narrower icon */
|
||||
margin: -.1rem .3rem 0 0;
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
padding: 0 .5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.chapter-row {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chapter-description {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.timeline-items {
|
||||
position: relative;
|
||||
height: 5rem;
|
||||
border-bottom: solid 1px var(--shadow-weak);
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: absolute;
|
||||
bottom: -.25rem;
|
||||
padding: .1rem .5rem;
|
||||
border-radius: .6rem;
|
||||
color: var(--primary);
|
||||
background: var(--background);
|
||||
transform: rotate(-60deg);
|
||||
transform-origin: 0 50%;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
|
||||
.link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: calc(-1rem + 1px);
|
||||
margin: .3rem .5rem 0 0;
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.chapters {
|
||||
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
227
assets/components/releases/details.vue
Normal file
227
assets/components/releases/details.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div class="details">
|
||||
<div class="column">
|
||||
<div class="tidbits">
|
||||
<a
|
||||
:title="release.url && `View scene on ${release.entity.name}`"
|
||||
:href="release.url"
|
||||
:class="{ link: release.url }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tidbit date nolink"
|
||||
>
|
||||
<span class="date-compact">{{ release.date ? formatDate(release.date, 'MMM D, YYYY', release.datePrecision) : 'Date N/A' }}</span>
|
||||
<span class="date-full">{{ release.date ? formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) : 'Date unknown' }}</span>
|
||||
|
||||
<Icon
|
||||
v-if="release.url"
|
||||
icon="share2"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="site">
|
||||
<template v-if="release.entity.type === 'channel' && release.entity.parent && !release.entity.independent">
|
||||
<a
|
||||
v-if="release.entity.parent.hasLogo"
|
||||
:href="`/network/${release.entity.parent.slug}`"
|
||||
class="logo-link"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.parent.slug}/thumbs/network.png`"
|
||||
:title="release.entity.parent.name"
|
||||
:alt="release.entity.parent.name"
|
||||
class="logo logo-parent"
|
||||
>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else
|
||||
:href="`/network/${release.entity.parent.slug}`"
|
||||
class="logo-link logo-name"
|
||||
>{{ release.entity.parent.name }}</a>
|
||||
|
||||
<span class="chain">presents</span>
|
||||
|
||||
<a
|
||||
v-if="release.entity.hasLogo"
|
||||
:href="`/${release.entity.type}/${release.entity.slug}`"
|
||||
class="logo-link"
|
||||
>
|
||||
<img
|
||||
v-if="release.entity.type === 'network'"
|
||||
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
|
||||
:title="release.entity.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="`/img/logos/${release.entity.parent.slug}/thumbs/${release.entity.slug}.png`"
|
||||
:title="release.entity.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else
|
||||
:href="`/${release.entity.type}/${release.entity.slug}`"
|
||||
class="logo-link logo-name"
|
||||
>{{ release.entity.name }}</a>
|
||||
</template>
|
||||
|
||||
<a
|
||||
v-else
|
||||
:href="`/${release.entity.type}/${release.entity.slug}`"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
|
||||
:title="release.entity.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.details {
|
||||
background: var(--profile);
|
||||
color: var(--text-light);
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
cursor: default;
|
||||
|
||||
.column {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text-light);
|
||||
|
||||
.icon {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text-light);
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tidbits {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tidbit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&.date {
|
||||
flex-shrink: 0;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: var(--lighten);
|
||||
margin: -.2rem 0 0 .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.site {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 50%;
|
||||
padding: .25rem 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo-site {
|
||||
height: 2.25rem;
|
||||
max-width: 15rem;
|
||||
margin: .25rem 0;
|
||||
object-fit: contain;
|
||||
object-position: 100% 50%;
|
||||
}
|
||||
|
||||
.logo-parent {
|
||||
height: 1.5rem;
|
||||
max-width: 10rem;
|
||||
object-fit: contain;
|
||||
object-position: 100% 50%;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
padding: .5rem 0;
|
||||
color: var(--text-light);
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chain {
|
||||
color: var(--lighten);
|
||||
padding: 0 .5rem;
|
||||
font-weight: bold;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.date-compact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-mega) {
|
||||
.logo-parent,
|
||||
.chain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo-site {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-site {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.date-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.date-compact {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
303
assets/components/releases/movie-tile.vue
Normal file
303
assets/components/releases/movie-tile.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div class="tile">
|
||||
<Details :release="movie" />
|
||||
|
||||
<div class="movie">
|
||||
<RouterLink
|
||||
:to="{ name: 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }"
|
||||
class="cover"
|
||||
>
|
||||
<img
|
||||
v-if="movie.covers[0]"
|
||||
:src="getPath(movie.covers[0], 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(movie.covers[0], 'lazy') }"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:title="movie.title"
|
||||
class="unavailable"
|
||||
><Icon icon="blocked" /></div>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited"
|
||||
icon="heart7"
|
||||
class="stash stashed"
|
||||
@click.prevent.native="unstashMovie"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited === false"
|
||||
icon="heart8"
|
||||
class="stash unstashed"
|
||||
@click.prevent.native="stashMovie"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="stash && !stash.primary"
|
||||
icon="cross2"
|
||||
class="stash unstash"
|
||||
@click.prevent.native="unstashMovie"
|
||||
/>
|
||||
</RouterLink>
|
||||
|
||||
<div class="info">
|
||||
<RouterLink
|
||||
:to="{ name: 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }"
|
||||
class="title-link"
|
||||
>
|
||||
<h3 class="title">{{ movie.title }}</h3>
|
||||
</RouterLink>
|
||||
|
||||
<ul
|
||||
class="actors nolist"
|
||||
:title="movie.actors.map(actor => actor.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-for="actor in movie.actors"
|
||||
:key="`tag-${movie.id}-${actor.id}`"
|
||||
class="actor"
|
||||
><RouterLink
|
||||
:to="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="actor-link"
|
||||
>{{ actor.name }}</RouterLink></li>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:title="movie.tags.map(tag => tag.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-for="tag in movie.tags"
|
||||
:key="`tag-${movie.id}-${tag.id}`"
|
||||
class="tag"
|
||||
><RouterLink
|
||||
:to="`/tag/${tag.slug}`"
|
||||
class="tag-link"
|
||||
>{{ tag.name }}</RouterLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Details from './tile-details.vue';
|
||||
|
||||
async function stashMovie() {
|
||||
this.favorited = true;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('stashMovie', {
|
||||
movieId: this.movie.id,
|
||||
stashId: this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', true);
|
||||
} catch (error) {
|
||||
this.favorited = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unstashMovie() {
|
||||
if (!this.stash || this.stash.primary) {
|
||||
this.favorited = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('unstashMovie', {
|
||||
movieId: this.movie.id,
|
||||
stashId: this.stash?.id || this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', false);
|
||||
} catch (error) {
|
||||
this.favorited = true;
|
||||
}
|
||||
}
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Details,
|
||||
},
|
||||
props: {
|
||||
movie: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
favorited: this.movie.isFavorited,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sfw,
|
||||
},
|
||||
methods: {
|
||||
stashMovie,
|
||||
unstashMovie,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
font-size: 0;
|
||||
|
||||
&:hover .unstashed,
|
||||
&:hover .unstash {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title-link {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 16rem;
|
||||
width: 11.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
background-color: var(--shadow-hint);
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.unavailable .icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
fill: var(--shadow-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actors {
|
||||
height: 0;
|
||||
flex-grow: 1;
|
||||
padding: 0 1rem;
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actor:not(:last-child)::after {
|
||||
content: ',';
|
||||
margin: 0 .25rem 0 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.actor-link {
|
||||
font-size: 1rem;
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
padding: .25rem 1rem;
|
||||
height: 1.75rem;
|
||||
line-height: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
background: var(--background);
|
||||
font-size: .75rem;
|
||||
padding: .25rem .5rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stash {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: .25rem .5rem .5rem .5rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
fill: var(--lighten-weak);
|
||||
filter: drop-shadow(0 0 2px var(--darken));
|
||||
|
||||
&:hover.unstashed,
|
||||
&.stashed {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
&:hover.unstash {
|
||||
fill: var(--text-light);
|
||||
filter: drop-shadow(0 0 2px var(--darken-weak));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.cover {
|
||||
height: 12rem;
|
||||
width: 8.25rem;
|
||||
}
|
||||
|
||||
/* ensure no half actor names show */
|
||||
.actors {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
124
assets/components/releases/movies.vue
Normal file
124
assets/components/releases/movies.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="movies">
|
||||
<div class="content-inner">
|
||||
<SearchBar :placeholder="`Search ${totalCount} movies`" />
|
||||
|
||||
<div
|
||||
ref="tiles"
|
||||
class="tiles"
|
||||
>
|
||||
<MovieTile
|
||||
v-for="movie in movies"
|
||||
:key="`movie-${movie.id}`"
|
||||
:movie="movie"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
v-if="totalCount > 0"
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MovieTile from './movie-tile.vue';
|
||||
import SearchBar from '../search/bar.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
async function fetchMovies() {
|
||||
if (this.$route.query.query) {
|
||||
await this.searchMovies();
|
||||
return;
|
||||
}
|
||||
|
||||
const { movies, totalCount } = await this.$store.dispatch('fetchMovies', {
|
||||
limit: this.limit,
|
||||
range: this.$route.params.range,
|
||||
pageNumber: this.$route.params.pageNumber,
|
||||
});
|
||||
|
||||
this.movies = movies;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.$refs.tiles.scrollIntoView();
|
||||
}
|
||||
|
||||
async function searchMovies() {
|
||||
const { movies, totalCount } = await this.$store.dispatch('searchMovies', {
|
||||
query: this.$route.query.query,
|
||||
limit: this.limit,
|
||||
pageNumber: this.$route.params.pageNumber,
|
||||
});
|
||||
|
||||
this.movies = movies;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.$refs.tiles.scrollIntoView();
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.pageTitle = 'Movies';
|
||||
|
||||
await this.fetchMovies();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MovieTile,
|
||||
SearchBar,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
movies: [],
|
||||
totalCount: 0,
|
||||
limit: 20,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: fetchMovies,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchMovies,
|
||||
searchMovies,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.movies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,416 +1,509 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="release"
|
||||
class="content"
|
||||
>
|
||||
<Banner :release="release" />
|
||||
<div
|
||||
v-if="release"
|
||||
ref="content"
|
||||
class="content"
|
||||
@scroll="events.emit('scroll', $event)"
|
||||
>
|
||||
<Scroll
|
||||
v-slot="slotProps"
|
||||
class="scroll-light banner"
|
||||
:style="{ 'background-image': bannerBackground }"
|
||||
:expandable="false"
|
||||
>
|
||||
<Banner
|
||||
:release="release"
|
||||
class="media"
|
||||
@load="slotProps.loaded"
|
||||
/>
|
||||
</Scroll>
|
||||
|
||||
<div class="details">
|
||||
<div class="column">
|
||||
<a
|
||||
v-if="release.date"
|
||||
v-tooltip.bottom="release.url && `View scene on ${release.site.name}`"
|
||||
:title="release.url && `View scene on ${release.site.name}`"
|
||||
:href="release.url"
|
||||
:class="{ link: release.url }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tidbit date"
|
||||
>
|
||||
<Icon
|
||||
v-if="isAfter(new Date(), release.date)"
|
||||
icon="calendar2"
|
||||
/>
|
||||
<Details :release="release" />
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
v-tooltip.bottom="'To be released'"
|
||||
icon="sun3"
|
||||
/>
|
||||
<button
|
||||
v-if="release.photos?.length > 0 || release.scenesPhotos?.length > 0"
|
||||
class="album-toggle"
|
||||
@click="$router.push({ hash: '#album' })"
|
||||
><Icon icon="grid3" />View album</button>
|
||||
|
||||
<span class="showable">{{ formatDate(release.date, 'MMM D, YYYY') }}</span>
|
||||
<span class="hideable">{{ formatDate(release.date, 'MMMM D, YYYY') }}</span>
|
||||
</a>
|
||||
<Album
|
||||
v-if="showAlbum"
|
||||
:items="[release.poster, ...(release.photos || []), ...(release.scenesPhotos || [])]"
|
||||
:title="release.title"
|
||||
:path="config.media.mediaPath"
|
||||
@close="$router.replace({ hash: undefined })"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="release.shootId"
|
||||
v-tooltip.bottom="`Shoot #`"
|
||||
class="tidbit shoot hideable"
|
||||
>
|
||||
<Icon icon="clapboard-play" />
|
||||
{{ release.shootId }}
|
||||
</span>
|
||||
<div class="info column">
|
||||
<div class="row row-title">
|
||||
<h2
|
||||
v-if="release.title"
|
||||
class="title"
|
||||
>
|
||||
{{ release.title }}
|
||||
<template v-if="release.movies?.[0]?.title && /^scene \d+$/i.test(release.title)"><span class="title-composed"> from </span>{{ release.movies[0].title }}</template>
|
||||
</h2>
|
||||
|
||||
<span
|
||||
v-if="release.duration"
|
||||
v-tooltip.bottom="`Duration`"
|
||||
class="tidbit duration hideable"
|
||||
>
|
||||
<Icon icon="stopwatch" />
|
||||
<h2
|
||||
v-else-if="release.actors.length > 0"
|
||||
class="title title-composed"
|
||||
>
|
||||
{{ release.actors.map(actor => actor.name).join(', ') }} for {{ release.entity.name }}
|
||||
<Icon
|
||||
v-tooltip="`This scene has no known official title`"
|
||||
icon="question2"
|
||||
/>
|
||||
</h2>
|
||||
|
||||
<span
|
||||
v-if="release.duration >= 3600"
|
||||
class="duration-segment"
|
||||
>{{ Math.floor(release.duration / 3600).toString().padStart(2, '0') }}:</span>
|
||||
<span class="duration-segment">{{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}:</span>
|
||||
<span class="duration-segment">{{ (release.duration % 60).toString().padStart(2, '0') }}</span>
|
||||
</span>
|
||||
<StashButton
|
||||
:stashed-by="stashedBy"
|
||||
@stash="(stash) => stashScene(stash)"
|
||||
@unstash="(stash) => unstashScene(stash)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="tidbit site">
|
||||
<a
|
||||
v-if="release.site.independent"
|
||||
:href="`/network/${release.network.slug}`"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.network.slug}/network.png`"
|
||||
:title="release.network.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
</a>
|
||||
<div class="row associations">
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in release.actors"
|
||||
:key="actor.id"
|
||||
>
|
||||
<Actor :actor="actor" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<a :href="`/network/${release.network.slug}`">
|
||||
<img
|
||||
:src="`/img/logos/${release.network.slug}/network.png`"
|
||||
:title="release.network.name"
|
||||
:alt="release.network.name"
|
||||
class="logo logo-network"
|
||||
>
|
||||
</a>
|
||||
<Tags
|
||||
v-if="release.tags.length > 0"
|
||||
:tags="release.tags"
|
||||
/>
|
||||
|
||||
<span class="chain">presents</span>
|
||||
<div
|
||||
v-if="release.movies?.length > 0 || release.series?.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<span class="row-label">Part of</span>
|
||||
|
||||
<a
|
||||
:href="`/site/${release.site.slug}`"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.network.slug}/${release.site.slug}.png`"
|
||||
:title="release.site.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
<div class="movies">
|
||||
<router-link
|
||||
v-for="movie in [...release.movies, ...release.series]"
|
||||
:key="`movie-${movie.id}`"
|
||||
:to="{ name: movie.type || 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }"
|
||||
class="movie"
|
||||
>
|
||||
<span class="movie-title">{{ movie.title }}</span>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-if="movie.covers.length > 0 || movie.poster"
|
||||
:src="getPath(movie.covers[0] || movie.poster, 'thumbnail')"
|
||||
class="movie-cover"
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info column">
|
||||
<h2 class="row title">{{ release.title }}</h2>
|
||||
<Releases
|
||||
v-if="release.scenes && release.scenes.length > 0"
|
||||
:releases="release.scenes"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in release.actors"
|
||||
:key="actor.id"
|
||||
>
|
||||
<Actor :actor="actor" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="release.directors && release.directors.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<span class="row-label">Director</span>
|
||||
|
||||
<div v-if="release.scenes && release.scenes.length > 0">
|
||||
<h3>Scenes</h3>
|
||||
<router-link
|
||||
v-for="director in release.directors"
|
||||
:key="`director-${director.id}`"
|
||||
class="link director"
|
||||
:to="`/director/${director.id}/${director.slug}`"
|
||||
>{{ director.name }}</router-link>
|
||||
</div>
|
||||
|
||||
<Releases
|
||||
v-if="release.scenes && release.scenes.length > 0"
|
||||
:releases="release.scenes"
|
||||
class="row"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="release.description"
|
||||
class="row"
|
||||
>
|
||||
<span class="row-label">Description</span>
|
||||
<p class="description">{{ release.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="release.movie">
|
||||
<h3>Movie</h3>
|
||||
<div
|
||||
v-if="release.chapters?.length > 0"
|
||||
class="row nolist"
|
||||
>
|
||||
<span class="row-label">Chapters</span>
|
||||
|
||||
<Release :release="release.movie" />
|
||||
</div>
|
||||
<Chapters :chapters="release.chapters" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="release.tags.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<Icon icon="price-tags3" />
|
||||
<div class="row row-tidbits">
|
||||
<div
|
||||
v-if="release.duration"
|
||||
class="row-tidbit"
|
||||
>
|
||||
<span class="row-label">Duration</span>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="tag in release.tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
class="tag"
|
||||
>
|
||||
<a
|
||||
:href="`/tag/${tag.slug}`"
|
||||
class="link"
|
||||
>{{ tag.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="duration">{{ formatDuration(release.duration) }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="release.duration"
|
||||
class="row duration showable"
|
||||
>
|
||||
<Icon icon="stopwatch" />
|
||||
<div
|
||||
v-if="release.shootId"
|
||||
class="row-tidbit"
|
||||
>
|
||||
<span class="row-label">Shoot #</span>
|
||||
{{ release.shootId }}
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="release.duration >= 3600"
|
||||
class="duration-segment"
|
||||
>{{ Math.floor(release.duration / 3600).toString().padStart(2, '0') }}:</span>
|
||||
<span class="duration-segment">{{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}:</span>
|
||||
<span class="duration-segment">{{ (release.duration % 60).toString().padStart(2, '0') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="release.studio"
|
||||
class="row-tidbit"
|
||||
>
|
||||
<span class="row-label">Studio</span>
|
||||
|
||||
<p
|
||||
v-if="release.description"
|
||||
class="row description"
|
||||
>
|
||||
<Icon icon="info2" />
|
||||
{{ release.description }}
|
||||
</p>
|
||||
<router-link
|
||||
:to="`/studio/${release.studio.slug}`"
|
||||
class="link studio"
|
||||
>{{ release.studio.name }}</router-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="release.studio"
|
||||
class="row"
|
||||
>
|
||||
<Icon icon="video-camera2" />
|
||||
<div
|
||||
v-if="release.productionDate"
|
||||
class="row-tidbit"
|
||||
>
|
||||
<span class="row-label">Shoot date</span>
|
||||
{{ formatDate(release.productionDate, 'MMMM D, YYYY') }}
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="release.studio"
|
||||
:href="release.studio.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
>{{ release.studio.name }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="release.productionLocation"
|
||||
class="row-tidbit"
|
||||
>
|
||||
<span class="row-label">Location</span>
|
||||
<span class="location">
|
||||
<span
|
||||
v-if="release.productionLocation.city"
|
||||
class="location-segment"
|
||||
>{{ release.productionLocation.city }}, </span>
|
||||
<span
|
||||
v-if="release.productionLocation.state"
|
||||
class="location-segment"
|
||||
>{{ release.productionLocation.state }}, </span>
|
||||
<span
|
||||
v-if="release.productionLocation.country"
|
||||
class="location-segment"
|
||||
>{{ release.productionLocation.country.alias || release.productionLocation.country.name }}
|
||||
<img
|
||||
class="flag"
|
||||
:src="`/img/flags/${release.productionLocation.country.alpha2.toLowerCase()}.svg`"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="release.shootId"
|
||||
class="row showable"
|
||||
>
|
||||
<Icon icon="clapboard-play" />
|
||||
<div
|
||||
v-if="release.comment"
|
||||
class="row"
|
||||
>
|
||||
<span class="row-label">Comment</span>
|
||||
<span>{{ release.comment }}</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="release.url"
|
||||
:title="`release.shootId`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link shoot"
|
||||
>{{ release.shootId }}</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="row-label">Added</span>
|
||||
|
||||
<span class="row">
|
||||
<Icon icon="drawer-in" />
|
||||
|
||||
<a
|
||||
:href="`/added/${formatDate(release.dateAdded, 'YYYY-MM-DD')}`"
|
||||
:title="`Added on ${formatDate(release.dateAdded, 'MMMM D, YYYY')}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link added"
|
||||
>{{ formatDate(release.dateAdded, 'MMMM D, YYYY') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="`/added/${formatDate(release.createdAt, 'YYYY/MM/DD')}`"
|
||||
:title="`Added on ${formatDate(release.createdAt, 'MMMM D, YYYY HH:mm')}`"
|
||||
class="link added"
|
||||
>{{ release.createdBatchId }}: {{ formatDate(release.createdAt, 'MMMM D, YYYY HH:mm') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Details from './details.vue';
|
||||
import Banner from './banner.vue';
|
||||
import Actor from '../tile/actor.vue';
|
||||
import Release from '../tile/release.vue';
|
||||
import StashButton from '../stashes/button.vue';
|
||||
import Album from '../album/album.vue';
|
||||
import Tags from './tags.vue';
|
||||
import Chapters from './chapters.vue';
|
||||
import Actor from '../actors/tile.vue';
|
||||
import Releases from './releases.vue';
|
||||
import Scroll from '../scroll/scroll.vue';
|
||||
|
||||
function pageTitle() {
|
||||
return this.release && this.release.title;
|
||||
async function fetchRelease(scroll = true) {
|
||||
if (this.$route.name === 'scene') {
|
||||
this.release = await this.$store.dispatch('fetchReleaseById', this.$route.params.releaseId);
|
||||
}
|
||||
|
||||
if (this.$route.name === 'movie') {
|
||||
this.release = await this.$store.dispatch('fetchMovieById', this.$route.params.releaseId);
|
||||
}
|
||||
|
||||
if (this.$route.name === 'serie') {
|
||||
this.release = await this.$store.dispatch('fetchSerieById', this.$route.params.releaseId);
|
||||
}
|
||||
|
||||
if (scroll && this.$refs.content) {
|
||||
this.$refs.content.scrollTop = 0;
|
||||
}
|
||||
|
||||
this.stashedBy = this.release.stashes;
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
this.release = await this.$store.dispatch('fetchReleaseById', this.$route.params.releaseId);
|
||||
async function stashScene(stashId) {
|
||||
this.stashedBy = await this.$store.dispatch(this.$route.name === 'movie' ? 'stashMovie' : 'stashScene', {
|
||||
sceneId: this.release.id,
|
||||
movieId: this.release.id,
|
||||
stashId,
|
||||
});
|
||||
}
|
||||
|
||||
async function unstashScene(stashId) {
|
||||
this.stashedBy = await this.$store.dispatch(this.$route.name === 'movie' ? 'unstashMovie' : 'unstashScene', {
|
||||
sceneId: this.release.id,
|
||||
movieId: this.release.id,
|
||||
stashId,
|
||||
});
|
||||
}
|
||||
|
||||
function me() {
|
||||
return this.$store.state.auth.user;
|
||||
}
|
||||
|
||||
function bannerBackground() {
|
||||
return (this.release.poster && this.getBgPath(this.release.poster, 'thumbnail'))
|
||||
|| (this.release.covers.length > 0 && this.getBgPath(this.release.covers[0], 'thumbnail'));
|
||||
}
|
||||
|
||||
function pageTitle() {
|
||||
return this.release
|
||||
&& (this.release.title
|
||||
|| (this.release.actors.length > 0 ? `${this.release.actors.map((actor) => actor.name).join(', ')} for ${this.release.entity.name}` : null));
|
||||
}
|
||||
|
||||
function showAlbum() {
|
||||
return (this.release.photos?.length > 0 || this.release.scenesPhotos?.length > 0) && this.$route.hash === '#album';
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actor,
|
||||
Banner,
|
||||
Releases,
|
||||
Release,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
release: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageTitle,
|
||||
},
|
||||
mounted,
|
||||
components: {
|
||||
Actor,
|
||||
Album,
|
||||
Banner,
|
||||
Chapters,
|
||||
Details,
|
||||
Releases,
|
||||
Scroll,
|
||||
StashButton,
|
||||
Tags,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
release: null,
|
||||
stashedBy: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageTitle,
|
||||
bannerBackground,
|
||||
me,
|
||||
showAlbum,
|
||||
},
|
||||
watch: {
|
||||
$route: fetchRelease,
|
||||
},
|
||||
mounted: fetchRelease,
|
||||
methods: {
|
||||
fetchRelease,
|
||||
stashScene,
|
||||
unstashScene,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
.column {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
padding: 0 1rem;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
@import 'breakpoints';
|
||||
.expand-bottom {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.banner {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
|
||||
:deep(.scrollable) {
|
||||
backdrop-filter: blur(1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 1rem;
|
||||
border-left: solid 1px $shadow-hint;
|
||||
border-right: solid 1px $shadow-hint;
|
||||
padding: 1rem 0;
|
||||
border-left: solid 1px var(--shadow-hint);
|
||||
border-right: solid 1px var(--shadow-hint);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
fill: $shadow-strong;
|
||||
margin: 0 1rem 0 0;
|
||||
&.associations {
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
background: $profile;
|
||||
color: $text-contrast;
|
||||
box-shadow: 0 0 3px $shadow-weak;
|
||||
cursor: default;
|
||||
.row-label {
|
||||
display: block;
|
||||
margin: 0 0 .5rem 0;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $text-contrast;
|
||||
}
|
||||
.icon {
|
||||
margin: 0 .5rem 0 0;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.tidbit {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px $highlight-hint;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: $highlight-weak;
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
&.date,
|
||||
&.duration,
|
||||
&.shoot {
|
||||
flex-shrink: 0;
|
||||
padding: 1.25rem 1rem 1.25rem 0;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
.row-tidbit {
|
||||
display: inline-block;
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
.site {
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: .25rem 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-block;
|
||||
filter: $logo-highlight;
|
||||
}
|
||||
|
||||
.logo-site {
|
||||
height: 3rem;
|
||||
max-width: 15rem;
|
||||
object-fit: contain;
|
||||
object-position: 100% 50%;
|
||||
}
|
||||
|
||||
.logo-network {
|
||||
height: 1.5rem;
|
||||
max-width: 10rem;
|
||||
object-fit: contain;
|
||||
object-position: 100% 50%;
|
||||
}
|
||||
|
||||
.chain {
|
||||
color: $highlight;
|
||||
padding: 0 .5rem;
|
||||
font-weight: bold;
|
||||
font-size: .8rem;
|
||||
.row-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 1.5rem 0;
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.25;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
padding: .25rem;
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-composed {
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
.album-toggle {
|
||||
height: fit-content;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .5rem 1rem;
|
||||
border: none;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
color: var(--shadow);
|
||||
background: none;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: -.1rem .5rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.duration-segment {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin: -.25rem 0 0 0;
|
||||
}
|
||||
|
||||
.actors {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actor {
|
||||
width: 10rem;
|
||||
margin: 0 1rem .5rem 0;
|
||||
.movies {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.movie {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover .movie-title {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.movie-cover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.movie-title {
|
||||
padding: .5rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.releases {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.flag {
|
||||
height: 1rem;
|
||||
margin: 0 0 -.15rem .1rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
color: $link;
|
||||
display: inline-flex;
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
|
||||
&.director:not(:last-child)::after {
|
||||
content: ', ';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: $primary;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag .link {
|
||||
background: $background;
|
||||
display: inline-block;
|
||||
padding: .5rem;
|
||||
margin: 0 .25rem .25rem 0;
|
||||
box-shadow: 0 0 2px $shadow-weak;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.showable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint3) {
|
||||
.logo-network,
|
||||
.chain {
|
||||
display: none;
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.releases {
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,17 +512,20 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row .showable {
|
||||
display: block;
|
||||
.row .showable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tidbit .showable {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo-site {
|
||||
width: 15rem;
|
||||
max-width: 100%;
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.actors {
|
||||
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,60 +1,92 @@
|
||||
<template>
|
||||
<div class="releases">
|
||||
<h3
|
||||
v-if="context"
|
||||
class="heading"
|
||||
><span class="range">{{ range }}</span> releases for '{{ context }}'</h3>
|
||||
<div class="releases">
|
||||
<h3
|
||||
v-if="context"
|
||||
class="heading"
|
||||
><span class="range">{{ range }}</span> releases for '{{ context }}'</h3>
|
||||
|
||||
<ul class="nolist tiles">
|
||||
<li
|
||||
v-for="release in releases"
|
||||
:key="`release-${release.id}`"
|
||||
>
|
||||
<ReleaseTile :release="release" />
|
||||
</li>
|
||||
</ul>
|
||||
<Ellipsis v-if="!done" />
|
||||
|
||||
<span
|
||||
v-if="releases.length === 0 && range !== 'all'"
|
||||
class="empty"
|
||||
>No {{ range }} releases</span>
|
||||
<ul
|
||||
v-else-if="releases.length > 0"
|
||||
:key="sfw"
|
||||
class="nolist tiles"
|
||||
>
|
||||
<li
|
||||
v-for="(release, index) in releases"
|
||||
:key="`release-${release.id}`"
|
||||
>
|
||||
<SceneTile
|
||||
:release="release"
|
||||
:referer="referer"
|
||||
:index="index"
|
||||
:stash="stash"
|
||||
@stash="isStashed => $emit('stash', isStashed)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span
|
||||
v-else-if="releases.length === 0"
|
||||
class="empty"
|
||||
>No recent or upcoming releases</span>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="releases.length === 0 && range !== 'all'"
|
||||
class="empty"
|
||||
>No {{ range }} releases</span>
|
||||
|
||||
<span
|
||||
v-else-if="releases.length === 0"
|
||||
class="empty"
|
||||
>No recent or upcoming releases</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ReleaseTile from '../tile/release.vue';
|
||||
import Ellipsis from '../loading/ellipsis.vue';
|
||||
import SceneTile from './scene-tile.vue';
|
||||
|
||||
function range() {
|
||||
return this.$store.state.ui.range;
|
||||
return this.$route.params.range;
|
||||
}
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReleaseTile,
|
||||
},
|
||||
props: {
|
||||
releases: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
range,
|
||||
},
|
||||
components: {
|
||||
Ellipsis,
|
||||
SceneTile,
|
||||
},
|
||||
props: {
|
||||
releases: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
done: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
referer: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['stash'],
|
||||
computed: {
|
||||
range,
|
||||
sfw,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
@import 'breakpoints';
|
||||
|
||||
.heading {
|
||||
padding: 0;
|
||||
@@ -65,27 +97,58 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.releases {
|
||||
flex-grow: 1;
|
||||
border-top: solid 1px var(--crease);
|
||||
|
||||
&.embedded {
|
||||
border: none;
|
||||
|
||||
.tiles {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiles {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, .33fr));
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: $shadow-strong;
|
||||
display: inline-block;
|
||||
padding: 1rem;
|
||||
color: var(--shadow-strong);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint4) {
|
||||
@media(max-width: $breakpoint-mega) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, .5fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18.5rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
118
assets/components/releases/scene-preview.vue
Normal file
118
assets/components/releases/scene-preview.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<a
|
||||
:href="`/scene/${scene.id}/${scene.slug || ''}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="scene nolink"
|
||||
>
|
||||
<img
|
||||
:src="getPath(scene.poster, 'thumbnail')"
|
||||
class="scene-poster"
|
||||
>
|
||||
|
||||
<div class="scene-header">
|
||||
<span class="scene-actors nolist">{{ scene.actors.map(actor => actor.name).join(', ') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="scene-footer">
|
||||
<img
|
||||
v-if="scene.entity.parent"
|
||||
:src="`/img/logos/${scene.entity.parent.slug}/favicon_light.png`"
|
||||
class="scene-favicon"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="`/img/logos/${scene.entity.slug}/favicon_light.png`"
|
||||
class="scene-favicon"
|
||||
>
|
||||
|
||||
<span class="scene-title">{{ scene.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scene {
|
||||
width: 14rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.scene-poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 0;
|
||||
}
|
||||
|
||||
.scene-header,
|
||||
.scene-footer {
|
||||
width: 100%;
|
||||
height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: var(--darken-weak);
|
||||
color: var(--text-light);
|
||||
font-size: .7rem;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 0 2px var(--text-dark);
|
||||
}
|
||||
|
||||
.scene-header {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.scene-footer {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.scene-title {
|
||||
padding: .25rem .5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scene-actors {
|
||||
padding: 0 .5rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scene-unstash {
|
||||
fill: var(--lighten-strong);
|
||||
padding: .25rem;
|
||||
filter: drop-shadow(0 0 1px var(--shadow));
|
||||
|
||||
&:hover {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.scene-favicon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: .1rem 0 0 .25rem;
|
||||
}
|
||||
</style>
|
||||
501
assets/components/releases/scene-tile.vue
Normal file
501
assets/components/releases/scene-tile.vue
Normal file
@@ -0,0 +1,501 @@
|
||||
<template>
|
||||
<div
|
||||
:id="`scene-${release.id}`"
|
||||
:class="{ new: release.isNew }"
|
||||
class="tile"
|
||||
>
|
||||
<Details
|
||||
:release="release"
|
||||
class="details-compact"
|
||||
/>
|
||||
|
||||
<div class="tile-body">
|
||||
<span class="poster">
|
||||
<a
|
||||
:href="`/scene/${release.id}/${release.slug || ''}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
>
|
||||
<img
|
||||
v-if="release.poster"
|
||||
:src="getPath(release.poster, 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(release.poster, 'lazy') }"
|
||||
:alt="release.title"
|
||||
:width="release.poster.thumbnailWidth"
|
||||
:height="release.poster.thumbnailHeight"
|
||||
class="thumbnail"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="release.photos && release.photos.length > 0"
|
||||
:src="getPath(release.photos[0], 'thumbnail')"
|
||||
:style="{ 'background-image': getBgPath(release.photos[0], 'lazy') } "
|
||||
:alt="release.title"
|
||||
:width="release.photos[0].thumbnailWidth"
|
||||
:height="release.photos[0].thumbnailHeight"
|
||||
class="thumbnail"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:title="release.title"
|
||||
class="thumbnail unavailable"
|
||||
><Icon icon="blocked" />No thumbnail available</div>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited"
|
||||
icon="heart7"
|
||||
class="stash stashed"
|
||||
@click.prevent.native="unstashScene"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="(!stash || stash.primary) && favorited === false"
|
||||
icon="heart8"
|
||||
class="stash unstashed"
|
||||
@click.prevent.native="stashScene"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="stash && !stash.primary"
|
||||
icon="cross2"
|
||||
class="stash unstash"
|
||||
@click.prevent.native="unstashScene"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div class="info">
|
||||
<Details
|
||||
:release="release"
|
||||
class="details-wide"
|
||||
/>
|
||||
|
||||
<a
|
||||
:href="`/scene/${release.id}/${release.slug || ''}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="row link"
|
||||
>
|
||||
<h3
|
||||
v-if="release.title"
|
||||
v-tooltip.bottom="release.title"
|
||||
:title="release.title"
|
||||
class="title"
|
||||
>
|
||||
{{ release.title }}<template v-if="release.movies?.[0]?.title && /^scene \d+$/i.test(release.title)"><span class="title-composed"> from </span>{{ release.movies[0].title }}</template>
|
||||
</h3>
|
||||
|
||||
<h3
|
||||
v-else-if="release.actors.length > 0"
|
||||
class="title title-composed"
|
||||
>{{ release.actors[0].name }} for {{ release.entity.name }}</h3>
|
||||
|
||||
<h3
|
||||
v-else
|
||||
class="title title-empty"
|
||||
>{{ release.entity.name }}</h3>
|
||||
</a>
|
||||
|
||||
<span
|
||||
v-if="release.actors?.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<ul
|
||||
class="actors nolist"
|
||||
:title="release.actors.map(actor => actor.name).join(', ')"
|
||||
>
|
||||
<li
|
||||
v-for="actor in release.actors"
|
||||
:key="actor.id"
|
||||
class="actor"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{ name: 'actor', params: { actorId: actor.id, actorSlug: actor.slug } }"
|
||||
:class="{ [actor.gender]: !!actor.gender }"
|
||||
class="actor-link"
|
||||
>{{ actor.name }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
|
||||
<div class="labels">
|
||||
<RouterLink
|
||||
v-if="release.shootId && release.studio"
|
||||
:to="`/studio/${release.studio.slug}`"
|
||||
:title="release.studio && release.studio.name"
|
||||
class="shoot nolink"
|
||||
>{{ release.shootId }}</RouterLink>
|
||||
|
||||
<span
|
||||
v-else-if="release.shootId"
|
||||
:title="release.studio && release.studio.name"
|
||||
class="shoot nolink"
|
||||
>{{ release.shootId }}</span>
|
||||
|
||||
<ul
|
||||
v-if="release.tags?.length > 0"
|
||||
:title="release.tags.map(tag => tag.name).join(', ')"
|
||||
class="tags nolist"
|
||||
>
|
||||
<li
|
||||
v-for="tag in release.tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
class="tag"
|
||||
>
|
||||
<RouterLink
|
||||
:to="`/tag/${tag.slug}`"
|
||||
class="tag-link"
|
||||
>{{ tag.name }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Details from './tile-details.vue';
|
||||
|
||||
async function stashScene() {
|
||||
this.favorited = true;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('stashScene', {
|
||||
sceneId: this.release.id,
|
||||
stashId: this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', true);
|
||||
} catch (error) {
|
||||
this.favorited = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unstashScene() {
|
||||
if (!this.stash || this.stash.primary) {
|
||||
this.favorited = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('unstashScene', {
|
||||
sceneId: this.release.id,
|
||||
stashId: this.stash?.id || this.$store.getters.favorites.id,
|
||||
});
|
||||
|
||||
this.$emit('stash', false);
|
||||
} catch (error) {
|
||||
this.favorited = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Details,
|
||||
},
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['stash'],
|
||||
data() {
|
||||
return {
|
||||
favorited: this.release.isFavorited,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
stashScene,
|
||||
unstashScene,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.tile {
|
||||
background: var(--background);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
|
||||
&.new .poster::after {
|
||||
content: 'new';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
bottom: 0;
|
||||
right: .5rem;
|
||||
width: 2rem;
|
||||
box-sizing: border-box;
|
||||
padding: .1rem 0 .05rem 0;
|
||||
border-radius: .25rem .25rem 0 0;
|
||||
background: var(--info);
|
||||
color: var(--text-light);
|
||||
font-size: .7rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
box-shadow: 0 0 3px var(--shadow);
|
||||
}
|
||||
|
||||
&:hover .unstashed,
|
||||
&:hover .unstash {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
}
|
||||
|
||||
.tile-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.poster {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.covers {
|
||||
background: var(--profile);
|
||||
display: flex;
|
||||
|
||||
.cover {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 14rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
object-fit: cover;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--shadow-hint);
|
||||
color: var(--shadow);
|
||||
text-shadow: 1px 1px 0 var(--highlight);
|
||||
|
||||
.icon {
|
||||
display: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
fill: var(--shadow-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.stash {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: .25rem .25rem .5rem .5rem;
|
||||
filter: drop-shadow(0 0 2px var(--darken));
|
||||
fill: var(--lighten-weak);
|
||||
|
||||
&:hover.unstashed,
|
||||
&.stashed {
|
||||
fill: var(--primary);
|
||||
filter: drop-shadow(0 0 2px var(--darken-weak));
|
||||
}
|
||||
|
||||
&:hover.unstash {
|
||||
fill: var(--text-light);
|
||||
filter: drop-shadow(0 0 2px var(--darken-weak));
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: .9rem;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-composed,
|
||||
.title-empty {
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
.actors {
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
max-height: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
|
||||
.actor:not(:last-of-type)::after {
|
||||
content: ",";
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
.actor-link {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-size: .9rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.labels {
|
||||
padding: 0 .5rem 1.25rem .5rem;
|
||||
max-height: .5rem;
|
||||
overflow-y: hidden;
|
||||
font-size: 0;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.shoot {
|
||||
display: inline;
|
||||
padding: .25rem .5rem;
|
||||
background: var(--primary);
|
||||
color: var(--text-light);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
box-shadow: inset 0 0 3px var(--shadow-weak);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: inline;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 0 1rem 0;
|
||||
|
||||
&:not(:first-child) .tag-link {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
background: var(--shadow-touch);
|
||||
color: var(--shadow);
|
||||
display: inline-block;
|
||||
padding: .25rem .5rem;
|
||||
margin: 0 1px 0 0;
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.details-wide {
|
||||
margin: 0 0 .4rem 0;
|
||||
}
|
||||
|
||||
.details-compact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.thumbnail {
|
||||
height: 11rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.tile-body {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.poster {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 9rem;
|
||||
height: 6rem;
|
||||
font-size: 0;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: .5rem .25rem 0 .25rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0 0 .15rem 0;
|
||||
}
|
||||
|
||||
.title,
|
||||
.actor-link {
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.details-wide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.details-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.shoot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tile.new .poster::after {
|
||||
top: 0;
|
||||
right: .25rem;
|
||||
bottom: auto;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
padding: .05rem 0 .1rem 0;
|
||||
}
|
||||
|
||||
.stash {
|
||||
left: 0;
|
||||
padding: .25rem .5rem .5rem .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
assets/components/releases/tags.vue
Normal file
136
assets/components/releases/tags.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div
|
||||
class="tags-container"
|
||||
:class="{ overflowing }"
|
||||
>
|
||||
<ul
|
||||
ref="tags"
|
||||
class="tags nolist"
|
||||
:class="{ expanded }"
|
||||
>
|
||||
<li
|
||||
v-for="tag in tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
class="tag"
|
||||
>
|
||||
<a
|
||||
:href="`/tag/${tag.slug}`"
|
||||
class="link"
|
||||
>{{ tag.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
v-if="overflowing && !expanded"
|
||||
class="tags-more"
|
||||
@click="expanded = true"
|
||||
>more tags</button>
|
||||
|
||||
<button
|
||||
v-if="expanded"
|
||||
class="tags-more"
|
||||
@click="expanded = false"
|
||||
>fewer tags</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
function updateOverflowing() {
|
||||
nextTick(() => {
|
||||
const containerBoundaries = this.$refs.tags.getBoundingClientRect();
|
||||
const containerBottom = containerBoundaries.top + containerBoundaries.height;
|
||||
|
||||
this.overflowing = Array.from(this.$refs.tags.querySelectorAll('.tag')).some((tag) => {
|
||||
const tagBoundaries = tag.getBoundingClientRect();
|
||||
|
||||
return tagBoundaries.top > containerBottom;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
window.addEventListener('resize', this.updateOverflowing);
|
||||
this.updateOverflowing();
|
||||
}
|
||||
|
||||
function beforeUnmount() {
|
||||
window.removeEventListener('resize', this.updateOverflowing);
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
overflowing: false,
|
||||
expanded: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
expanded: updateOverflowing,
|
||||
},
|
||||
mounted,
|
||||
beforeUnmount,
|
||||
methods: {
|
||||
updateOverflowing,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-container {
|
||||
margin: 0 0 1.5rem 0;
|
||||
|
||||
&.overflowing {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
max-height: 4.7rem;
|
||||
padding: 2px 1rem 0 1rem;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
&.expanded {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.tag .link {
|
||||
color: var(--link);
|
||||
background: var(--background);
|
||||
display: inline-block;
|
||||
padding: .5rem;
|
||||
margin: 0 .25rem .25rem 0;
|
||||
box-shadow: 0 0 2px var(--shadow-weak);
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tags-more {
|
||||
background: var(--shadow-touch);
|
||||
padding: .5rem 1rem;
|
||||
border: none;
|
||||
margin: .25rem 0 .5rem 1rem;
|
||||
color: var(--shadow);
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
color: var(--shadow-strong);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
assets/components/releases/tile-details.vue
Normal file
171
assets/components/releases/tile-details.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<span
|
||||
class="details"
|
||||
:class="{ new: release.isNew }"
|
||||
>
|
||||
<span
|
||||
v-if="release.entity && release.entity.type !== 'network' && !release.entity.independent && release.entity.parent"
|
||||
class="site"
|
||||
>
|
||||
<router-link
|
||||
v-tooltip.bottom="`Part of ${release.entity.parent.name}`"
|
||||
:title="`Part of ${release.entity.parent.name}`"
|
||||
:to="`/${release.entity.parent.type}/${release.entity.parent.slug}`"
|
||||
class="site-link"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.parent.slug}/favicon_light.png`"
|
||||
class="favicon favicon-light"
|
||||
>
|
||||
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.parent.slug}/favicon_dark.png`"
|
||||
class="favicon favicon-dark"
|
||||
>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-tooltip.bottom="`More from ${release.entity.name}`"
|
||||
:title="`More from ${release.entity.name}`"
|
||||
:to="`/${release.entity.type}/${release.entity.slug}`"
|
||||
class="site-link"
|
||||
>{{ release.entity.name }}</router-link>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
v-else-if="release.entity"
|
||||
:to="`/${release.entity.type}/${release.entity.slug}`"
|
||||
class="site site-link"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.slug}/favicon_light.png`"
|
||||
class="favicon favicon-light"
|
||||
>
|
||||
|
||||
<img
|
||||
:src="`/img/logos/${release.entity.slug}/favicon_dark.png`"
|
||||
class="favicon favicon-dark"
|
||||
>{{ release.entity.name }}
|
||||
</router-link>
|
||||
|
||||
<a
|
||||
v-if="release.date"
|
||||
v-tooltip.bottom="release.url && `View release on ${release.entity.name}`"
|
||||
:title="release.url && `View release on ${release.entity.name}`"
|
||||
:href="release.url"
|
||||
:class="{ upcoming: isAfter(release.date, new Date()) }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="date"
|
||||
>{{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }}</a>
|
||||
|
||||
<a
|
||||
v-else
|
||||
:href="release.url"
|
||||
title="Scene date N/A, showing date added"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="date"
|
||||
>{{ `(${formatDate(release.createdAt, 'MMMM D, YYYY')})` }}</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
background: var(--info);
|
||||
font-size: 0;
|
||||
font-weight: bold;
|
||||
|
||||
.favicon {
|
||||
width: 2rem;
|
||||
box-sizing: border-box;
|
||||
padding: .4rem .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.site,
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-size: .8rem;
|
||||
padding: .4rem .5rem;
|
||||
color: var(--text-light);
|
||||
text-decoration: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--lighten-weak);
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.site {
|
||||
padding: 0 .5rem 0 0;
|
||||
}
|
||||
|
||||
.site-link {
|
||||
display: flex;
|
||||
color: var(--text-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.favicon-dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
/* light details bar
|
||||
.details {
|
||||
background: var(--background);
|
||||
box-shadow: none;
|
||||
|
||||
.favicon-dark {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.favicon-light {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.site-link,
|
||||
.date {
|
||||
color: var(--text);
|
||||
}
|
||||
*/
|
||||
|
||||
.details .favicon {
|
||||
padding: .35rem .5rem .35rem .5rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
padding: .35rem .5rem;
|
||||
}
|
||||
|
||||
.site {
|
||||
padding: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
251
assets/components/scroll/scroll.vue
Normal file
251
assets/components/scroll/scroll.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="scroll">
|
||||
<Expand
|
||||
v-if="expanded"
|
||||
:expanded="expanded"
|
||||
class="expand-light"
|
||||
@expand="(state) => $emit('expand', state)"
|
||||
/>
|
||||
|
||||
<Expand
|
||||
v-if="expanded"
|
||||
:expanded="expanded"
|
||||
class="expand-dark"
|
||||
@expand="(state) => $emit('expand', state)"
|
||||
/>
|
||||
|
||||
<div class="scrollable">
|
||||
<div
|
||||
v-show="enabled && !expanded"
|
||||
class="scroll-button scroll-left noselect"
|
||||
:class="{ 'scroll-start': scrollAtStart }"
|
||||
@click="scroll('left')"
|
||||
><Icon icon="arrow-left3" /></div>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="scroll-content"
|
||||
>
|
||||
<slot :loaded="loaded" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="enabled && !expanded"
|
||||
class="scroll-button scroll-right noselect"
|
||||
:class="{ 'scroll-end': scrollAtEnd }"
|
||||
@click="scroll('right')"
|
||||
><Icon icon="arrow-right3" /></div>
|
||||
</div>
|
||||
|
||||
<Expand
|
||||
v-if="expanded || (expandable && scrollable)"
|
||||
:expanded="expanded"
|
||||
class="expand-light"
|
||||
@expand="(state) => $emit('expand', state)"
|
||||
/>
|
||||
|
||||
<Expand
|
||||
v-if="expanded || (expandable && scrollable)"
|
||||
:expanded="expanded"
|
||||
class="expand-dark"
|
||||
@expand="(state) => $emit('expand', state)"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="album && items && items.length > 0 && scrollable"
|
||||
class="album-toggle"
|
||||
@click="showAlbum = true"
|
||||
><Icon icon="grid3" />View album</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Expand from '../expand/expand.vue';
|
||||
|
||||
function updateScroll() {
|
||||
this.scrollable = this.$refs.content.scrollWidth > this.$refs.content.clientWidth;
|
||||
this.scrollAtStart = this.$refs.content.scrollLeft === 0;
|
||||
this.scrollAtEnd = this.$refs.content.scrollWidth - this.$refs.content.clientWidth === this.$refs.content.scrollLeft;
|
||||
}
|
||||
|
||||
function scroll(direction) {
|
||||
if (direction === 'right') {
|
||||
this.$refs.content.scrollLeft = this.$refs.content.scrollLeft + this.$refs.content.clientWidth - 100;
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
this.$refs.content.scrollLeft = this.$refs.content.scrollLeft - this.$refs.content.clientWidth + 100;
|
||||
}
|
||||
}
|
||||
|
||||
function loaded(_event) {
|
||||
// typically triggered by slotted component when an image loads, affecting scrollWidth
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
this.$refs.content.addEventListener('scroll', () => this.updateScroll());
|
||||
window.addEventListener('resize', this.updateScroll);
|
||||
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
function beforeUnmount() {
|
||||
this.$refs.content.removeEventListener('scroll', this.updateScroll);
|
||||
|
||||
window.removeEventListener('resize', this.updateScroll);
|
||||
}
|
||||
|
||||
function updated() {
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Expand,
|
||||
},
|
||||
props: {
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
expandable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
album: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollable: true,
|
||||
scrollAtStart: true,
|
||||
scrollAtEnd: false,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
updated,
|
||||
beforeUnmount,
|
||||
methods: {
|
||||
scroll,
|
||||
loaded,
|
||||
updateScroll,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.scroll.expanded {
|
||||
padding: 0;
|
||||
|
||||
.scroll {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
overflow-x: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-button {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
|
||||
&.scroll-start,
|
||||
&.scroll-end {
|
||||
/* use over v-show so button stays visible while still hovered */
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.scroll-start {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.scroll-end {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: var(--lighten);
|
||||
}
|
||||
|
||||
&.scroll-start .icon,
|
||||
&.scroll-end .icon {
|
||||
fill: var(--lighten-weak);
|
||||
}
|
||||
|
||||
&:hover:not(.scroll-start):not(.scroll-end) .icon {
|
||||
fill: var(--lighten-strong);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-left {
|
||||
left: 0;
|
||||
padding: 1rem 2rem 1rem .5rem;
|
||||
}
|
||||
|
||||
.scroll-right {
|
||||
right: 0;
|
||||
padding: 1rem .5rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.scroll .expand-light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scroll-light {
|
||||
.expand-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.expand-dark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-dark .expand-light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
/* buttons block swiping motion */
|
||||
.scroll-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
assets/components/search/bar.vue
Normal file
101
assets/components/search/bar.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<form
|
||||
class="search"
|
||||
@submit.prevent="() => search()"
|
||||
>
|
||||
<input
|
||||
v-model="query"
|
||||
:placeholder="placeholder || 'Search'"
|
||||
class="query"
|
||||
@input="() => search(true)"
|
||||
>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="search-button"
|
||||
><Icon icon="search" /></button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function search(typing) {
|
||||
if (!typing || this.eager) {
|
||||
this.$router.replace({
|
||||
query: { query: this.query || undefined },
|
||||
params: { ...this.$route.params, pageNumber: 1 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
this.query = this.$route.query.query || null;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
eager: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: this.$route.query.query || null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: resetQuery,
|
||||
},
|
||||
methods: {
|
||||
search,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.query {
|
||||
max-width: 40rem;
|
||||
min-width: 10rem;
|
||||
color: var(--text);
|
||||
background: var(--background);
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 3px var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
118
assets/components/search/search.vue
Normal file
118
assets/components/search/search.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="content-inner">
|
||||
<span
|
||||
v-if="loading"
|
||||
class="summary"
|
||||
>Searching...</span>
|
||||
|
||||
<template v-if="actors.length > 0">
|
||||
<span class="summary">Found {{ actors.length }} actors for '{{ query }}'</span>
|
||||
|
||||
<div class="tiles">
|
||||
<Actor
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
:actor="actor.aliasFor || actor"
|
||||
:alias="actor.aliasFor && actor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="releases.length > 0">
|
||||
<span class="summary">Found {{ releases.length }} releases for '{{ query }}'</span>
|
||||
|
||||
<Releases
|
||||
class="embedded"
|
||||
:releases="releases"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span
|
||||
v-if="!loading && actors.length === 0 && releases.length === 0"
|
||||
class="summary"
|
||||
>No results</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Actor from '../actors/tile.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
|
||||
async function search() {
|
||||
const results = await this.$store.dispatch('search', {
|
||||
query: this.query,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
|
||||
if (results) {
|
||||
this.actors = results.actors;
|
||||
this.releases = results.releases;
|
||||
}
|
||||
}
|
||||
|
||||
function query() {
|
||||
return this.$route.query.query || this.$route.query.q;
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.search();
|
||||
}
|
||||
|
||||
async function watchQuery() {
|
||||
await this.search();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actor,
|
||||
Releases,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
actors: [],
|
||||
releases: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
query,
|
||||
},
|
||||
watch: {
|
||||
query: watchQuery,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
search,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.content-inner {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: block;
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
grid-gap: 0 .5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint0) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
415
assets/components/sidebar/sidebar.vue
Normal file
415
assets/components/sidebar/sidebar.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-container"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<div
|
||||
class="sidebar"
|
||||
@click.stop
|
||||
>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-header">
|
||||
<router-link
|
||||
to="/updates"
|
||||
class="logo-link"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<h1 class="sidebar-logo">
|
||||
<div
|
||||
class="logo"
|
||||
v-html="logo"
|
||||
/>
|
||||
</h1>
|
||||
</router-link>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="sidebar-close noselect"
|
||||
@click.native="$emit('toggleSidebar', false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Search
|
||||
class="search"
|
||||
@search="$emit('toggleSidebar', false)"
|
||||
/>
|
||||
|
||||
<nav class="nav">
|
||||
<ul class="nolist">
|
||||
<li
|
||||
class="nav-item"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
to="/updates"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Home</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="nav-item"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
to="/actors"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Actors</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="nav-item"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
to="/channels"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Channels</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="nav-item"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
to="/movies"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Movies</a>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="nav-item"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
v-slot="{ href, isActive, navigate }"
|
||||
to="/tags"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
class="nav-link"
|
||||
:href="href"
|
||||
:class="{ active: isActive }"
|
||||
@click="navigate"
|
||||
>Tags</a>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section controls noselect">
|
||||
<label
|
||||
v-if="login && me"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'user', params: { username: me.username } }"
|
||||
class="toggle username nolink"
|
||||
>{{ me.username }}</router-link>
|
||||
</label>
|
||||
|
||||
<div class="toggles noselect">
|
||||
<label
|
||||
v-if="login && !me"
|
||||
@click="$emit('toggleSidebar', false)"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'login', query: { ref: $route.path } }"
|
||||
class="toggle nolink"
|
||||
><Icon icon="enter2" />Log in</router-link>
|
||||
</label>
|
||||
|
||||
<label
|
||||
v-if="login && me"
|
||||
class="toggle"
|
||||
@click.stop="$store.dispatch('logout')"
|
||||
><Icon icon="exit2" />Log out</label>
|
||||
|
||||
<label
|
||||
v-show="sfw"
|
||||
class="toggle"
|
||||
@click="setSfw(false)"
|
||||
><Icon icon="fire" />Disable safe mode</label>
|
||||
|
||||
<label
|
||||
v-show="!sfw"
|
||||
class="toggle"
|
||||
@click="setSfw(true)"
|
||||
><Icon icon="flower" />Enable safe mode</label>
|
||||
|
||||
<label
|
||||
v-show="theme === 'dark'"
|
||||
class="toggle"
|
||||
@click="setTheme('light')"
|
||||
><Icon icon="sun" />Use light theme</label>
|
||||
|
||||
<label
|
||||
v-show="theme === 'light'"
|
||||
class="toggle"
|
||||
@click="setTheme('dark')"
|
||||
><Icon icon="moon" />Use dark theme</label>
|
||||
|
||||
<label
|
||||
class="toggle"
|
||||
@click="$emit('showFilters', true)"
|
||||
><Icon icon="filter" />Filters</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import Search from '../header/search.vue';
|
||||
|
||||
import logo from '../../img/logo.svg';
|
||||
|
||||
function sfw(state) {
|
||||
return state.ui.sfw;
|
||||
}
|
||||
|
||||
function theme(state) {
|
||||
return state.ui.theme;
|
||||
}
|
||||
|
||||
function login(state) {
|
||||
return state.auth.login;
|
||||
}
|
||||
|
||||
function signup(state) {
|
||||
return state.auth.signup;
|
||||
}
|
||||
|
||||
function me(state) {
|
||||
return state.auth.user;
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
this.$store.dispatch('setTheme', newTheme);
|
||||
}
|
||||
|
||||
function setSfw(enabled) {
|
||||
this.$store.dispatch('setSfw', enabled);
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Search,
|
||||
},
|
||||
emits: ['toggleSidebar', 'showFilters'],
|
||||
data() {
|
||||
return {
|
||||
logo,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
login,
|
||||
signup,
|
||||
me,
|
||||
sfw,
|
||||
theme,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
setTheme,
|
||||
setSfw,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: var(--darken-weak);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 15rem;
|
||||
height: 100%;
|
||||
margin: 0 0 0 auto;
|
||||
color: var(--text);
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 3rem;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
width: 1.25rem;
|
||||
height: 100%;
|
||||
padding: 0 1.125rem;
|
||||
fill: var(--shadow-modest);
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
:deep(.search) {
|
||||
height: 3rem;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
padding: 0;
|
||||
margin: 0 0 .5rem 0;
|
||||
|
||||
.search-input {
|
||||
padding: .5rem 0 .5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex-grow: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--shadow);
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
color: var(--shadow-strong);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin: .5rem 0 0 0;
|
||||
}
|
||||
|
||||
.toggles {
|
||||
flex-shrink: 0;
|
||||
border-top: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
color: var(--shadow);
|
||||
font-weight: bold;
|
||||
|
||||
&.username {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
&.active .icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--shadow-strong);
|
||||
|
||||
&:not(.active) .icon {
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark .sidebar {
|
||||
background: var(--profile);
|
||||
|
||||
.nav-link {
|
||||
color: var(--shadow);
|
||||
|
||||
&.active {
|
||||
color: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
fill: var(--lighten);
|
||||
|
||||
&:hover {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="site"
|
||||
class="content site"
|
||||
>
|
||||
<FilterBar :fetch-releases="fetchSite" />
|
||||
|
||||
<div class="header">
|
||||
<a
|
||||
v-tooltip.bottom="site.url && `Go to ${site.url}`"
|
||||
:href="site.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-site"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${site.network.slug}/${site.slug}.png`"
|
||||
:title="site.name"
|
||||
:alt="site.name"
|
||||
class="logo logo-site"
|
||||
>
|
||||
</a>
|
||||
|
||||
<ul class="tags nolist">
|
||||
<li
|
||||
v-for="tag in site.tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
class="tag"
|
||||
>{{ tag.name }}</li>
|
||||
</ul>
|
||||
|
||||
<a
|
||||
v-tooltip.bottom="`Go to ${site.network.name} overview`"
|
||||
:href="`/network/${site.network.slug}`"
|
||||
class="link link-network"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${site.network.slug}/network.png`"
|
||||
:title="site.network.name"
|
||||
:alt="site.network.name"
|
||||
class="logo logo-network"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content-inner">
|
||||
<Releases :releases="releases" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterBar from '../header/filter-bar.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
|
||||
async function fetchSite() {
|
||||
this.site = await this.$store.dispatch('fetchSites', { siteSlug: this.$route.params.siteSlug });
|
||||
this.releases = this.site.releases;
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchSite();
|
||||
|
||||
this.pageTitle = this.site.name;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
Releases,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
site: null,
|
||||
releases: null,
|
||||
pageTitle: null,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchSite,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.header {
|
||||
background: $profile;
|
||||
color: $text-contrast;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.link-site {
|
||||
margin: 0 1rem 0 0;
|
||||
justify-content: flex-start;
|
||||
|
||||
.logo {
|
||||
object-position: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.link-network {
|
||||
justify-content: flex-end;
|
||||
|
||||
.logo {
|
||||
object-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
max-width: 15rem;
|
||||
max-height: 5rem;
|
||||
object-fit: contain;
|
||||
filter: $logo-highlight;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: $shadow;
|
||||
padding: .5rem;
|
||||
margin: 0 .5rem .5rem 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.link {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-height: 2.5rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div class="sites">
|
||||
<ul class="nolist tiles">
|
||||
<li
|
||||
v-for="site in sites"
|
||||
:key="`site-${site.id}`"
|
||||
class="site"
|
||||
>
|
||||
<SiteTile :site="site" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SiteTile from '../tile/site.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SiteTile,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sites: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.sites {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
&.compact:not(.expanded) {
|
||||
flex-direction: row;
|
||||
|
||||
.tiles {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tile {
|
||||
width: 15rem;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(15rem, .5fr));
|
||||
}
|
||||
|
||||
&.compact .tiles {
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-gap: 0 1rem;
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: $highlight-weak $profile;
|
||||
}
|
||||
|
||||
.site {
|
||||
/* vertical grid-gap not compatible with bottom padding on scrolling containers */
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint3) {
|
||||
.sites.expanded .tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, .5fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint0) {
|
||||
.sites.expanded .tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
assets/components/stashes/add.vue
Normal file
53
assets/components/stashes/add.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="Add stash"
|
||||
@close="$emit('close', false)"
|
||||
>
|
||||
<form
|
||||
class="dialog-body"
|
||||
@submit.prevent="addStash"
|
||||
>
|
||||
<input
|
||||
ref="name"
|
||||
v-model="name"
|
||||
type="input"
|
||||
placeholder="Name"
|
||||
class="input"
|
||||
>
|
||||
|
||||
<div class="dialog-actions right">
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function addStash() {
|
||||
await this.$store.dispatch('createStash', {
|
||||
name: this.name,
|
||||
});
|
||||
|
||||
this.$emit('close', true);
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
this.$refs.name.focus();
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
};
|
||||
},
|
||||
emits: ['close'],
|
||||
mounted,
|
||||
methods: {
|
||||
addStash,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
112
assets/components/stashes/button.vue
Normal file
112
assets/components/stashes/button.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<span class="stash-container">
|
||||
<Tooltip class="stash-trigger">
|
||||
<Icon
|
||||
v-show="me"
|
||||
icon="menu"
|
||||
class="stash noselect"
|
||||
:class="{ stashed }"
|
||||
/>
|
||||
|
||||
<template v-slot:tooltip>
|
||||
<StashMenu
|
||||
:stashed-by="stashedBy"
|
||||
@stash="(stashId) => $emit('stash', stashId)"
|
||||
@unstash="(stashId) => $emit('unstash', stashId)"
|
||||
/>
|
||||
</template>
|
||||
</Tooltip>
|
||||
|
||||
<Icon
|
||||
v-show="me && favorited"
|
||||
icon="heart7"
|
||||
class="stash stashed noselect"
|
||||
@click.native="() => $emit('unstash', favorites.id)"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="me && !favorited"
|
||||
icon="heart8"
|
||||
class="stash unstashed noselect"
|
||||
@click.native="() => $emit('stash', favorites.id)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StashMenu from './menu.vue';
|
||||
|
||||
function favorited() {
|
||||
return this.stashedBy.some(stash => stash.primary);
|
||||
}
|
||||
|
||||
function stashed() {
|
||||
return this.stashedBy.some(stash => !stash.primary);
|
||||
}
|
||||
|
||||
function favorites() {
|
||||
return this.$store.getters.favorites;
|
||||
}
|
||||
|
||||
function me() {
|
||||
return this.$store.state.auth.user;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StashMenu,
|
||||
},
|
||||
props: {
|
||||
stashedBy: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['stash', 'unstash'],
|
||||
computed: {
|
||||
me,
|
||||
favorites,
|
||||
favorited,
|
||||
stashed,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.stash-container {
|
||||
flex-shrink: 0;
|
||||
|
||||
&.light .icon {
|
||||
fill: var(--lighten);
|
||||
}
|
||||
}
|
||||
|
||||
.stash.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0 .5rem;
|
||||
fill: var(--shadow);
|
||||
|
||||
&.stashed {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.stash-trigger {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.stash.icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
assets/components/stashes/menu.vue
Normal file
71
assets/components/stashes/menu.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<ul class="menu nolist">
|
||||
<li
|
||||
v-for="stash in stashes"
|
||||
:key="`stash-${stash.id}`"
|
||||
class="menu-item"
|
||||
>
|
||||
<label class="menu-stash noselect">
|
||||
<Checkbox
|
||||
:checked="stashedByIds.has(stash.id)"
|
||||
class="menu-check"
|
||||
@change="(checked) => checked ? $emit('stash', stash.id) : $emit('unstash', stash.id)"
|
||||
/>{{ stash.name }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
|
||||
function stashes() {
|
||||
return this.$store.state.auth.user?.stashes || [];
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
stashedBy: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['stash', 'unstash'],
|
||||
data() {
|
||||
const stashedByIds = new Set(this.stashedBy.map(stash => stash.id));
|
||||
|
||||
return {
|
||||
stashedByIds,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stashes,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-item {
|
||||
color: var(--text);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-stash {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 1rem .5rem .5rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-check {
|
||||
display: inline-block;
|
||||
margin: 0 .75rem 0 0;
|
||||
}
|
||||
</style>
|
||||
41
assets/components/stashes/remove.vue
Normal file
41
assets/components/stashes/remove.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<Dialog
|
||||
title="Remove stash"
|
||||
@close="$emit('close', false)"
|
||||
>
|
||||
<form
|
||||
class="dialog-body"
|
||||
@submit.prevent="removeStash"
|
||||
>
|
||||
Are you sure you want to remove stash "{{ stash.name }}"?
|
||||
|
||||
<div class="dialog-actions right">
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
>Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function removeStash() {
|
||||
await this.$store.dispatch('removeStash', this.stash.id);
|
||||
|
||||
this.$emit('close', true);
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
removeStash,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
300
assets/components/stashes/stash.vue
Normal file
300
assets/components/stashes/stash.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="stash"
|
||||
class="stash content"
|
||||
>
|
||||
<div class="stash-header">
|
||||
<h2
|
||||
:title="stash.name"
|
||||
class="stash-name"
|
||||
>{{ stash.name }}</h2>
|
||||
|
||||
<span class="header-section">
|
||||
<RouterLink
|
||||
v-if="stash.user"
|
||||
:to="{ name: 'user', params: { username: stash.user.username } }"
|
||||
class="header-item stash-username nolink"
|
||||
><Icon icon="user3" /><span class="username-name">{{ stash.user.username }}</span></RouterLink>
|
||||
|
||||
<label
|
||||
v-if="isMine"
|
||||
v-tooltip="'Public'"
|
||||
:class="{ public: stash.public }"
|
||||
class="header-item stash-public"
|
||||
>
|
||||
<Icon
|
||||
v-show="stash.public"
|
||||
icon="eye"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="!stash.public"
|
||||
icon="eye-blocked"
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
:checked="stash.public"
|
||||
class="light"
|
||||
@change="checked => publishStash(checked)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Icon
|
||||
v-if="isMine && !stash.primary"
|
||||
icon="bin"
|
||||
class="stash-remove"
|
||||
@click.native="showRemoveStash = true"
|
||||
/>
|
||||
|
||||
<RemoveStash
|
||||
v-if="showRemoveStash"
|
||||
:stash="stash"
|
||||
@close="removeStash"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="content-inner">
|
||||
<FilterBar :ranges="['scenes', 'actors', 'movies']" />
|
||||
|
||||
<Releases
|
||||
v-if="$route.params.range === 'scenes' && stash.scenes?.length > 0"
|
||||
:releases="stash.scenes.map(item => item.scene)"
|
||||
:stash="stash"
|
||||
class="stash-section stash-scenes"
|
||||
@stash="fetchStash"
|
||||
/>
|
||||
|
||||
<ul
|
||||
v-if="$route.params.range === 'actors'"
|
||||
class="stash-section stash-actors nolist"
|
||||
>
|
||||
<li
|
||||
v-for="item in stash.actors"
|
||||
:key="item.id"
|
||||
><Actor
|
||||
:actor="item.actor"
|
||||
:stash="stash"
|
||||
@stash="fetchStash"
|
||||
/></li>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
v-if="$route.params.range === 'movies'"
|
||||
class="stash-movies"
|
||||
>
|
||||
<Movie
|
||||
v-for="item in stash.movies"
|
||||
:key="`movie-${item.id}`"
|
||||
:movie="item.movie"
|
||||
:stash="stash"
|
||||
@stash="fetchStash"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-bottom"
|
||||
/>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Actor from '../actors/tile.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
import Movie from '../releases/movie-tile.vue';
|
||||
import RemoveStash from './remove.vue';
|
||||
import Toggle from '../form/toggle.vue';
|
||||
import FilterBar from '../filters/filter-bar.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
async function fetchStash() {
|
||||
this.stash = await this.$store.dispatch('fetchStash', {
|
||||
stashId: this.$route.params.stashId,
|
||||
section: this.$route.params.range,
|
||||
pageNumber: this.$route.params.pageNumber || 1,
|
||||
limit: this.limit,
|
||||
});
|
||||
|
||||
this.isMine = this.stash.user?.id === this.$store.state.auth.user?.id;
|
||||
|
||||
if (this.$route.params.range === 'scenes') {
|
||||
this.totalCount = this.stash.sceneTotal;
|
||||
}
|
||||
|
||||
if (this.$route.params.range === 'actors') {
|
||||
this.totalCount = this.stash.actorTotal;
|
||||
}
|
||||
|
||||
if (this.$route.params.range === 'movies') {
|
||||
this.totalCount = this.stash.movieTotal;
|
||||
}
|
||||
|
||||
this.pageTitle = this.stash.name;
|
||||
}
|
||||
|
||||
async function publishStash(isPublic) {
|
||||
await this.$store.dispatch('updateStash', {
|
||||
stashId: this.stash.id,
|
||||
stash: { public: isPublic },
|
||||
});
|
||||
|
||||
this.fetchStash();
|
||||
}
|
||||
|
||||
async function removeStash(removed) {
|
||||
this.showRemoveStash = false;
|
||||
|
||||
if (removed && this.stash.user) {
|
||||
this.$router.replace({ name: 'user', params: { username: this.stash.user.username } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
this.$router.replace({ name: 'home' });
|
||||
}
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchStash();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Actor,
|
||||
Movie,
|
||||
Releases,
|
||||
RemoveStash,
|
||||
Pagination,
|
||||
FilterBar,
|
||||
Toggle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stash: null,
|
||||
limit: Number(this.$route.query.limit) || 20,
|
||||
pageTitle: null,
|
||||
showRemoveStash: false,
|
||||
isMine: false,
|
||||
totalCount: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route: fetchStash,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchStash,
|
||||
publishStash,
|
||||
removeStash,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.stash-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--profile);
|
||||
color: var(--text-light);
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
margin: -.1rem .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-section,
|
||||
.header-item {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stash-name,
|
||||
.stash-username {
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stash-username {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
|
||||
.stash-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stash-public {
|
||||
cursor: pointer;
|
||||
margin: 0 .5rem 0 0;
|
||||
|
||||
.icon {
|
||||
margin: 0 .75rem 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.stash-remove.icon {
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
fill: var(--lighten-strong);
|
||||
|
||||
&:hover {
|
||||
fill: var(--text-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.stash-actors {
|
||||
display: grid;
|
||||
grid-gap: .5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-template-rows: min-content;
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
border-top: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.stash-movies {
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
|
||||
grid-template-rows: min-content;
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-top: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.stash-scenes .tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||
grid-template-rows: min-content;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
.stash-name {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.username-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stash-username {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
114
assets/components/stats/stats.vue
Normal file
114
assets/components/stats/stats.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="stats">
|
||||
<div class="content-inner">
|
||||
<h1 class="heading">Stats</h1>
|
||||
|
||||
<dl class="stat-table">
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Version</dt>
|
||||
<dd class="stat-value">{{ version }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Content updated</dt>
|
||||
<dd class="stat-value">{{ formatDate(lastScrape, 'YYYY-MM-DD HH:mm') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<dl class="stat-table">
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Networks</dt>
|
||||
<dd class="stat-value">{{ totalNetworks }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Channels</dt>
|
||||
<dd class="stat-value">{{ totalChannels }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Scenes</dt>
|
||||
<dd class="stat-value">{{ totalScenes }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Movies</dt>
|
||||
<dd class="stat-value">{{ totalMovies }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<dt class="stat-label">Actors</dt>
|
||||
<dd class="stat-value">{{ totalActors }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
async function mounted() {
|
||||
const stats = await this.$store.dispatch('fetchStats');
|
||||
|
||||
this.totalScenes = stats.totalScenes;
|
||||
this.totalMovies = stats.totalMovies;
|
||||
this.totalActors = stats.totalActors;
|
||||
this.totalNetworks = stats.totalNetworks;
|
||||
this.totalChannels = stats.totalChannels;
|
||||
this.lastScrape = stats.lastScrape;
|
||||
|
||||
this.version = VERSION; // eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
totalScenes: 0,
|
||||
totalMovies: 0,
|
||||
totalActors: 0,
|
||||
totalNetworks: 0,
|
||||
totalChannels: 0,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-table {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
padding: .5rem 0;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +1,168 @@
|
||||
<template>
|
||||
<div class="photos">
|
||||
<ul class="nolist photos-inner">
|
||||
<li>
|
||||
<a
|
||||
v-if="tag.poster"
|
||||
:href="`/img/${tag.poster.path}`"
|
||||
:title="tag.poster.comment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="`/img/${tag.poster.thumbnail}`"
|
||||
:alt="tag.poster.comment"
|
||||
class="poster"
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
<div class="photos">
|
||||
<Campaign
|
||||
:tag="tag"
|
||||
:min-height="240"
|
||||
:max-ratio="1.5"
|
||||
class="photo-link photo"
|
||||
@campaign="campaign => $emit('campaign', campaign)"
|
||||
/>
|
||||
|
||||
<li
|
||||
v-for="photo in tag.photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
>
|
||||
<a
|
||||
:title="photo.comment"
|
||||
:href="`/img/${photo.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
:src="`/img/${photo.thumbnail}`"
|
||||
:alt="photo.comment"
|
||||
class="photo"
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a
|
||||
v-for="photo in photos"
|
||||
:key="`photo-${photo.id}`"
|
||||
:title="photo.comment"
|
||||
:href="`/img/${photo.path}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="photo-link"
|
||||
>
|
||||
<img
|
||||
:src="getPath(photo, 'thumbnail', { local: true })"
|
||||
:style="{ 'background-image': getBgPath(photo, 'lazy', { local: true }) }"
|
||||
:alt="photo.comment"
|
||||
:width="photo.thumbnailWidth"
|
||||
:height="photo.thumbnailHeight"
|
||||
class="photo"
|
||||
@load="$emit('load', $event)"
|
||||
>
|
||||
|
||||
<Logo :photo="photo" />
|
||||
|
||||
<router-link
|
||||
v-if="photo.comment && photo.entity"
|
||||
:to="`/${photo.entity.type}/${photo.entity.slug}`"
|
||||
class="photo-comment"
|
||||
>{{ photo.comment }} for {{ photo.entity.name }}</router-link>
|
||||
|
||||
<span
|
||||
v-else-if="photo.comment"
|
||||
class="photo-comment"
|
||||
>{{ photo.comment }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logo from '../album/logo.vue';
|
||||
import Campaign from '../campaigns/campaign.vue';
|
||||
|
||||
function photos() {
|
||||
if (this.tag.poster && this.$store.state.ui.sfw) {
|
||||
return [this.tag.poster].concat(this.tag.photos).map(photo => photo.sfw);
|
||||
}
|
||||
|
||||
if (this.$store.state.ui.sfw) {
|
||||
return this.tag.photos.map(photo => photo.sfw);
|
||||
}
|
||||
|
||||
if (this.tag.poster) {
|
||||
return [this.tag.poster].concat(this.tag.photos);
|
||||
}
|
||||
|
||||
return this.tag.photos;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Logo,
|
||||
Campaign,
|
||||
},
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['load', 'campaign'],
|
||||
computed: {
|
||||
photos,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.photos {
|
||||
background: $profile;
|
||||
display: flex;
|
||||
padding: 0 1rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: .5rem 1rem;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
font-size: 0;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
|
||||
&.compact {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
overflow-x: auto;
|
||||
|
||||
.photos-inner {
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.poster,
|
||||
.photo {
|
||||
width: 100%;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0 0 1rem;
|
||||
|
||||
.photo-link {
|
||||
margin: 0 .5rem .5rem 0;
|
||||
}
|
||||
|
||||
.poster,
|
||||
.photo {
|
||||
max-height: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 .5rem 0 0;
|
||||
|
||||
&:last-child {
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.photo-comment {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
::v-deep(.album-logo) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poster,
|
||||
.photo {
|
||||
width: 100%;
|
||||
margin: 0 0 .5rem 0;
|
||||
width: auto;
|
||||
max-height: 15rem;
|
||||
max-width: 100%;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
object-fit: cover;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.photo-comment {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
color: var(--text-light);
|
||||
background: var(--shadow);
|
||||
font-size: .9rem;
|
||||
text-shadow: 0 0 3px var(--shadow);
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
line-height: 1.25;
|
||||
transform: translateY(100%);
|
||||
transition: transform .25s ease;
|
||||
}
|
||||
|
||||
::v-deep(.campaign) .campaign-banner {
|
||||
max-height: 15rem;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,178 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="tag"
|
||||
class="content"
|
||||
>
|
||||
<FilterBar :fetch-releases="fetchReleases" />
|
||||
<div
|
||||
v-if="tag"
|
||||
class="tag content"
|
||||
>
|
||||
<div class="header">
|
||||
<h2 class="title">
|
||||
<Icon icon="price-tag4" />
|
||||
{{ tag.name }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tag"
|
||||
:class="{ nomedia: !hasMedia }"
|
||||
>
|
||||
<div class="header">
|
||||
<h2 class="title">
|
||||
<Icon icon="price-tag4" />
|
||||
{{ tag.name }}
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="content-inner"
|
||||
@scroll="events.emit('scroll', $event)"
|
||||
>
|
||||
<div
|
||||
v-if="description"
|
||||
class="description header-description"
|
||||
v-html="description"
|
||||
/>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2 class="title">
|
||||
<Icon icon="price-tag4" />
|
||||
{{ tag.name }}
|
||||
</h2>
|
||||
<Scroll
|
||||
v-if="hasMedia"
|
||||
v-slot="scroll"
|
||||
class="scroll-dark"
|
||||
>
|
||||
<Photos
|
||||
:tag="tag"
|
||||
:class="{ expanded }"
|
||||
@load="scroll.loaded"
|
||||
@campaign="campaign => showBannerCampaign = !campaign"
|
||||
/>
|
||||
</Scroll>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<p
|
||||
v-if="description"
|
||||
class="description"
|
||||
v-html="description"
|
||||
/>
|
||||
<button
|
||||
v-if="tag.photos && tag.photos.length > 2"
|
||||
class="album-toggle"
|
||||
@click="$router.push({ hash: '#album' })"
|
||||
><Icon icon="grid3" />View album</button>
|
||||
|
||||
<Photos
|
||||
v-if="hasMedia"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Album
|
||||
v-if="showAlbum"
|
||||
:items="[tag.poster, ...tag.photos]"
|
||||
:title="tag.name"
|
||||
:local="true"
|
||||
:tag="tag"
|
||||
class="portrait"
|
||||
@close="$router.replace({ hash: undefined })"
|
||||
/>
|
||||
|
||||
<div class="content-inner">
|
||||
<Photos
|
||||
v-if="hasMedia"
|
||||
:tag="tag"
|
||||
class="compact"
|
||||
/>
|
||||
<div
|
||||
v-if="showBannerCampaign"
|
||||
class="campaign-container"
|
||||
>
|
||||
<Campaign
|
||||
:tag="tag"
|
||||
:min-ratio="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Releases :releases="tag.releases" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FilterBar
|
||||
ref="filter"
|
||||
:fetch-releases="fetchReleases"
|
||||
/>
|
||||
|
||||
<Releases
|
||||
:releases="releases"
|
||||
:done="done"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
:items-total="totalCount"
|
||||
:items-per-page="limit"
|
||||
class="pagination-bottom"
|
||||
/>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable no-v-html */
|
||||
import { Converter } from 'showdown';
|
||||
|
||||
import escapeHtml from '../../../src/utils/escape-html';
|
||||
|
||||
import FilterBar from '../header/filter-bar.vue';
|
||||
import FilterBar from '../filters/filter-bar.vue';
|
||||
import Photos from './photos.vue';
|
||||
import Album from '../album/album.vue';
|
||||
import Releases from '../releases/releases.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
import Scroll from '../scroll/scroll.vue';
|
||||
import Campaign from '../campaigns/campaign.vue';
|
||||
|
||||
const converter = new Converter();
|
||||
|
||||
async function fetchReleases() {
|
||||
this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug });
|
||||
async function fetchReleases(scroll = true) {
|
||||
this.done = false;
|
||||
|
||||
this.hasMedia = this.tag.poster || this.tag.photos.length > 0;
|
||||
this.description = this.tag.description && converter.makeHtml(escapeHtml(this.tag.description));
|
||||
const { tag, releases, totalCount } = await this.$store.dispatch('fetchTagBySlug', {
|
||||
tagSlug: this.$route.params.tagSlug,
|
||||
pageNumber: Number(this.$route.params.pageNumber),
|
||||
limit: this.limit,
|
||||
range: this.$route.params.range,
|
||||
});
|
||||
|
||||
this.tag = tag;
|
||||
this.releases = releases;
|
||||
this.totalCount = totalCount;
|
||||
|
||||
this.hasMedia = this.tag.poster || this.tag.photos.length > 0;
|
||||
this.description = this.tag.description && converter.makeHtml(escapeHtml(this.tag.description));
|
||||
|
||||
this.done = true;
|
||||
|
||||
if (this.hasMedia) {
|
||||
this.showBannerCampaign = true;
|
||||
}
|
||||
|
||||
if (scroll && this.$refs.filter) {
|
||||
this.$refs.filter.$el.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function showAlbum() {
|
||||
return this.tag.photos?.length > 0 && this.$route.hash === '#album';
|
||||
}
|
||||
|
||||
async function watchRoute(to, from) {
|
||||
if (to.hash !== '#album' && from.hash !== '#album') {
|
||||
await this.fetchReleases();
|
||||
}
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchReleases();
|
||||
this.pageTitle = this.tag.name;
|
||||
await this.fetchReleases();
|
||||
this.pageTitle = this.tag.name;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
Photos,
|
||||
Releases,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tag: null,
|
||||
description: null,
|
||||
releases: null,
|
||||
pageTitle: null,
|
||||
hasMedia: false,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchReleases,
|
||||
},
|
||||
components: {
|
||||
FilterBar,
|
||||
Releases,
|
||||
Album,
|
||||
Photos,
|
||||
Pagination,
|
||||
Scroll,
|
||||
Campaign,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tag: null,
|
||||
description: null,
|
||||
releases: null,
|
||||
done: false,
|
||||
totalCount: 0,
|
||||
limit: 20,
|
||||
pageTitle: null,
|
||||
hasMedia: false,
|
||||
expanded: false,
|
||||
showBannerCampaign: false, // only show if photo campaign is not available
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showAlbum,
|
||||
},
|
||||
watch: {
|
||||
$route: watchRoute,
|
||||
'$store.state.ui.tagFilter': fetchReleases,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchReleases,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -99,119 +180,60 @@ export default {
|
||||
@import 'theme';
|
||||
|
||||
.description a {
|
||||
color: $link;
|
||||
color: var(--link);
|
||||
text-decoration: inherit;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.description,
|
||||
.description p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.nomedia {
|
||||
flex-direction: column;
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $profile;
|
||||
color: $text-contrast;
|
||||
display: none;
|
||||
padding: .5rem 1rem;
|
||||
|
||||
.title {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: $profile;
|
||||
color: $text-contrast;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0 1rem;
|
||||
margin: -1rem 0 0 0;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
background: var(--profile);
|
||||
color: var(--text-light);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
text-transform: capitalize;
|
||||
|
||||
.icon {
|
||||
fill: $text-contrast;
|
||||
fill: var(--text-light);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding: 0 1rem .5rem 1rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-light);
|
||||
background: var(--profile);
|
||||
}
|
||||
|
||||
.releases {
|
||||
padding: 1rem;
|
||||
.scroll {
|
||||
background: var(--background-dim);
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint3) {
|
||||
.tag {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.photos.compact {
|
||||
display: flex;
|
||||
}
|
||||
.campaign-container {
|
||||
max-height: 90px;
|
||||
padding: .5rem 1rem 0 1rem;
|
||||
background: var(--background-dim);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,147 +1,338 @@
|
||||
<template>
|
||||
<div class="tags">
|
||||
<h3>Oral</h3>
|
||||
<div class="tags">
|
||||
<div class="content-inner">
|
||||
<SearchBar
|
||||
:placeholder="'Search tags'"
|
||||
class="search"
|
||||
/>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.oral"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(tags, category) in categories"
|
||||
:key="category"
|
||||
class="category"
|
||||
>
|
||||
<h3 class="heading">{{ category }}</h3>
|
||||
|
||||
<h3>Penetration</h3>
|
||||
<div
|
||||
:key="sfw"
|
||||
class="tiles"
|
||||
>
|
||||
<Tag
|
||||
v-for="tag in tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
:lazy="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.penetration"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Group</h3>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.group"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Ethnicity</h3>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.ethnicity"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Finish</h3>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.finish"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Misc</h3>
|
||||
|
||||
<div class="tiles">
|
||||
<Tag
|
||||
v-for="tag in tags.misc.concat(tags.body)"
|
||||
:key="`tag-${tag.id}`"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tag from '../tile/tag.vue';
|
||||
import Tag from './tile.vue';
|
||||
import SearchBar from '../search/bar.vue';
|
||||
|
||||
const tagSlugsByCategory = {
|
||||
popular: [
|
||||
'anal',
|
||||
'lesbian',
|
||||
'interracial',
|
||||
'mff',
|
||||
'mfm',
|
||||
'teen',
|
||||
'milf',
|
||||
'blowjob',
|
||||
'gay',
|
||||
'transsexual',
|
||||
'dp',
|
||||
'gangbang',
|
||||
'facial',
|
||||
'creampie',
|
||||
'squirting',
|
||||
],
|
||||
appearance: [
|
||||
'asian',
|
||||
'black',
|
||||
'latina',
|
||||
'white',
|
||||
'natural-boobs',
|
||||
'enhanced-boobs',
|
||||
'blonde',
|
||||
'brunette',
|
||||
'redhead',
|
||||
'tattoos',
|
||||
'piercings',
|
||||
],
|
||||
sexuality: [
|
||||
'gay',
|
||||
'bisexual',
|
||||
'transsexual',
|
||||
],
|
||||
oral: [
|
||||
'blowjob',
|
||||
'pussy-eating',
|
||||
'ass-eating',
|
||||
'deepthroat',
|
||||
'facefucking',
|
||||
'69',
|
||||
'atm',
|
||||
],
|
||||
manual: [
|
||||
'handjob',
|
||||
'fingering',
|
||||
'anal-fingering',
|
||||
'titty-fucking',
|
||||
'fisting',
|
||||
'anal-fisting',
|
||||
],
|
||||
group: [
|
||||
'mfm',
|
||||
'mff',
|
||||
'orgy',
|
||||
'gangbang',
|
||||
'blowbang',
|
||||
],
|
||||
cumshot: [
|
||||
'facial',
|
||||
'creampie',
|
||||
'cum-in-mouth',
|
||||
'cum-on-boobs',
|
||||
'cum-on-butt',
|
||||
'cum-on-pussy',
|
||||
'anal-creampie',
|
||||
'oral-creampie',
|
||||
'bukkake',
|
||||
'fake-cum',
|
||||
],
|
||||
roleplay: [
|
||||
'family',
|
||||
'parody',
|
||||
'schoolgirl',
|
||||
'nurse',
|
||||
'maid',
|
||||
'nun',
|
||||
],
|
||||
extreme: [
|
||||
'dp',
|
||||
'airtight',
|
||||
'dap',
|
||||
'dvp',
|
||||
'triple-penetration',
|
||||
'tap',
|
||||
'tvp',
|
||||
],
|
||||
fetish: [
|
||||
'bdsm',
|
||||
'femdom',
|
||||
'bondage',
|
||||
'free-use',
|
||||
'latex',
|
||||
'blindfold',
|
||||
],
|
||||
toys: [
|
||||
'toys',
|
||||
'toy-anal',
|
||||
'toy-dp',
|
||||
'double-dildo',
|
||||
'double-dildo-blowjob',
|
||||
'double-dildo-kiss',
|
||||
'double-dildo-anal',
|
||||
'double-dildo-dp',
|
||||
],
|
||||
misc: [
|
||||
'gaping',
|
||||
'squirting',
|
||||
'oil',
|
||||
'vr',
|
||||
'bts',
|
||||
],
|
||||
};
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
async function fetchTags() {
|
||||
if (this.$route.query.query) {
|
||||
await this.searchTags();
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = await this.$store.dispatch('fetchTags', {
|
||||
slugs: Object.values(tagSlugsByCategory).flat(),
|
||||
});
|
||||
|
||||
const tagsBySlug = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag }), {});
|
||||
|
||||
this.categories = Object.entries(tagSlugsByCategory).reduce((acc, [category, tagSlugs]) => {
|
||||
const categoryTags = tagSlugs.map((tagSlug) => tagsBySlug[tagSlug]).filter(Boolean);
|
||||
|
||||
if (categoryTags.length === 0) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[category]: categoryTags,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function searchTags() {
|
||||
const tags = await this.$store.dispatch('searchTags', {
|
||||
minLength: 1,
|
||||
query: this.$route.query.query,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
this.categories = {
|
||||
results: tags,
|
||||
};
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
const tags = await this.$store.dispatch('fetchTags', {
|
||||
slugs: [
|
||||
'airtight',
|
||||
'anal',
|
||||
'anal-creampie',
|
||||
'asian',
|
||||
'ass-eating',
|
||||
'ass-to-mouth',
|
||||
'bdsm',
|
||||
'blowbang',
|
||||
'blowjob',
|
||||
'bukkake',
|
||||
'caucasian',
|
||||
'creampie',
|
||||
'da-tp',
|
||||
'deepthroat',
|
||||
'double-anal',
|
||||
'double-blowjob',
|
||||
'double-penetration',
|
||||
'double-vaginal',
|
||||
'dv-tp',
|
||||
'ebony',
|
||||
'facefucking',
|
||||
'facial',
|
||||
'gangbang',
|
||||
'gaping',
|
||||
'interracial',
|
||||
'latina',
|
||||
'mff',
|
||||
'mfm',
|
||||
'oral-creampie',
|
||||
'orgy',
|
||||
'pussy-eating',
|
||||
'swallowing',
|
||||
'tattoo',
|
||||
'trainbang',
|
||||
'triple-anal',
|
||||
],
|
||||
});
|
||||
this.pageTitle = 'Tags';
|
||||
|
||||
this.tags = tags.reduce((acc, tag) => {
|
||||
if (!tag.group) {
|
||||
return { ...acc, misc: [...acc.misc, tag] };
|
||||
}
|
||||
|
||||
if (acc[tag.group.slug]) {
|
||||
return { ...acc, [tag.group.slug]: [...acc[tag.group.slug], tag] };
|
||||
}
|
||||
|
||||
return { ...acc, [tag.group.slug]: [tag] };
|
||||
}, { misc: [] });
|
||||
await this.fetchTags();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tag,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: {},
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
components: {
|
||||
Tag,
|
||||
SearchBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categories: {},
|
||||
pageTitle: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sfw,
|
||||
},
|
||||
watch: {
|
||||
$route: fetchTags,
|
||||
'$store.state.ui.tagFilter': fetchTags,
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
fetchTags,
|
||||
searchTags,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
@import 'breakpoints';
|
||||
|
||||
.tags {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, .33fr));
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-transform: capitalize;
|
||||
padding: 0 0 0 .5rem;
|
||||
margin: 1.25rem 0 1rem 0;
|
||||
}
|
||||
|
||||
.category:first-child .heading {
|
||||
margin: .5rem 0 1rem 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-mega) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(17.75rem, .5fr));
|
||||
}
|
||||
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(14.5rem, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 11rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-small) {
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-micro) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
}
|
||||
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 7.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-mini) {
|
||||
.tiles {
|
||||
grid-template-columns: repeat(auto-fill, minmax(11.5rem, 1fr));
|
||||
}
|
||||
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-nano) {
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-pico) {
|
||||
::v-deep(.poster),
|
||||
::v-deep(.blank) {
|
||||
height: 8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
166
assets/components/tags/tile.vue
Normal file
166
assets/components/tags/tile.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div
|
||||
class="tile"
|
||||
>
|
||||
<router-link
|
||||
:to="`/tag/${tag.slug}`"
|
||||
:title="tag.name"
|
||||
:class="{ blank: !tag.poster }"
|
||||
class="poster-link"
|
||||
>
|
||||
<template v-if="tag.poster">
|
||||
<img
|
||||
v-if="!lazy && !sfw"
|
||||
:src="`/img/${tag.poster.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/img/${tag.poster.lazy})` }"
|
||||
:title="comment"
|
||||
:alt="tag.name"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="!lazy && sfw"
|
||||
:src="`/img/${tag.poster.sfw.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/img/${tag.poster.sfw.lazy})` }"
|
||||
:title="tag.poster.sfw.comment"
|
||||
:alt="tag.name"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="lazy && !sfw"
|
||||
:src="`/img/${tag.poster.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/img/${tag.poster.lazy})` }"
|
||||
:title="comment"
|
||||
:alt="tag.name"
|
||||
loading="lazy"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<img
|
||||
v-if="lazy && sfw"
|
||||
:src="`/img/${tag.poster.sfw.thumbnail}`"
|
||||
:style="{ 'background-image': `url(/img/${tag.poster.sfw.lazy})` }"
|
||||
:title="tag.poster.sfw.comment"
|
||||
:alt="tag.name"
|
||||
loading="lazy"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<Logo
|
||||
v-if="!sfw"
|
||||
:photo="tag.poster"
|
||||
favicon
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
icon="price-tag2"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
class="title"
|
||||
:to="`/tag/${tag.slug}`"
|
||||
:title="tag.name"
|
||||
>{{ tag.name }}</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logo from '../album/logo.vue';
|
||||
|
||||
function sfw() {
|
||||
return this.$store.state.ui.sfw;
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
},
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
comment: this.tag.poster?.entity ? `${this.tag.poster.comment} for ${this.tag.poster.entity.name}` : this.tag.poster?.comment,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sfw,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
font-size: 0;
|
||||
|
||||
&:hover {
|
||||
.poster,
|
||||
.blank {
|
||||
box-shadow: 0 0 2px var(--darken);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poster {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.poster,
|
||||
.blank {
|
||||
height: 13.5rem;
|
||||
}
|
||||
|
||||
.poster-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
background: var(--shadow-hint);
|
||||
|
||||
.icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: var(--shadow-strong);
|
||||
text-decoration: none;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="actor"
|
||||
class="actor"
|
||||
>
|
||||
<a
|
||||
:href="`/actor/${actor.slug}`"
|
||||
class="link"
|
||||
>
|
||||
<span
|
||||
v-tooltip.top="actor.name"
|
||||
class="name"
|
||||
>{{ actor.name }}</span>
|
||||
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
:src="`/media/${actor.avatar.thumbnail || actor.avatar}`"
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="avatar"
|
||||
><img
|
||||
:src="`/img/avatar_${actor.gender || 'female'}.png`"
|
||||
class="avatar-fallback"
|
||||
></span>
|
||||
|
||||
<span
|
||||
class="details"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
v-if="actor.age"
|
||||
v-tooltip="`Born on ${formatDate(actor.birthdate, 'MMMM D, YYYY')}`"
|
||||
class="age"
|
||||
>{{ actor.age }}</span>
|
||||
|
||||
<span
|
||||
v-if="actor.ageThen && actor.ageThen < actor.age"
|
||||
v-tooltip="`${actor.ageThen} years old on release date`"
|
||||
class="age-then"
|
||||
>{{ actor.ageThen }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="actor.origin"
|
||||
v-tooltip="`Born in ${actor.origin.country.alias || actor.origin.country.name}`"
|
||||
class="country"
|
||||
>
|
||||
{{ actor.origin.country.alpha2 }}
|
||||
<img
|
||||
class="flag"
|
||||
:src="`/img/flags/${actor.origin.country.alpha2.toLowerCase()}.png`"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.actor {
|
||||
width: 100%;
|
||||
background: $background;
|
||||
display: inline-block;
|
||||
margin: 0 .5rem .5rem 0;
|
||||
position: relative;
|
||||
box-shadow: 0 0 3px $shadow-weak;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $link;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
padding: .5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
color: $shadow-weak;
|
||||
background: $shadow-hint;
|
||||
height: 12rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
object-position: 50% 0;
|
||||
}
|
||||
|
||||
.avatar-fallback {
|
||||
max-height: 75%;
|
||||
max-width: 80%;
|
||||
opacity: .1;
|
||||
}
|
||||
|
||||
.details {
|
||||
background: $shadow;
|
||||
color: $text-contrast;
|
||||
width: 100%;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.age-then {
|
||||
color: $highlight;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
:href="`/network/${network.slug}`"
|
||||
:title="network.name"
|
||||
class="tile"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${network.slug}/network.png`"
|
||||
:alt="network.name"
|
||||
class="logo"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
network: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tile {
|
||||
background: $profile;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 1rem;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .25);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
color: $text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
filter: $logo-highlight;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $text;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,310 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="tile"
|
||||
:class="{ movie: release.type === 'movie' }"
|
||||
>
|
||||
<span class="poster">
|
||||
<span class="details">
|
||||
<router-link
|
||||
v-if="release.site.independent"
|
||||
:to="`/network/${release.network.slug}`"
|
||||
class="site site-link"
|
||||
><img
|
||||
:src="`/img/logos/${release.network.slug}/favicon.png`"
|
||||
class="favicon"
|
||||
>{{ release.network.name }}</router-link>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
v-tooltip.bottom="`Part of ${release.network.name}`"
|
||||
:title="`Part of ${release.network.name}`"
|
||||
:to="`/site/${release.site.slug}`"
|
||||
class="site site-link"
|
||||
><img
|
||||
:src="`/img/logos/${release.network.slug}/favicon.png`"
|
||||
class="favicon"
|
||||
>{{ release.site.name }}</router-link>
|
||||
|
||||
<a
|
||||
v-if="release.date"
|
||||
v-tooltip.bottom="release.url && `View scene on ${release.site.name}`"
|
||||
:title="release.url && `View scene on ${release.site.name}`"
|
||||
:href="release.url"
|
||||
:class="{ upcoming: isAfter(release.date, new Date()) }"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="date"
|
||||
>{{ formatDate(release.date, 'MMM D, YYYY') }}</a>
|
||||
|
||||
<a
|
||||
v-else
|
||||
:href="release.url"
|
||||
:class="{ upcoming: isAfter(release.date, new Date()) }"
|
||||
title="Scene date N/A, showing date added"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="date"
|
||||
>{{ `(${formatDate(release.dateAdded, 'MMM D, YYYY')})` }}</a>
|
||||
</span>
|
||||
|
||||
<a
|
||||
:href="`/${release.type || 'scene'}/${release.id}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
>
|
||||
<img
|
||||
v-if="release.poster"
|
||||
:src="`/media/${release.poster.thumbnail}`"
|
||||
:alt="release.title"
|
||||
class="thumbnail"
|
||||
>
|
||||
|
||||
<img
|
||||
v-else-if="release.covers && release.covers.length > 0"
|
||||
:src="`/media/${release.covers[0].thumbnail}`"
|
||||
:alt="release.title"
|
||||
class="thumbnail"
|
||||
>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:title="release.title"
|
||||
class="thumbnail"
|
||||
>No thumbnail available</div>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div class="info">
|
||||
<a
|
||||
:href="`/${release.type || 'scene'}/${release.id}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="row link"
|
||||
>
|
||||
<h3
|
||||
v-tooltip.top="release.title"
|
||||
:title="release.title"
|
||||
class="title"
|
||||
>
|
||||
<Icon
|
||||
v-if="release.type === 'movie'"
|
||||
icon="film"
|
||||
/>{{ release.title }}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
<span class="row">
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in release.actors"
|
||||
:key="actor.id"
|
||||
class="actor"
|
||||
>
|
||||
<a
|
||||
:href="`/actor/${actor.slug}`"
|
||||
class="actor-link"
|
||||
>{{ actor.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
|
||||
<ul
|
||||
:title="release.tags.map(tag => tag.name).join(', ')"
|
||||
class="tags nolist"
|
||||
>
|
||||
<li
|
||||
v-for="tag in release.tags"
|
||||
:key="`tag-${tag.slug}`"
|
||||
class="tag"
|
||||
>
|
||||
<router-link
|
||||
:to="`/tag/${tag.slug}`"
|
||||
class="tag-link"
|
||||
>{{ tag.name }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tile {
|
||||
background: $background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 .5rem 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 3px $shadow-weak;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.poster {
|
||||
position: relative;
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 14rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
object-fit: cover;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: $shadow-hint;
|
||||
color: $shadow;
|
||||
text-shadow: 1px 1px 0 $highlight;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 .5rem;
|
||||
margin: 0 0 .25rem 0;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
font-size: 0;
|
||||
|
||||
.favicon {
|
||||
height: 1rem;
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.site,
|
||||
.date {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $shadow;
|
||||
position: relative;
|
||||
font-size: .8rem;
|
||||
padding: .25rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.date.upcoming:before {
|
||||
content: '';
|
||||
color: $text-contrast;
|
||||
background: $primary;
|
||||
width: .25rem;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -.75rem;
|
||||
padding: .25rem;
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.site {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 .25rem .25rem 0;
|
||||
color: $text;
|
||||
max-height: 2.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.icon {
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.network {
|
||||
color: #555;
|
||||
margin: 0 .25rem 0 0;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.actors {
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
max-height: 2.75rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
max-height: .5rem;
|
||||
padding: .25rem .5rem 1rem .5rem;
|
||||
word-wrap: break-word;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.actor {
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 0 .25rem .25rem 0;
|
||||
}
|
||||
|
||||
.actor:not(:last-of-type)::after {
|
||||
content: ",";
|
||||
}
|
||||
|
||||
.actor-link {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-link {
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
color: $shadow;
|
||||
display: inline-block;
|
||||
padding: .25rem;
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
border: solid 1px $shadow-hint;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
:href="`/site/${site.slug}`"
|
||||
:title="site.name"
|
||||
class="tile"
|
||||
>
|
||||
<img
|
||||
:src="`/img/logos/${site.network.slug}/${site.slug}.png`"
|
||||
:alt="site.name"
|
||||
class="logo"
|
||||
>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
site: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tile {
|
||||
background: $tile;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .25);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
color: $text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
filter: $logo-highlight;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $text;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<a
|
||||
:href="`/tag/${tag.slug}`"
|
||||
:title="tag.name"
|
||||
class="tile"
|
||||
>
|
||||
<img
|
||||
v-if="tag.poster"
|
||||
:src="`/img/${tag.poster.thumbnail}`"
|
||||
:alt="tag.name"
|
||||
class="poster"
|
||||
>
|
||||
|
||||
<span class="title">{{ tag.name }}</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'theme';
|
||||
|
||||
.tile {
|
||||
color: $text;
|
||||
background: $background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .25);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.poster {
|
||||
width: 100%;
|
||||
height: 14rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
padding: .5rem 1rem;
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
184
assets/components/tooltip/tooltip.vue
Normal file
184
assets/components/tooltip/tooltip.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="tooltip-container">
|
||||
<div
|
||||
ref="trigger"
|
||||
class="trigger noselect"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="opened"
|
||||
ref="tooltip"
|
||||
class="tooltip-wrapper"
|
||||
:style="{ transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)` }"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
class="tooltip-inner"
|
||||
:style="{ 'max-height': `calc(100vh - ${tooltipY}px - 1rem)` }"
|
||||
>
|
||||
<div class="tooltip">
|
||||
<slot name="tooltip" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tooltip-arrow"
|
||||
:style="{ transform: `translate3d(${arrowOffset}px, 0, 0)` }"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
function getX(triggerBoundary, tooltipBoundary) {
|
||||
const idealPosition = triggerBoundary.left + (triggerBoundary.width / 2) - (tooltipBoundary.width / 2);
|
||||
const rightEdgeOverflow = Math.max((idealPosition + tooltipBoundary.width) - window.innerWidth, 0);
|
||||
|
||||
// don't overflow left edge
|
||||
if (idealPosition < 0) {
|
||||
return {
|
||||
tooltipX: 0,
|
||||
arrowOffset: idealPosition,
|
||||
};
|
||||
}
|
||||
|
||||
// don't overflow right edge
|
||||
if (rightEdgeOverflow > 0) {
|
||||
return {
|
||||
tooltipX: window.innerWidth - tooltipBoundary.width,
|
||||
arrowOffset: rightEdgeOverflow,
|
||||
};
|
||||
}
|
||||
|
||||
// position at the center of trigger
|
||||
return {
|
||||
tooltipX: idealPosition,
|
||||
arrowOffset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function calculate() {
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerBoundary = this.$refs.trigger.getBoundingClientRect();
|
||||
const tooltipBoundary = this.$refs.tooltip.getBoundingClientRect();
|
||||
|
||||
const { tooltipX, arrowOffset } = this.getX(triggerBoundary, tooltipBoundary);
|
||||
|
||||
this.tooltipY = triggerBoundary.top + triggerBoundary.height + 5;
|
||||
this.tooltipX = tooltipX;
|
||||
this.arrowOffset = arrowOffset;
|
||||
}
|
||||
|
||||
async function open() {
|
||||
this.events.emit('blur');
|
||||
|
||||
await nextTick();
|
||||
|
||||
this.opened = true;
|
||||
await nextTick();
|
||||
|
||||
this.calculate();
|
||||
this.$emit('open');
|
||||
}
|
||||
|
||||
function close() {
|
||||
this.opened = false;
|
||||
|
||||
this.tooltipY = 0;
|
||||
this.tooltipX = 0;
|
||||
this.arrowOffset = 0;
|
||||
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (this.opened) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
function mounted() {
|
||||
this.events.on('blur', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
this.events.on('resize', () => {
|
||||
this.calculate();
|
||||
});
|
||||
|
||||
this.events.on('scroll', () => {
|
||||
this.calculate();
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
opened: false,
|
||||
tooltipX: 0,
|
||||
tooltipY: 0,
|
||||
arrowOffset: 0,
|
||||
};
|
||||
},
|
||||
emits: ['open', 'close'],
|
||||
mounted,
|
||||
methods: {
|
||||
calculate,
|
||||
getX,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-wrapper {
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 .5rem var(--darken);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: -.5rem;
|
||||
left: calc(50% - .5rem);
|
||||
border-left: .5rem solid transparent;
|
||||
border-right: .5rem solid transparent;
|
||||
border-bottom: .5rem solid var(--background);
|
||||
margin: 0 auto;
|
||||
filter: drop-shadow(0 0 3px var(--darken-weak));
|
||||
}
|
||||
</style>
|
||||
252
assets/components/users/alert.vue
Normal file
252
assets/components/users/alert.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="alert">
|
||||
<div class="alert-section alert-header">
|
||||
<div class="alert-targets">
|
||||
<Icon
|
||||
v-if="alert.notify"
|
||||
icon="bell2"
|
||||
class="alert-action"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="alert.email"
|
||||
icon="envelop"
|
||||
class="alert-action"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="alert.stashes?.length > 0"
|
||||
v-tooltip="alert.stashes.map(stash => stash.name).join()"
|
||||
icon="heart7"
|
||||
class="alert-action"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="header-actions noselect">
|
||||
<Icon
|
||||
v-if="isMe"
|
||||
icon="bin"
|
||||
class="alert-remove"
|
||||
@click.native="showRemoveAlert = true"
|
||||
/>
|
||||
|
||||
<RemoveAlert
|
||||
v-if="showRemoveAlert"
|
||||
:alert="alert"
|
||||
@close="removeAlert"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="alert-triggers">
|
||||
<div
|
||||
v-if="alert.actors?.length > 0"
|
||||
class="alert-section alert-trigger"
|
||||
>
|
||||
<h4
|
||||
v-if="alert.actors.length > 1"
|
||||
class="alert-heading"
|
||||
>Actors</h4>
|
||||
|
||||
<h4
|
||||
v-else
|
||||
class="alert-heading"
|
||||
>Actor</h4>
|
||||
|
||||
<ul class="alert-actors nolist">
|
||||
<li
|
||||
v-for="actor in alert.actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="alert-actor"
|
||||
>
|
||||
<ActorPreview
|
||||
:actor="actor"
|
||||
:alert="alert"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="alert.tags?.length > 0"
|
||||
class="alert-section alert-trigger"
|
||||
>
|
||||
<h4
|
||||
v-if="alert.tags.length > 1"
|
||||
class="alert-heading"
|
||||
>Tags</h4>
|
||||
|
||||
<h4
|
||||
v-else
|
||||
class="alert-heading"
|
||||
>Tag</h4>
|
||||
|
||||
<ul class="alert-tags nolist">
|
||||
<li
|
||||
v-for="tag in alert.tags"
|
||||
:key="`tag-${tag.id}`"
|
||||
><router-link
|
||||
:to="`/tag/${tag.slug}`"
|
||||
class="tag nolink"
|
||||
>{{ tag.name }}</router-link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="alert.entity"
|
||||
class="alert-section alert-trigger"
|
||||
>
|
||||
<h4 class="alert-heading">Channel</h4>
|
||||
|
||||
<Entity
|
||||
v-if="alert.entity"
|
||||
:entity="alert.entity"
|
||||
class="entity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActorPreview from '../actors/preview.vue';
|
||||
import RemoveAlert from '../alerts/remove.vue';
|
||||
import Entity from '../entities/tile.vue';
|
||||
|
||||
async function removeAlert(removed) {
|
||||
this.showRemoveAlert = false;
|
||||
|
||||
if (removed) {
|
||||
this.$emit('remove');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActorPreview,
|
||||
Entity,
|
||||
RemoveAlert,
|
||||
},
|
||||
props: {
|
||||
alert: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isMe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['remove'],
|
||||
data() {
|
||||
return {
|
||||
showRemoveAlert: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
removeAlert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.alert {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
|
||||
.alert-section {
|
||||
display: inline-block;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
|
||||
.alert-action {
|
||||
padding: .5rem;
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-triggers {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
display: block;
|
||||
padding: 0 .5rem .5rem 0;
|
||||
margin: 0;
|
||||
color: var(--shadow-strong);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.alert-more {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 0 .5rem;
|
||||
color: var(--shadow);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.alert-remove {
|
||||
height: auto;
|
||||
padding: 0 .5rem 0 .75rem;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-triggers {
|
||||
dislay: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.alert-trigger {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.alert-actors,
|
||||
.alert-tags {
|
||||
display: flex;
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--shadow-strong);
|
||||
padding: .5rem;
|
||||
border: solid 1px var(--shadow-hint);
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: solid 1px var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.entity {
|
||||
width: 10rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
</style>
|
||||
247
assets/components/users/stash.vue
Normal file
247
assets/components/users/stash.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="stash">
|
||||
<div class="stash-section stash-header">
|
||||
<router-link
|
||||
:to="{ name: 'stash', params: { stashId: stash.id, stashSlug: stash.slug, range: 'scenes', pageNumber: 1 } }"
|
||||
class="stash-link nolink"
|
||||
>
|
||||
<h4 class="stash-name">{{ stash.name }}</h4>
|
||||
<span class="stash-more">Browse</span>
|
||||
</router-link>
|
||||
|
||||
<span class="header-actions noselect">
|
||||
<label
|
||||
v-if="isMe"
|
||||
v-tooltip="'Public'"
|
||||
:class="{ public: stash.public }"
|
||||
class="stash-public"
|
||||
>
|
||||
<Icon
|
||||
v-show="stash.public"
|
||||
icon="eye"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="!stash.public"
|
||||
icon="eye-blocked"
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
:checked="stash.public"
|
||||
@change="checked => publishStash(checked)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Icon
|
||||
v-if="isMe && !stash.primary"
|
||||
icon="bin"
|
||||
class="stash-remove"
|
||||
@click.native="showRemoveStash = true"
|
||||
/>
|
||||
|
||||
<RemoveStash
|
||||
v-if="showRemoveStash"
|
||||
:stash="stash"
|
||||
@close="removeStash"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="stash.scenes?.length > 0"
|
||||
class="stash-section stash-scenes nolist"
|
||||
>
|
||||
<li
|
||||
v-for="{ scene } in stash.scenes"
|
||||
:key="scene.id"
|
||||
class="stash-scene"
|
||||
>
|
||||
<ScenePreview
|
||||
:scene="scene"
|
||||
:stash="stash"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
v-if="stash.actors?.length > 0"
|
||||
class="stash-section stash-actors nolist"
|
||||
>
|
||||
<li
|
||||
v-for="{ actor } in stash.actors"
|
||||
:key="actor.id"
|
||||
class="stash-actor"
|
||||
>
|
||||
<ActorPreview
|
||||
:actor="actor"
|
||||
:stash="stash"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActorPreview from '../actors/preview.vue';
|
||||
import ScenePreview from '../releases/scene-preview.vue';
|
||||
import RemoveStash from '../stashes/remove.vue';
|
||||
import Toggle from '../form/toggle.vue';
|
||||
|
||||
async function publishStash(isPublic) {
|
||||
await this.$store.dispatch('updateStash', {
|
||||
stashId: this.stash.id,
|
||||
stash: { public: isPublic },
|
||||
});
|
||||
|
||||
this.$emit('publish', isPublic);
|
||||
}
|
||||
|
||||
async function removeStash(removed) {
|
||||
this.showRemoveStash = false;
|
||||
|
||||
if (removed) {
|
||||
this.$emit('remove');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActorPreview,
|
||||
ScenePreview,
|
||||
RemoveStash,
|
||||
Toggle,
|
||||
},
|
||||
props: {
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isMe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['publish', 'remove'],
|
||||
data() {
|
||||
return {
|
||||
showRemoveStash: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
publishStash,
|
||||
removeStash,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stash {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--darken-weak);
|
||||
}
|
||||
|
||||
.stash-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
|
||||
&:not(:last-child),
|
||||
&.stash-header {
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.stash-header {
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stash-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover .stash-more {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stash-name {
|
||||
display: inline-block;
|
||||
padding: .5rem;
|
||||
margin: 0;
|
||||
color: var(--shadow-strong);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stash-more {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 0 .5rem;
|
||||
color: var(--shadow);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.stash-public {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-strong);
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stash-remove {
|
||||
height: auto;
|
||||
padding: 0 .5rem 0 .75rem;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--shadow-strong);
|
||||
}
|
||||
}
|
||||
|
||||
.stash-actors,
|
||||
.stash-scenes {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
grid-gap: .5rem;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stash-scenes {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.stash-actor,
|
||||
.stash-scene {
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
font-size: 0;
|
||||
|
||||
&:last-child {
|
||||
padding: 0 .5rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
250
assets/components/users/user.vue
Normal file
250
assets/components/users/user.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="user"
|
||||
class="user"
|
||||
>
|
||||
<div class="header">
|
||||
<h2 class="username">{{ user.username }}</h2>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="user.stashes?.length > 0"
|
||||
class="section"
|
||||
>
|
||||
<div class="section-header">
|
||||
<h3 class="section-heading">Stashes</h3>
|
||||
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="header-add"
|
||||
@click="showAddStash = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul class="section-body stashes nolist">
|
||||
<li
|
||||
v-for="stash in user.stashes"
|
||||
:key="stash.id"
|
||||
class="stashes-stash"
|
||||
>
|
||||
<Stash
|
||||
:stash="stash"
|
||||
:is-me="isMe"
|
||||
@publish="() => fetchUser()"
|
||||
@remove="() => fetchUser()"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="isMe"
|
||||
class="stashes-stash stashes-add"
|
||||
@click="showAddStash = true"
|
||||
>
|
||||
<Icon icon="plus2" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<AddStash
|
||||
v-if="showAddStash"
|
||||
@close="closeAddStash"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-heading">Alerts</h3>
|
||||
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="header-add"
|
||||
@click="showAddAlert = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul class="section-body alerts nolist">
|
||||
<li
|
||||
v-for="alert in user.alerts"
|
||||
:key="`alert-${alert.id}`"
|
||||
class="alert"
|
||||
>
|
||||
<Alert
|
||||
:alert="alert"
|
||||
:is-me="isMe"
|
||||
@remove="() => fetchUser()"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="alerts-add"
|
||||
@click="showAddAlert = true"
|
||||
>
|
||||
<Icon icon="plus2" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<AddAlert
|
||||
v-if="showAddAlert"
|
||||
@close="closeAddAlert"
|
||||
>Alert</AddAlert>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Stash from './stash.vue';
|
||||
import Alert from './alert.vue';
|
||||
import AddStash from '../stashes/add.vue';
|
||||
import AddAlert from '../alerts/add.vue';
|
||||
|
||||
async function fetchUser() {
|
||||
this.user = await this.$store.dispatch('fetchUser', this.$route.params.username);
|
||||
this.isMe = this.user?.id === this.$store.state.auth.user?.id;
|
||||
|
||||
this.pageTitle = this.user?.username;
|
||||
}
|
||||
|
||||
async function closeAddStash(addedStash) {
|
||||
this.showAddStash = false;
|
||||
|
||||
if (addedStash) {
|
||||
await this.fetchUser();
|
||||
}
|
||||
}
|
||||
|
||||
async function closeAddAlert(addedAlert) {
|
||||
this.showAddAlert = false;
|
||||
|
||||
if (addedAlert) {
|
||||
await this.fetchUser();
|
||||
}
|
||||
}
|
||||
|
||||
async function mounted() {
|
||||
await this.fetchUser();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddAlert,
|
||||
AddStash,
|
||||
Alert,
|
||||
Stash,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: this.$route.params.username === this.$store.state.auth.user?.username
|
||||
? this.$store.state.auth.user
|
||||
: null,
|
||||
isMe: false,
|
||||
pageTitle: null,
|
||||
showAddStash: false,
|
||||
showAddAlert: false,
|
||||
};
|
||||
},
|
||||
mounted,
|
||||
methods: {
|
||||
closeAddAlert,
|
||||
closeAddStash,
|
||||
fetchUser,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: var(--profile);
|
||||
}
|
||||
|
||||
.username {
|
||||
padding: .5rem 1rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-light);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 0;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.stashes,
|
||||
.alerts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: 15fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
color: var(--primary);
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.header-add {
|
||||
height: auto;
|
||||
padding: .5rem 1rem;
|
||||
fill: var(--shadow);
|
||||
|
||||
&:hover {
|
||||
fill: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.stashes-stash {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stashes-add,
|
||||
.alerts-add {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
background: var(--shadow-touch);
|
||||
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: var(--shadow-hint);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $breakpoint-kilo) {
|
||||
.stashes,
|
||||
.alerts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
assets/components/video/player.vue
Normal file
143
assets/components/video/player.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<video
|
||||
ref="player"
|
||||
:poster="poster"
|
||||
crossorigin="anonymous"
|
||||
class="player video-js vjs-big-play-centered"
|
||||
@playing="$emit('play')"
|
||||
@pause="$emit('pause')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import videoJs from 'video.js';
|
||||
import 'videojs-vr/dist/videojs-vr.min';
|
||||
|
||||
function updatePlayer() {
|
||||
this.player.src(this.getPath(this.video));
|
||||
this.player.poster(this.poster);
|
||||
}
|
||||
|
||||
function initPlayer() {
|
||||
this.player = videoJs(this.$refs.player, {
|
||||
controls: true,
|
||||
inactivityTimeout: 1000,
|
||||
controlBar: {
|
||||
pictureInPictureToggle: false,
|
||||
volumePanel: {
|
||||
inline: false,
|
||||
},
|
||||
},
|
||||
}, () => {
|
||||
this.player.src(this.getPath(this.video));
|
||||
|
||||
if (this.video.isVr) {
|
||||
this.player.vr({ projection: '180' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
player: null,
|
||||
};
|
||||
},
|
||||
emits: ['play', 'pause'],
|
||||
watch: {
|
||||
video: updatePlayer,
|
||||
poster: updatePlayer,
|
||||
},
|
||||
mounted: initPlayer,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.player.video-js {
|
||||
.vjs-poster {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
background-color: var(--darken);
|
||||
}
|
||||
|
||||
.vjs-slider {
|
||||
background-color: var(--darken-weak);
|
||||
}
|
||||
|
||||
.vjs-play-progress {
|
||||
background-color: var(--primary);
|
||||
|
||||
.vjs-time-tooltip {
|
||||
color: var(--darken-strong);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-load-progress {
|
||||
background-color: var(--lighten-weak);
|
||||
|
||||
div {
|
||||
background-color: var(--darken-weak);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-mouse-display .vjs-time-tooltip {
|
||||
background-color: var(--darken-strong);
|
||||
}
|
||||
|
||||
.vjs-remaining-time {
|
||||
line-height: 2rem;
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vjs-big-play-button {
|
||||
background-color: var(--darken);
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: -2rem 0 0 -2rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
.vjs-icon-placeholder {
|
||||
font-size: 2.5rem;
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-control:focus::before,
|
||||
.vjs-control:hover::before,
|
||||
.vjs-control:focus {
|
||||
text-shadow: 0 0 .5rem var(--lighten-strong);
|
||||
}
|
||||
|
||||
.vjs-error-display {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vjs-modal-dialog-content {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.vjs-big-play-button {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
assets/css/_breakpoints.scss
Normal file
9
assets/css/_breakpoints.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
$breakpoint-pico: 270px;
|
||||
$breakpoint-nano: 320px;
|
||||
$breakpoint-mini: 400px;
|
||||
$breakpoint-micro: 540px;
|
||||
$breakpoint-small: 620px;
|
||||
$breakpoint: 720px;
|
||||
$breakpoint-kilo: 900px;
|
||||
$breakpoint-mega: 1200px;
|
||||
$breakpoint-giga: 1500px;
|
||||
5
assets/css/_forms.scss
Normal file
5
assets/css/_forms.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.form-heading {
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
85
assets/css/_inputs.scss
Normal file
85
assets/css/_inputs.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
border: solid 1px var(--shadow-hint);
|
||||
color: var(--shadow-strong);
|
||||
background: var(--background);
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
|
||||
&:focus {
|
||||
border: solid 1px var(--primary);
|
||||
}
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
color: var(--shadow-strong);
|
||||
background: var(--background);
|
||||
padding: .5rem;
|
||||
font-size: 1rem;
|
||||
border: solid 1px var(--shadow-weak);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: .5rem;
|
||||
font-size: .9rem;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
color: var(--text-light);
|
||||
background: var(--primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-strong);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--shadow-weak);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
color: var(--primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--highlight-strong);
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.album-toggle {
|
||||
height: fit-content;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .5rem 1rem;
|
||||
border: none;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
color: var(--shadow);
|
||||
background: var(--background-dim);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
margin: -.1rem .5rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -17,3 +17,17 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nolink {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,132 @@
|
||||
/* $primary: #ff886c; */
|
||||
$breakpoint0: 540px;
|
||||
$breakpoint: 720px;
|
||||
$breakpoint2: 900px;
|
||||
$breakpoint3: 1200px;
|
||||
$breakpoint4: 1500px;
|
||||
$primary: #ff6c88;
|
||||
|
||||
$background: #fff;
|
||||
$background-dim: #fafafa;
|
||||
$text: #222;
|
||||
$text-contrast: #fff;
|
||||
:root {
|
||||
--primary: #f65596;
|
||||
--primary-strong: #f90071;
|
||||
--primary-faded: #ffcce4;
|
||||
|
||||
$shadow: rgba(0, 0, 0, .5);
|
||||
$shadow-extreme: rgba(0, 0, 0, .9);
|
||||
$shadow-strong: rgba(0, 0, 0, .7);
|
||||
$shadow-weak: rgba(0, 0, 0, .2);
|
||||
$shadow-hint: rgba(0, 0, 0, .1);
|
||||
/*
|
||||
--primary: #f04288;
|
||||
|
||||
$highlight: rgba(255, 255, 255, .5);
|
||||
$highlight-extreme: rgba(255, 255, 255, .9);
|
||||
$highlight-strong: rgba(255, 255, 255, .7);
|
||||
$highlight-weak: rgba(255, 255, 255, .2);
|
||||
$highlight-hint: rgba(255, 255, 255, .075);
|
||||
--primary: #ff6c88;
|
||||
--primary-strong: #ff4166;
|
||||
--primary-faded: #ffdfee;
|
||||
|
||||
$logo-shadow: drop-shadow(1px 0 0 $shadow-weak) drop-shadow(-1px 0 0 $shadow-weak) drop-shadow(0 1px 0 $shadow-weak) drop-shadow(0 -1px 0 $shadow-weak);
|
||||
/* $logo-highlight: drop-shadow(1px 0 0 $highlight-weak) drop-shadow(-1px 0 0 $highlight-weak) drop-shadow(0 1px 0 $highlight-weak) drop-shadow(0 -1px 0 $highlight-weak); */
|
||||
$logo-highlight: drop-shadow(0 0 1px $highlight);
|
||||
--primary: #f28;
|
||||
--primary-strong: #f90071;
|
||||
--primary-faded: #ffcce4;
|
||||
*/
|
||||
|
||||
$profile: #222;
|
||||
$tile: #2a2a2a;
|
||||
--text-dark: #222;
|
||||
--text-light: #fff;
|
||||
|
||||
$link: #cc4466;
|
||||
$empty: #333;
|
||||
--background-light: #fff;
|
||||
--background-dark: #222;
|
||||
|
||||
$male: #0af;
|
||||
$female: #f0a;
|
||||
--darken: rgba(0, 0, 0, .5);
|
||||
--darken-censor: rgba(0, 0, 0, .95);
|
||||
--darken-extreme: rgba(0, 0, 0, .9);
|
||||
--darken-strong: rgba(0, 0, 0, .7);
|
||||
--darken-weak: rgba(0, 0, 0, .2);
|
||||
--darken-hint: rgba(0, 0, 0, .1);
|
||||
--darken-touch: rgba(0, 0, 0, .05);
|
||||
|
||||
--lighten-censor: rgba(255, 255, 255, .95);
|
||||
--lighten-extreme: rgba(255, 255, 255, .9);
|
||||
--lighten-strong: rgba(255, 255, 255, .7);
|
||||
--lighten: rgba(255, 255, 255, .5);
|
||||
--lighten-weak: rgba(255, 255, 255, .2);
|
||||
--lighten-hint: rgba(255, 255, 255, .05);
|
||||
--lighten-touch: rgba(255, 255, 255, .03);
|
||||
|
||||
--logo-shadow: drop-shadow(1px 0 0 $shadow-weak) drop-shadow(-1px 0 0 $shadow-weak) drop-shadow(0 1px 0 $shadow-weak) drop-shadow(0 -1px 0 $shadow-weak);
|
||||
--logo-highlight: drop-shadow(0 0 1px $highlight);
|
||||
|
||||
--info: #321b24;
|
||||
|
||||
--male: #0af;
|
||||
--female: #f0a;
|
||||
|
||||
--alert: #f00;
|
||||
--error: #f00;
|
||||
--warn: #fa0;
|
||||
--success: #5c2;
|
||||
|
||||
--enabled: #5c2;
|
||||
--enabled-background: rgba(0, 255, 0, .1);
|
||||
--disabled: #c20;
|
||||
--disabled-background: rgba(255, 0, 0, .1);
|
||||
--disabled-handle: #aaa;
|
||||
}
|
||||
|
||||
.light {
|
||||
--text: #222;
|
||||
--text-contrast: #fff;
|
||||
|
||||
--background: var(--background-light);
|
||||
--background-censor: rgba(255, 255, 255, .95);
|
||||
--background-dim: #f5f5f5;
|
||||
--background-soft: #fdfdfd;
|
||||
|
||||
--profile: #222;
|
||||
--tile: #2a2a2a;
|
||||
|
||||
--link: #dd6688;
|
||||
--link-external: #48f;
|
||||
--empty: #333;
|
||||
|
||||
--crease: #eaeaea;
|
||||
|
||||
--shadow: rgba(0, 0, 0, .5);
|
||||
--shadow-censor: rgba(0, 0, 0, .95);
|
||||
--shadow-extreme: rgba(0, 0, 0, .9);
|
||||
--shadow-strong: rgba(0, 0, 0, .7);
|
||||
--shadow-modest: rgba(0, 0, 0, .3);
|
||||
--shadow-weak: rgba(0, 0, 0, .2);
|
||||
--shadow-hint: rgba(0, 0, 0, .1);
|
||||
--shadow-touch: rgba(0, 0, 0, .05);
|
||||
|
||||
--highlight: rgba(255, 255, 255, .5);
|
||||
--highlight-extreme: rgba(255, 255, 255, .9);
|
||||
--highlight-strong: rgba(255, 255, 255, .7);
|
||||
--highlight-modest: rgba(255, 255, 255, .3);
|
||||
--highlight-weak: rgba(255, 255, 255, .2);
|
||||
--highlight-hint: rgba(255, 255, 255, .075);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--text: #fff;
|
||||
--text-contrast: #222;
|
||||
|
||||
--background: #181818;
|
||||
--background-censor: rgba(0, 0, 0, .95);
|
||||
--background-dim: #111;
|
||||
--background-soft: #000;
|
||||
|
||||
--profile: #0a0a0a;
|
||||
--tile: #2a2a2a;
|
||||
|
||||
--link: #dd6688;
|
||||
--empty: #333;
|
||||
|
||||
--crease: #222;
|
||||
|
||||
--shadow: rgba(255, 255, 255, .5);
|
||||
--shadow-extreme: rgba(255, 255, 255, .9);
|
||||
--shadow-strong: rgba(255, 255, 255, .7);
|
||||
--shadow-modest: rgba(255, 255, 255, .3);
|
||||
--shadow-weak: rgba(255, 255, 255, .2);
|
||||
--shadow-hint: rgba(255, 255, 255, .075);
|
||||
--shadow-touch: rgba(255, 255, 255, .05);
|
||||
|
||||
--highlight: rgba(0, 0, 0, .5);
|
||||
--highlight-extreme: rgba(0, 0, 0, .9);
|
||||
--highlight-strong: rgba(0, 0, 0, .7);
|
||||
--highlight-modest: rgba(0, 0, 0, .3);
|
||||
--highlight-weak: rgba(0, 0, 0, .2);
|
||||
--highlight-hint: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
background: #222;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
|
||||
&:not(.popover) .tooltip-inner {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
@@ -81,12 +84,11 @@
|
||||
}
|
||||
|
||||
&.popover {
|
||||
$color: #f9f9f9;
|
||||
$color: #fff;
|
||||
|
||||
.popover-inner {
|
||||
background: $color;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, .1);
|
||||
}
|
||||
|
||||
1663
assets/css/_video.scss
Normal file
1663
assets/css/_video.scss
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,8 @@
|
||||
@import 'theme';
|
||||
@import 'forms';
|
||||
@import 'inputs';
|
||||
@import 'states';
|
||||
@import 'video';
|
||||
@import 'tooltip';
|
||||
|
||||
html,
|
||||
@@ -8,9 +11,13 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
color: $text;
|
||||
margin: 0;
|
||||
font-family: Verdana, sans-serif;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nolist {
|
||||
@@ -24,69 +31,25 @@ body {
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: $shadow;
|
||||
color: var(--shadow);
|
||||
padding: 0;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.icon.icon-href {
|
||||
fill: $shadow;
|
||||
fill: var(--shadow);
|
||||
|
||||
:hover {
|
||||
fill: $primary;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
font-weight: bold;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: $shadow;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $shadow-hint;
|
||||
|
||||
.icon {
|
||||
fill: $shadow-strong;
|
||||
}
|
||||
}
|
||||
.link {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.expand-sidebar:hover {
|
||||
background: $shadow-hint;
|
||||
}
|
||||
|
||||
.expand-header {
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
background: $shadow-hint;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background: $profile;
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
fill: $highlight;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
||||
&:hover .icon {
|
||||
background: $highlight-hint;
|
||||
fill: $text-contrast;
|
||||
}
|
||||
.link-external {
|
||||
color: var(--link-external);
|
||||
}
|
||||
|
||||
235
assets/img/avatars/Ace Rockwood - outline - hardon.svg
Normal file
235
assets/img/avatars/Ace Rockwood - outline - hardon.svg
Normal file
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
width="338.93176"
|
||||
height="589.84552"
|
||||
viewBox="0 0 338.93176 589.84552"
|
||||
sodipodi:docname="Ace Rockwood - outline - hardon.svg"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs6" />
|
||||
<sodipodi:namedview
|
||||
inkscape:document-rotation="0"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
id="namedview4"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.1565452"
|
||||
inkscape:cx="4.7736842"
|
||||
inkscape:cy="356.99934"
|
||||
inkscape:window-x="1182"
|
||||
inkscape:window-y="932"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g10" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g10"
|
||||
transform="translate(-36.295882,-155.56359)">
|
||||
<path
|
||||
d="m 275.47475,596.59256 c 0,0 -5.57045,16.25525 -2.26517,28.71709 3.30528,12.46185 16.14089,37.99437 -2.7299,43.01501 -18.87079,5.02063 -17.86029,-21.31429 -17.53605,-23.41181 0.32424,-2.09752 7.39331,-27.47071 6.01109,-52.99575"
|
||||
id="path841"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 276.91561,665.35059 c 0,0 12.61189,8.44579 14.50873,-10.68153"
|
||||
id="path847"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 253.22194,645.08168 c 0,0 -11.01816,56.67539 -28.36563,95.24526"
|
||||
id="path849"
|
||||
sodipodi:nodetypes="cc"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 244.57937,683.12347 c 0,0 3.2737,26.40483 9.54025,57.82357"
|
||||
id="path851"
|
||||
sodipodi:nodetypes="cc"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 316.04674,497.63234 c 0,0 62.71911,92.14608 54.69476,243.32198"
|
||||
id="path853"
|
||||
sodipodi:nodetypes="cc"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 120.06808,741.6296 c 0,0 -6.62708,-56.81125 8.51463,-136.07764 0,0 10.38277,-52.37528 6.92135,-77.56092"
|
||||
id="path855"
|
||||
sodipodi:nodetypes="ccc"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 135.50406,527.99104 c 0,0 -3.81154,-4.17766 15.59765,-31.45559 0,0 2.60877,-1.97035 4.27961,-19.96442 0,0 -4.26771,-18.38982 -0.90695,-29.02908"
|
||||
id="path857"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 154.47437,447.54195 c 0,0 -3.22246,-21.67664 -15.69254,-40.07537 0,0 -5.65525,34.58955 -24.34696,61.05037 0,0 -6.06915,7.2186 -0.60673,13.38372"
|
||||
id="path859"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 113.82814,481.90067 c 0,0 8.2906,7.41008 21.67592,46.09037"
|
||||
id="path861"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 128.81165,604.54868 c 0,0 -64.030285,-53.11103 -70.652618,-87.58275 0,0 -0.190498,-5.4772 -12.31525,-16.03813 0,0 -5.931684,-4.74148 -2.683719,-17.79945 0,0 1.222082,-4.08813 0.936722,-5.49299 -0.28536,-1.40486 -15.879565,-52.62724 16.336608,-114.89948 0,0 3.684461,-7.51013 3.859208,-11.52805 0.174747,-4.01793 -4.770333,-53.55053 14.207228,-71.63893 18.97756,-18.0884 13.354543,-12.18511 35.389091,-31.66871 0,0 7.81837,-6.40344 7.74477,-9.50585 -0.0736,-3.1024 23.35598,-21.87076 47.93206,-34.74942 0,0 11.27377,-7.2633 11.02038,-19.28434 -0.2534,-12.02104 -4.20148,-25.01746 -4.20148,-25.01746"
|
||||
id="path863"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<g
|
||||
transform="rotate(-24.597622,275.06212,612.76383)"
|
||||
inkscape:transform-center-y="3.7858371"
|
||||
inkscape:transform-center-x="-37.612849"
|
||||
id="g868">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path839"
|
||||
d="m 301.91735,580.43313 c 0,0 20.81118,37.91217 29.99809,71.56868 0,0 -1.27474,2.72695 1.28783,3.73133 2.56257,1.00438 1.04672,7.25865 1.04672,7.25865 0,0 -2.91284,37.39221 -29.37703,14.46554 0,0 -1.27844,-0.52885 -2.43403,-3.12866 -1.15559,-2.59982 -2.25028,-3.46823 -3.00127,-4.09416 -0.75098,-0.62594 -17.48928,-34.97245 -26.66746,-57.11914" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path883"
|
||||
d="m 333.16826,655.78513 c 0,0 -27.35631,-11.44567 -31.39538,16.61767" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
d="m 258.95472,591.9171 c 0,0 -1.19161,-6.96253 6.25959,-16.53364"
|
||||
id="path885"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 316.04674,497.63234 c 0,0 -1.06007,-1.80628 -1.06103,-4.64592 -9.5e-4,-2.83965 3.44823,-19.38153 -0.53273,-33.49052 0,0 -2.58853,-11.54571 0.30044,-23.06558 2.88897,-11.51988 6.4611,-21.08418 5.34295,-32.33966 0,0 -0.28941,-1.91395 1.29432,-7.18164 1.58374,-5.26769 0.9702,-17.77481 0.52632,-19.31286 0,0 -0.9758,-7.57132 4.77059,-10.11194 0,0 5.70975,-4.12189 8.85414,-10.47351 0,0 -5.23319,-8.96335 2.55042,-10.41987"
|
||||
id="path887"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 342.99285,294.02579 c 0,0 2.40863,34.29764 -4.90069,52.56505 0,0 0.11318,6.77567 -2.55042,10.41987"
|
||||
id="path889"
|
||||
sodipodi:nodetypes="ccc"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 315.57928,464.92242 c 0,0 -1.26908,-10.90421 0.85002,-17.80542 2.1191,-6.90121 12.41382,-29.04882 18.64462,-68.0488 0,0 -0.41097,-3.1104 4.79552,-12.60636 5.20649,-9.49597 4.31944,-23.71327 4.31944,-23.71327 0,0 -0.62263,-5.09044 0.52859,-10.37701 1.15122,-5.28657 0.90097,-9.52259 7.22618,-30.47694 6.32521,-20.95435 -0.69525,-46.50796 -0.5162,-47.42953 0.17904,-0.92158 -3.16691,-11.40884 -13.83809,-21.72476 -10.67118,-10.31592 -19.99048,-11.46923 -19.99048,-11.46923 0,0 -5.97561,-0.50521 -13.58234,-7.00204 -7.60674,-6.49682 -31.87768,-16.8264 -31.87768,-16.8264"
|
||||
id="path893"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 280.09799,468.44554 c 0,0 2.90232,-4.65637 11.26679,0.31348"
|
||||
id="path895"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 284.69236,466.46836 c 0,0 -4.41383,5.42951 -0.006,7.00776"
|
||||
id="path897"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path903"
|
||||
cx="85.181969"
|
||||
cy="424.32559"
|
||||
rx="9.2404251"
|
||||
ry="4.6258879"
|
||||
transform="rotate(-19.456297)" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path905"
|
||||
cx="221.74664"
|
||||
cy="372.79306"
|
||||
rx="2.4708648"
|
||||
ry="2.4088912" />
|
||||
<path
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.56045;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path907"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="-280.18185"
|
||||
sodipodi:cy="406.56561"
|
||||
sodipodi:rx="2.0267301"
|
||||
sodipodi:ry="1.6016541"
|
||||
sodipodi:start="6.2560712"
|
||||
sodipodi:end="3.2413906"
|
||||
sodipodi:arc-type="slice"
|
||||
d="m -278.15587,406.52219 a 2.0267301,1.6016541 0 0 1 -1.0394,1.4425 2.0267301,1.6016541 0 0 1 -2.09913,-0.0603 2.0267301,1.6016541 0 0 1 -0.9041,-1.49834 l 2.01665,0.15957 z"
|
||||
transform="matrix(0.21840939,-0.97585723,0.98143007,0.1918203,0,0)" />
|
||||
<path
|
||||
d="m 138.78183,407.46658 c 0,0 3.10416,-7.61169 2.10619,-17.95459 0,0 -0.64177,-4.93172 0.83747,-9.14456 1.47925,-4.21285 4.08993,-12.80237 5.85172,-22.38993"
|
||||
id="path909"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 147.57721,357.9775 c 0,0 -0.25569,-1.24908 2.28661,-3.58316 2.54229,-2.33407 7.55128,-11.40353 9.73711,-19.99208"
|
||||
id="path913"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 226.49166,410.49362 c 0,0 -1.75222,14.98063 37.81976,6.59317"
|
||||
id="path915"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 230.09209,441.27245 c 0,0 3.08682,18.35365 39.82279,11.0958"
|
||||
id="path917"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 160.46824,474.23252 c 0,0 5.54755,17.65952 45.97744,22.23086"
|
||||
id="path919"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 267.47967,291.38393 c 0,0 12.90638,54.84957 6.43688,75.26399"
|
||||
id="path921"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 175.64273,372.31082 c 0,0 3.74111,22.67897 56.34499,16.53804"
|
||||
id="path925"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 245.53449,253.09208 c 0,0 -68.25989,-12.22844 -87.19681,-7.69003"
|
||||
id="path927"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 300.12954,225.9896 c 0,0 -24.55374,10.43093 -36.08466,27.88195"
|
||||
id="path929"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 332.63151,261.29222 c 0,0 -27.14867,-14.42698 -24.6289,-27.59554"
|
||||
id="path931"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 326.6876,367.48422 c 0,0 -7.45101,6.4638 -32.86038,8.19647"
|
||||
id="path935"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 231.66646,518.93745 c 0,0 11.89409,5.38851 20.24104,36.03176"
|
||||
id="path937"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 319.26342,402.92347 c 0,0 -0.66468,6.77058 -19.66362,11.10696"
|
||||
id="path941"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 314.05126,437.06207 c 0,0 -3.75693,9.33427 -19.48835,13.97482"
|
||||
id="path943"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 316.04674,497.63234 c 0,0 -11.16016,13.50078 -10.55031,39.39049"
|
||||
id="path945" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path870"
|
||||
d="m 291.42434,654.66906 c 0,0 2.35422,-13.25577 -12.8767,-34.10306"
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
222
assets/img/avatars/Ace Rockwood - outline.svg
Normal file
222
assets/img/avatars/Ace Rockwood - outline.svg
Normal file
@@ -0,0 +1,222 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="Ace Rockwood - outline.svg"
|
||||
viewBox="0 0 338.93176 589.84552"
|
||||
height="589.84552"
|
||||
width="338.93176"
|
||||
id="svg2"
|
||||
version="1.1">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs6" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="g10"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="932"
|
||||
inkscape:window-x="1182"
|
||||
inkscape:cy="288.68986"
|
||||
inkscape:cx="-140.67064"
|
||||
inkscape:zoom="0.81780096"
|
||||
showgrid="false"
|
||||
id="namedview4"
|
||||
inkscape:window-height="1024"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<g
|
||||
transform="translate(-36.295882,-155.56359)"
|
||||
id="g10"
|
||||
inkscape:label="Image"
|
||||
inkscape:groupmode="layer">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path839"
|
||||
d="m 301.91735,580.43313 c 0,0 20.81118,37.91217 29.99809,71.56868 0,0 -1.27474,2.72695 1.28783,3.73133 2.56257,1.00438 1.04672,7.25865 1.04672,7.25865 0,0 -2.91284,37.39221 -29.37703,14.46554 0,0 -1.27844,-0.52885 -2.43403,-3.12866 -1.15559,-2.59982 -2.25028,-3.46823 -3.00127,-4.09416 -0.75098,-0.62594 -17.48928,-34.97245 -26.66746,-57.11914" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path841"
|
||||
d="m 275.47475,596.59256 c 0,0 -5.57045,16.25525 -2.26517,28.71709 3.30528,12.46185 16.14089,37.99437 -2.7299,43.01501 -18.87079,5.02063 -17.86029,-21.31429 -17.53605,-23.41181 0.32424,-2.09752 7.39331,-27.47071 6.01109,-52.99575" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path847"
|
||||
d="m 276.91561,665.35059 c 0,0 12.61189,8.44579 14.50873,-10.68153" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path849"
|
||||
d="m 253.22194,645.08168 c 0,0 -11.01816,56.67539 -28.36563,95.24526" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path851"
|
||||
d="m 244.57937,683.12347 c 0,0 3.2737,26.40483 9.54025,57.82357" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path853"
|
||||
d="m 316.04674,497.63234 c 0,0 62.71911,92.14608 54.69476,243.32198" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="ccc"
|
||||
id="path855"
|
||||
d="m 120.06808,741.6296 c 0,0 -6.62708,-56.81125 8.51463,-136.07764 0,0 10.38277,-52.37528 6.92135,-77.56092" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path857"
|
||||
d="m 135.50406,527.99104 c 0,0 -3.81154,-4.17766 15.59765,-31.45559 0,0 2.60877,-1.97035 4.27961,-19.96442 0,0 -4.26771,-18.38982 -0.90695,-29.02908" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path859"
|
||||
d="m 154.47437,447.54195 c 0,0 -3.22246,-21.67664 -15.69254,-40.07537 0,0 -5.65525,34.58955 -24.34696,61.05037 0,0 -6.06915,7.2186 -0.60673,13.38372" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path861"
|
||||
d="m 113.82814,481.90067 c 0,0 8.2906,7.41008 21.67592,46.09037" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path863"
|
||||
d="m 128.81165,604.54868 c 0,0 -64.030285,-53.11103 -70.652618,-87.58275 0,0 -0.190498,-5.4772 -12.31525,-16.03813 0,0 -5.931684,-4.74148 -2.683719,-17.79945 0,0 1.222082,-4.08813 0.936722,-5.49299 -0.28536,-1.40486 -15.879565,-52.62724 16.336608,-114.89948 0,0 3.684461,-7.51013 3.859208,-11.52805 0.174747,-4.01793 -4.770333,-53.55053 14.207228,-71.63893 18.97756,-18.0884 13.354543,-12.18511 35.389091,-31.66871 0,0 7.81837,-6.40344 7.74477,-9.50585 -0.0736,-3.1024 23.35598,-21.87076 47.93206,-34.74942 0,0 11.27377,-7.2633 11.02038,-19.28434 -0.2534,-12.02104 -4.20148,-25.01746 -4.20148,-25.01746" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path883"
|
||||
d="m 333.16826,655.78513 c 0,0 -27.35631,-11.44567 -31.39538,16.61767" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path885"
|
||||
d="m 258.95472,591.9171 c 0,0 -1.02835,-14.22935 13.05704,-19.53412" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path887"
|
||||
d="m 316.04674,497.63234 c 0,0 -1.06007,-1.80628 -1.06103,-4.64592 -9.5e-4,-2.83965 3.44823,-19.38153 -0.53273,-33.49052 0,0 -2.58853,-11.54571 0.30044,-23.06558 2.88897,-11.51988 6.4611,-21.08418 5.34295,-32.33966 0,0 -0.28941,-1.91395 1.29432,-7.18164 1.58374,-5.26769 0.9702,-17.77481 0.52632,-19.31286 0,0 -0.9758,-7.57132 4.77059,-10.11194 0,0 5.70975,-4.12189 8.85414,-10.47351 0,0 -5.23319,-8.96335 2.55042,-10.41987" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
sodipodi:nodetypes="ccc"
|
||||
id="path889"
|
||||
d="m 342.99285,294.02579 c 0,0 2.40863,34.29764 -4.90069,52.56505 0,0 0.11318,6.77567 -2.55042,10.41987" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path893"
|
||||
d="m 315.57928,464.92242 c 0,0 -1.26908,-10.90421 0.85002,-17.80542 2.1191,-6.90121 12.41382,-29.04882 18.64462,-68.0488 0,0 -0.41097,-3.1104 4.79552,-12.60636 5.20649,-9.49597 4.31944,-23.71327 4.31944,-23.71327 0,0 -0.62263,-5.09044 0.52859,-10.37701 1.15122,-5.28657 0.90097,-9.52259 7.22618,-30.47694 6.32521,-20.95435 -0.69525,-46.50796 -0.5162,-47.42953 0.17904,-0.92158 -3.16691,-11.40884 -13.83809,-21.72476 -10.67118,-10.31592 -19.99048,-11.46923 -19.99048,-11.46923 0,0 -5.97561,-0.50521 -13.58234,-7.00204 -7.60674,-6.49682 -31.87768,-16.8264 -31.87768,-16.8264" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path895"
|
||||
d="m 280.09799,468.44554 c 0,0 2.90232,-4.65637 11.26679,0.31348" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path897"
|
||||
d="m 284.69236,466.46836 c 0,0 -4.41383,5.42951 -0.006,7.00776" />
|
||||
<ellipse
|
||||
transform="rotate(-19.456297)"
|
||||
ry="4.6258879"
|
||||
rx="9.2404251"
|
||||
cy="424.32559"
|
||||
cx="85.181969"
|
||||
id="path903"
|
||||
style="opacity:1;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<ellipse
|
||||
ry="2.4088912"
|
||||
rx="2.4708648"
|
||||
cy="372.79306"
|
||||
cx="221.74664"
|
||||
id="path905"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
transform="matrix(0.21840939,-0.97585723,0.98143007,0.1918203,0,0)"
|
||||
d="m -278.15587,406.52219 a 2.0267301,1.6016541 0 0 1 -1.0394,1.4425 2.0267301,1.6016541 0 0 1 -2.09913,-0.0603 2.0267301,1.6016541 0 0 1 -0.9041,-1.49834 l 2.01665,0.15957 z"
|
||||
sodipodi:arc-type="slice"
|
||||
sodipodi:end="3.2413906"
|
||||
sodipodi:start="6.2560712"
|
||||
sodipodi:ry="1.6016541"
|
||||
sodipodi:rx="2.0267301"
|
||||
sodipodi:cy="406.56561"
|
||||
sodipodi:cx="-280.18185"
|
||||
sodipodi:type="arc"
|
||||
id="path907"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:7.56045;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path909"
|
||||
d="m 138.78183,407.46658 c 0,0 3.10416,-7.61169 2.10619,-17.95459 0,0 -0.64177,-4.93172 0.83747,-9.14456 1.47925,-4.21285 4.08993,-12.80237 5.85172,-22.38993" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path913"
|
||||
d="m 147.57721,357.9775 c 0,0 -0.25569,-1.24908 2.28661,-3.58316 2.54229,-2.33407 7.55128,-11.40353 9.73711,-19.99208" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path915"
|
||||
d="m 226.49166,410.49362 c 0,0 -1.75222,14.98063 37.81976,6.59317" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path917"
|
||||
d="m 230.09209,441.27245 c 0,0 3.08682,18.35365 39.82279,11.0958" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path919"
|
||||
d="m 160.46824,474.23252 c 0,0 5.54755,17.65952 45.97744,22.23086" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path921"
|
||||
d="m 267.47967,291.38393 c 0,0 12.90638,54.84957 6.43688,75.26399" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path925"
|
||||
d="m 175.64273,372.31082 c 0,0 3.74111,22.67897 56.34499,16.53804" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path927"
|
||||
d="m 245.53449,253.09208 c 0,0 -68.25989,-12.22844 -87.19681,-7.69003" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path929"
|
||||
d="m 300.12954,225.9896 c 0,0 -24.55374,10.43093 -36.08466,27.88195" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path931"
|
||||
d="m 332.63151,261.29222 c 0,0 -27.14867,-14.42698 -24.6289,-27.59554" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path935"
|
||||
d="m 326.6876,367.48422 c 0,0 -7.45101,6.4638 -32.86038,8.19647" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path937"
|
||||
d="m 231.66646,518.93745 c 0,0 11.89409,5.38851 20.24104,36.03176" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path941"
|
||||
d="m 319.26342,402.92347 c 0,0 -0.66468,6.77058 -19.66362,11.10696" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path943"
|
||||
d="m 314.05126,437.06207 c 0,0 -3.75693,9.33427 -19.48835,13.97482" />
|
||||
<path
|
||||
id="path945"
|
||||
d="m 316.04674,497.63234 c 0,0 -11.16016,13.50078 -10.55031,39.39049"
|
||||
style="opacity:1;fill:none;stroke:#000000;stroke-width:7.55906;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user