297 Commits

Author SHA1 Message Date
DebaucheryLibrarian
4839a3b94c Allowing HTML in disclaimer. 2023-06-19 04:06:52 +02:00
DebaucheryLibrarian
d8b641e461 Removed references to effective date for older databases. 2022-02-25 22:20:28 +01:00
DebaucheryLibrarian
7b3bdadd44 1.209.4 2022-02-25 22:13:43 +01:00
DebaucheryLibrarian
5deba6b90f Passing resolved parameters into before-scrape methods. 2022-02-25 22:13:41 +01:00
DebaucheryLibrarian
a5afffc968 1.209.3 2022-02-25 00:00:21 +01:00
DebaucheryLibrarian
a239a5c593 Fixed entity scenes function for studios. Added triple anal tag alias. 2022-02-25 00:00:18 +01:00
DebaucheryLibrarian
e56e7333e3 1.209.2 2022-02-24 23:33:26 +01:00
DebaucheryLibrarian
d55e3c37cd Updated media seeds from LegalPorno to AnalVids. 2022-02-24 23:33:19 +01:00
DebaucheryLibrarian
97b78ea016 1.209.1 2022-02-24 23:30:49 +01:00
DebaucheryLibrarian
3e290b74dc Updated affiliates from LegalPorno to AnalVids. 2022-02-24 23:30:47 +01:00
DebaucheryLibrarian
65141207ae 1.209.0 2022-02-24 23:28:27 +01:00
DebaucheryLibrarian
17dfeac1af Renamed LegalPorno to AnalVids, added missing studios. 2022-02-24 23:28:24 +01:00
DebaucheryLibrarian
4a9c428d69 1.208.1 2022-02-24 22:43:01 +01:00
DebaucheryLibrarian
333f252099 Added BJ Raw to Radical. 2022-02-24 22:42:59 +01:00
DebaucheryLibrarian
38232f258a 1.208.0 2022-02-24 22:33:03 +01:00
DebaucheryLibrarian
582269cfaa Added Radical metadata layout scraper and the Got Filled and Inserted channels. 2022-02-24 22:33:00 +01:00
DebaucheryLibrarian
0b646429fd 1.207.5 2022-02-20 23:40:23 +01:00
DebaucheryLibrarian
fecef6c1cf Added missing MOFOS serie channels. 2022-02-20 23:40:20 +01:00
DebaucheryLibrarian
73e5404c44 1.207.4 2022-02-20 16:52:32 +01:00
DebaucheryLibrarian
690d2bb3ed Added MOFOS to default rate limit config, added light logos. 2022-02-20 16:52:29 +01:00
DebaucheryLibrarian
1dd935e1e9 1.207.3 2022-02-14 01:27:58 +01:00
DebaucheryLibrarian
8386230f33 Added missing Team Skeet sites. 2022-02-14 01:27:56 +01:00
DebaucheryLibrarian
5a68b06137 1.207.2 2022-02-13 01:34:41 +01:00
DebaucheryLibrarian
5918364cf5 Preventing unnecessary deep scrapes in Team Skeet scraper. 2022-02-13 01:34:39 +01:00
DebaucheryLibrarian
df4d860d35 1.207.1 2022-02-12 17:19:49 +01:00
DebaucheryLibrarian
d9f0db6e3c Fixed reading screen ID from non-existent item in Bang scraper. 2022-02-12 17:19:47 +01:00
DebaucheryLibrarian
7372b560b2 1.207.0 2022-02-12 17:16:26 +01:00
DebaucheryLibrarian
9d638c685c 1.206.12 2022-02-12 17:16:23 +01:00
DebaucheryLibrarian
5da1acc38d Added media resize. 2022-02-12 17:16:20 +01:00
DebaucheryLibrarian
c7c62e220d Removed console log from Bang scraper. 2022-02-12 16:22:41 +01:00
DebaucheryLibrarian
9edd652a2c 1.206.11 2022-02-12 16:22:03 +01:00
DebaucheryLibrarian
cde760c1ea Preventing Bang! scraper from redundant deep scraping when base release is available. 2022-02-12 16:22:00 +01:00
DebaucheryLibrarian
19c7f958e1 1.206.10 2022-02-12 03:26:43 +01:00
DebaucheryLibrarian
c7710afdbf Fixed Bang! scraped failing when scene has no photos. 2022-02-12 03:26:40 +01:00
DebaucheryLibrarian
ba18a3dadb 1.206.9 2022-02-11 22:14:46 +01:00
DebaucheryLibrarian
496c29e569 Configured Reality Kings to fetch session from RK scene overview. 2022-02-11 22:14:44 +01:00
DebaucheryLibrarian
98a72a4929 1.206.8 2022-02-09 00:19:56 +01:00
DebaucheryLibrarian
c4f0b48932 Fixed cover dimensions on movie tile to show lazy image, showing missing cover icon. 2022-02-09 00:19:54 +01:00
DebaucheryLibrarian
93abbab873 1.206.7 2022-02-08 23:59:48 +01:00
DebaucheryLibrarian
f7bbab08bd Added Porn Pros main channel and working URLs for Flexible Positions, Public Violations and Squirt Disgrace. Added bisexual tag photos. 2022-02-08 23:59:44 +01:00
DebaucheryLibrarian
1e4ddf2bbf 1.206.6 2022-02-08 00:19:36 +01:00
DebaucheryLibrarian
9b7879bff7 Removed channel ID from Gamma update query to prevent duplicate scenes. Added sexuality tags with covers to overview. 2022-02-08 00:19:34 +01:00
DebaucheryLibrarian
03d96d4dec 1.206.5 2022-02-07 22:29:11 +01:00
DebaucheryLibrarian
1dfa034332 Handling 'page not found' with 200 OK in Gamma scraper. 2022-02-07 22:29:09 +01:00
DebaucheryLibrarian
196a1d33e3 1.206.4 2022-02-07 22:16:46 +01:00
DebaucheryLibrarian
3d349c413f Fixed Adult Time scraper. 2022-02-07 22:16:43 +01:00
DebaucheryLibrarian
bd5babd37e 1.206.3 2022-02-07 21:58:31 +01:00
DebaucheryLibrarian
fffe9080f3 Added stand-alone Spizoo sites. 2022-02-07 21:58:27 +01:00
DebaucheryLibrarian
5630b16e99 1.206.2 2022-02-07 01:57:14 +01:00
DebaucheryLibrarian
4bcf7ef45b Storing associations if existing media are found. 2022-02-07 01:57:09 +01:00
DebaucheryLibrarian
9abc1d48ac 1.206.1 2022-02-05 17:14:37 +01:00
DebaucheryLibrarian
ceaf5a3217 Not storing shallow movie data when argument is disabled. 2022-02-05 17:14:34 +01:00
DebaucheryLibrarian
36d84fb98b 1.206.0 2022-02-03 00:04:51 +01:00
DebaucheryLibrarian
3d80e9d77b Added movie URL file support. Fixed Gamma movie API scraper movie URL. 2022-02-03 00:04:48 +01:00
DebaucheryLibrarian
1dc38ffacc 1.205.20 2022-02-02 23:53:44 +01:00
DebaucheryLibrarian
ee210b5c68 Replaced Gender X with Gender X Films, fixed Zero Tolerance movie URLs. 2022-02-02 23:53:42 +01:00
DebaucheryLibrarian
3aae3cd3a1 1.205.19 2022-02-02 23:51:20 +01:00
DebaucheryLibrarian
f1a7f2f905 Added Gamma movie API scraper. 2022-02-02 23:51:15 +01:00
DebaucheryLibrarian
0b7e0319f1 1.205.18 2022-02-02 22:25:06 +01:00
DebaucheryLibrarian
10b8abf706 Updated Zero Tolerance channel to Zero Tolerance Films. 2022-02-02 22:25:04 +01:00
DebaucheryLibrarian
b163223ff1 1.205.17 2022-02-01 21:18:58 +01:00
DebaucheryLibrarian
b9f3eb85f9 Ignoring Gamma master categories in update scrape as these seem to contain irrelevant tags. 2022-02-01 21:18:54 +01:00
DebaucheryLibrarian
285a65f018 1.205.16 2022-01-30 22:15:09 +01:00
DebaucheryLibrarian
815d56d334 Preventing media associations when use-reference is unavailable. 2022-01-30 22:15:07 +01:00
DebaucheryLibrarian
27a9a233e2 1.205.15 2022-01-30 17:05:20 +01:00
DebaucheryLibrarian
22864105ac Writing media associations to disk for debugging. 2022-01-30 17:05:18 +01:00
DebaucheryLibrarian
7cf47cbf8d 1.205.14 2022-01-30 00:18:17 +01:00
DebaucheryLibrarian
502c588958 Updated Bang Bros product ID for Haze Her, removed wrong ones. 2022-01-30 00:18:15 +01:00
DebaucheryLibrarian
81792a7c2f 1.205.13 2022-01-30 00:08:41 +01:00
DebaucheryLibrarian
784d326584 Updated Bang Bros product ID for College Rules. 2022-01-30 00:08:39 +01:00
DebaucheryLibrarian
83dc7aa578 1.205.12 2022-01-29 23:50:01 +01:00
DebaucheryLibrarian
5a3b27fbf7 Fixed movie cover index for overview in GraphQL query. 2022-01-29 23:49:59 +01:00
DebaucheryLibrarian
5ff83a9497 1.205.11 2022-01-29 23:39:13 +01:00
DebaucheryLibrarian
07cb39c756 Fixed movie cover index in GraphQL query. 2022-01-29 23:39:12 +01:00
DebaucheryLibrarian
fffd54995d 1.205.10 2022-01-29 17:13:13 +01:00
DebaucheryLibrarian
5302176f0b Re-added channel count to networks page. 2022-01-29 17:13:09 +01:00
DebaucheryLibrarian
efd7cf8acc 1.205.9 2022-01-29 16:38:42 +01:00
DebaucheryLibrarian
52218c30f8 Not fetching mobile Gamma page when photos are not expected. 2022-01-29 16:38:36 +01:00
DebaucheryLibrarian
04c176fa8b 1.205.8 2022-01-29 03:34:31 +01:00
DebaucheryLibrarian
af320684b4 Added Bang Bros sites. 2022-01-29 03:34:26 +01:00
DebaucheryLibrarian
b2ba14ce45 1.205.7 2022-01-29 02:39:17 +01:00
DebaucheryLibrarian
d776cc2f71 Added Abuse Me to Bang Bros network. 2022-01-29 02:39:14 +01:00
DebaucheryLibrarian
b4bed2f623 1.205.6 2022-01-29 01:21:45 +01:00
DebaucheryLibrarian
5c5a11fdca Added Bang Bros members page scraper (not for full scenes). 2022-01-29 01:21:41 +01:00
DebaucheryLibrarian
a22c62d893 1.205.5 2022-01-28 04:05:38 +01:00
DebaucheryLibrarian
90bc2f3724 Changed release media association existence check, added log for details. 2022-01-28 04:05:29 +01:00
DebaucheryLibrarian
8cdc794a3a 1.205.4 2022-01-26 13:52:41 +01:00
DebaucheryLibrarian
250618df8b Added entity scene count function. 2022-01-26 13:52:39 +01:00
DebaucheryLibrarian
b62ae00c54 1.205.3 2022-01-26 01:47:36 +01:00
DebaucheryLibrarian
67d0a9e0e0 Fixed scene entity tag association. 2022-01-26 01:47:34 +01:00
b4425bc3bb Merge pull request 'Add Facials4k' (#44) from boiii/traxxx:add-facials-4k into master
Reviewed-on: http://gitea.unknown.name/DebaucheryLibrarian/traxxx/pulls/44
2022-01-25 22:29:10 +00:00
d77c145de3 Update 'seeds/02_sites.js' 2022-01-25 20:56:11 +00:00
DebaucheryLibrarian
6753f923d9 1.205.2 2022-01-25 15:34:17 +01:00
DebaucheryLibrarian
452f725332 Switched poster URL preference in Gamma scraper to prevent cropped image on XEmpire. 2022-01-25 15:34:15 +01:00
DebaucheryLibrarian
7c1f45bcfb 1.205.1 2022-01-24 22:29:29 +01:00
DebaucheryLibrarian
11ea54f6e8 Fixed outdated alerts query. 2022-01-24 22:29:27 +01:00
DebaucheryLibrarian
5b623ee46f 1.205.0 2022-01-24 00:15:19 +01:00
DebaucheryLibrarian
7ceec1c739 Added indexes to scene tag and movie scene tables improve performance. Removed scene count from channel overview, too expensive. 2022-01-24 00:15:11 +01:00
DebaucheryLibrarian
fc318536aa 1.204.13 2022-01-23 02:46:44 +01:00
DebaucheryLibrarian
cd8f690ac6 Preferring network over channel in deep URL channel matching. 2022-01-23 02:46:42 +01:00
DebaucheryLibrarian
240a8e86fd 1.204.12 2022-01-22 23:20:10 +01:00
DebaucheryLibrarian
c557f0f1f0 Improved Gamma image sources to prevent cropped images. 2022-01-22 23:20:08 +01:00
DebaucheryLibrarian
86f56695e2 1.204.11 2022-01-20 00:54:13 +01:00
DebaucheryLibrarian
20da2d1cf6 Reusing batch ID for movies to preserve new-flag. 2022-01-20 00:54:10 +01:00
DebaucheryLibrarian
372db86927 Disabled MindGeek session bundling to analyze Too Many Requests errors. 2022-01-16 22:24:47 +01:00
DebaucheryLibrarian
4c503a3177 1.204.10 2022-01-16 17:17:49 +01:00
DebaucheryLibrarian
57d46f5842 Fixed latest query in Gamma scraper. 2022-01-16 17:17:45 +01:00
DebaucheryLibrarian
7f11d0ca91 1.204.9 2022-01-12 17:50:27 +01:00
DebaucheryLibrarian
bbd788699e Fixed GraphQL query for scenes REST API. Added entry ID to scene search document. 2022-01-12 17:50:23 +01:00
DebaucheryLibrarian
e24add98ea 1.204.8 2022-01-10 02:17:21 +01:00
DebaucheryLibrarian
506971b44b Chunked duplicate check to prevent postgres stack depth errors. 2022-01-10 02:17:17 +01:00
DebaucheryLibrarian
43a0bc8a2c 1.204.7 2022-01-07 01:07:43 +01:00
DebaucheryLibrarian
baebdbc0bb Adding comment listing sister sites for non-exclusive Gamma API scenes. 2022-01-07 01:07:41 +01:00
DebaucheryLibrarian
1c24cae3b1 1.204.6 2022-01-07 01:02:36 +01:00
DebaucheryLibrarian
b9146aee5f Removed exclusive filter from Evil Angel parameters. 2022-01-07 01:02:34 +01:00
DebaucheryLibrarian
09bfcb01f5 1.204.5 2022-01-06 01:41:29 +01:00
DebaucheryLibrarian
5d80b6dc21 Added and improved Reality Kings logos. Added tag photos. 2022-01-06 01:41:26 +01:00
DebaucheryLibrarian
140a38c349 1.204.4 2021-12-31 01:45:06 +01:00
DebaucheryLibrarian
6760c11881 Disabled why-is-node-running to assess OOM. 2021-12-31 01:45:03 +01:00
DebaucheryLibrarian
2dcdbf9c37 1.204.3 2021-12-27 22:36:03 +01:00
DebaucheryLibrarian
3b958a02ef Added memory profiler configuration options. 2021-12-27 22:36:01 +01:00
DebaucheryLibrarian
605242b399 1.204.2 2021-12-27 17:29:33 +01:00
DebaucheryLibrarian
299f257a01 Logging memory usage on media fetch. 2021-12-27 17:29:23 +01:00
DebaucheryLibrarian
0ad64ef5f4 1.204.1 2021-12-20 03:29:16 +01:00
DebaucheryLibrarian
3bfe1e8fdb Storing scenes before fetching movies. 2021-12-20 03:29:14 +01:00
DebaucheryLibrarian
7d992f4129 Added triggers to memory dump. 2021-12-20 02:28:59 +01:00
DebaucheryLibrarian
280032937f Added centralized movie page fetcher. Added memory heap dump. 2021-12-20 02:22:10 +01:00
DebaucheryLibrarian
d93670842b 1.204.0 2021-12-19 23:03:48 +01:00
DebaucheryLibrarian
dc187a9a3a Added execute method to qu, removed runScripts from Gamma's fetchMovie to observe effect on memory usage. 2021-12-19 23:03:44 +01:00
DebaucheryLibrarian
5f89c6e14c 1.203.13 2021-12-19 00:08:22 +01:00
DebaucheryLibrarian
9f10420eb9 Fixed tag search GraphQL query. 2021-12-19 00:08:21 +01:00
DebaucheryLibrarian
9568370132 1.203.12 2021-12-18 23:44:54 +01:00
DebaucheryLibrarian
1c0c30be11 Improved and re-enabled why-is-node-running logger. 2021-12-18 23:44:51 +01:00
DebaucheryLibrarian
cdb7dcd6e9 1.203.11 2021-12-18 23:22:38 +01:00
DebaucheryLibrarian
8e4be06675 Closing JSDOM window after deep scrapes in an attempt to save memory. 2021-12-18 23:22:35 +01:00
DebaucheryLibrarian
de7a8c756a 1.203.10 2021-12-13 01:20:04 +01:00
DebaucheryLibrarian
65e2b72c6a Fixed Gamma movie scraper failing when data object is not available. 2021-12-13 01:20:02 +01:00
DebaucheryLibrarian
523c36ecd4 1.203.9 2021-12-11 22:46:58 +01:00
DebaucheryLibrarian
5b5688438c Fixed Gamma movie scene query. Added tag photos. 2021-12-11 22:46:55 +01:00
DebaucheryLibrarian
4ca07631c7 1.203.8 2021-12-05 23:59:20 +01:00
DebaucheryLibrarian
ec4e7bc12a Updated Jules Jordan scraper for Sperm Swallowers and The Ass Factory. 2021-12-05 23:59:17 +01:00
DebaucheryLibrarian
26d15c0a6f Updated Jules Jordan scraper for Manuel Ferrara and Girl Girl. 2021-12-05 23:43:23 +01:00
DebaucheryLibrarian
a484396db4 Hiding scenes with missing dates from network overview. Using computed effective date column. 2021-12-05 23:29:27 +01:00
DebaucheryLibrarian
681db02784 1.203.7 2021-12-05 02:54:57 +01:00
DebaucheryLibrarian
e88554666c Reduced media concurrency to assess effect on memory. Moved qu context removal to deep scrape runner. Updated movie graphql queries. 2021-12-05 02:54:55 +01:00
DebaucheryLibrarian
9f37ec4cff 1.203.6 2021-12-04 00:32:31 +01:00
DebaucheryLibrarian
abd063a578 Removed window map from http module to prevent memory pile-up with update scraping. 2021-12-04 00:32:28 +01:00
DebaucheryLibrarian
883e57ce1f 1.203.5 2021-12-02 15:51:33 +01:00
DebaucheryLibrarian
ad04502a8c Added Radical favicons. 2021-12-02 15:51:30 +01:00
DebaucheryLibrarian
f65ed16842 1.203.4 2021-12-02 00:45:24 +01:00
DebaucheryLibrarian
1918dd4ad5 Removing query methods from XEmpire release. 2021-12-02 00:45:22 +01:00
DebaucheryLibrarian
3983d3464f 1.203.3 2021-12-01 23:44:27 +01:00
DebaucheryLibrarian
61daf5f037 Added debug log for jsdom window closing. 2021-12-01 23:44:25 +01:00
DebaucheryLibrarian
eafa144104 1.203.2 2021-12-01 23:39:11 +01:00
DebaucheryLibrarian
60c151ff6a Reduced deep scrape concurrency to 1. 2021-12-01 23:39:09 +01:00
DebaucheryLibrarian
c97d18fcf9 1.203.1 2021-12-01 23:30:12 +01:00
DebaucheryLibrarian
e41f9fa937 Added waitImmediate to deep scrape, reduced concurrency. 2021-12-01 23:30:10 +01:00
DebaucheryLibrarian
56a7fb0ad9 1.203.0 2021-12-01 17:26:34 +01:00
DebaucheryLibrarian
e29cbc9fea Closing JSDOM window after deep scrape in an attempt to save memory. Reduced deep scrape concurrency to 5. 2021-12-01 17:26:13 +01:00
DebaucheryLibrarian
08f725a0b6 1.202.3 2021-12-01 00:00:26 +01:00
DebaucheryLibrarian
b93a5715cb Updated Jules Jordan scraper for new update page layout. 2021-12-01 00:00:24 +01:00
DebaucheryLibrarian
2018d5c298 1.202.2 2021-11-29 02:49:12 +01:00
DebaucheryLibrarian
87094a9498 Replaced cheerio with qu in Gamma scraper. 2021-11-29 02:49:07 +01:00
DebaucheryLibrarian
53a1c697d0 1.202.1 2021-11-28 01:20:41 +01:00
DebaucheryLibrarian
f04eb72891 Added upcoming scraper to PurgatoryX. 2021-11-28 01:20:39 +01:00
DebaucheryLibrarian
dfeb0c08cf 1.202.0 2021-11-27 23:55:22 +01:00
DebaucheryLibrarian
9d7183ac69 Added PurgatoryX scraper. 2021-11-27 23:55:16 +01:00
DebaucheryLibrarian
2539e88f47 1.201.6 2021-11-22 02:51:55 +01:00
DebaucheryLibrarian
20d0d860d3 Fixed MindGeek scraper trying to acquire session from mindgeek.com 2021-11-22 02:51:52 +01:00
DebaucheryLibrarian
6b4aa64d74 Improved MindGeek scraper session check to prevent crash when network session isn't available yet. 2021-11-22 02:44:03 +01:00
DebaucheryLibrarian
c7b9ec7a4b 1.201.5 2021-11-21 00:47:34 +01:00
DebaucheryLibrarian
e81864ebde Removed verbose http queued output. 2021-11-21 00:47:32 +01:00
DebaucheryLibrarian
0305a22a24 Removed stray console log from media module. 2021-11-21 00:43:35 +01:00
DebaucheryLibrarian
a406eb584d 1.201.4 2021-11-21 00:41:15 +01:00
DebaucheryLibrarian
2be3ea9bbb Explicitly adding headers to http module result. 2021-11-21 00:41:12 +01:00
DebaucheryLibrarian
0a8a7ff9a5 1.201.3 2021-11-21 00:31:12 +01:00
DebaucheryLibrarian
98c103c625 Fixed uuid import. Changed fs rmdir to rm. 2021-11-21 00:31:09 +01:00
DebaucheryLibrarian
e0908a5f5e 1.201.2 2021-11-21 00:19:13 +01:00
DebaucheryLibrarian
f0b7678444 Fixed upsert failing on empty insert array due breaking Knex API change. 2021-11-21 00:19:10 +01:00
DebaucheryLibrarian
64e9efe095 1.201.1 2021-11-21 00:15:21 +01:00
DebaucheryLibrarian
ab29ab053e Increased tile heart button size. Hiding entity tile info under hover effect. 2021-11-21 00:15:19 +01:00
DebaucheryLibrarian
a5081bc7c9 Updated primary color. 2021-11-21 00:04:21 +01:00
DebaucheryLibrarian
da9c628f9b 1.201.0 2021-11-21 00:00:14 +01:00
DebaucheryLibrarian
26539b74a5 Updated dependencies. Added periodic memory logger. 2021-11-20 23:59:48 +01:00
DebaucheryLibrarian
a867817dc1 Improved scene tile scaling. 2021-10-31 01:00:12 +02:00
DebaucheryLibrarian
d1e05915b5 Tweaked scene and actor tile design, tags grid breakpoints. 2021-10-31 00:31:18 +02:00
DebaucheryLibrarian
b764fdec85 Showing scene and channel count on entity page and tile. 2021-10-30 22:41:58 +02:00
DebaucheryLibrarian
8322d43b09 1.200.2 2021-10-28 02:10:37 +02:00
DebaucheryLibrarian
29b8c5e38e Including unextracted scenes in date determination. 2021-10-28 02:10:30 +02:00
DebaucheryLibrarian
83dd233991 1.200.1 2021-10-28 02:00:04 +02:00
DebaucheryLibrarian
0864154a0e Added unextracted property to keep paginating when extracting scenes. 2021-10-28 01:59:53 +02:00
DebaucheryLibrarian
53357d4bd2 1.200.0 2021-10-27 17:19:33 +02:00
DebaucheryLibrarian
69bf98edf1 1.199.17 2021-10-27 17:19:30 +02:00
DebaucheryLibrarian
a22c4d5679 Added beforeNetwork hook, used by MindGeek. Added Filthy Kings to Gamma. 2021-10-27 17:19:23 +02:00
DebaucheryLibrarian
e5ad1648eb 1.199.16 2021-10-26 23:42:42 +02:00
DebaucheryLibrarian
100a35b4e8 Added before scene fetch method to prevent e.g. unnecessary session requests, moved scraper assignment to entity lookup. Removed channel URL hostname matching.. 2021-10-26 23:42:32 +02:00
DebaucheryLibrarian
6c5d4389fe Not parsing HTML with jsdom when using http module directly to save memory. Added loading ellipsis to release grid pages. 2021-10-25 02:06:24 +02:00
DebaucheryLibrarian
92f9ff4104 1.199.15 2021-10-20 01:47:04 +02:00
DebaucheryLibrarian
0e4fd12d70 Added more loggers to http module. 2021-10-20 01:46:56 +02:00
DebaucheryLibrarian
9040285ce5 1.199.14 2021-10-17 23:57:10 +02:00
DebaucheryLibrarian
972b15e72d Added memory profiling. 2021-10-17 23:56:49 +02:00
DebaucheryLibrarian
c1aea78496 1.199.13 2021-10-17 19:59:11 +02:00
DebaucheryLibrarian
49f891ba44 Ignoring 1-second scene duration from MindGeek API. 2021-10-17 19:59:05 +02:00
DebaucheryLibrarian
239057f1b6 1.199.12 2021-10-17 00:28:18 +02:00
DebaucheryLibrarian
167df35d37 Fixed tags module not dealing with empty releases or tags, added origin tag column to chapter tag table. 2021-10-17 00:28:13 +02:00
DebaucheryLibrarian
b1b2ad2111 1.199.11 2021-10-11 04:31:46 +02:00
DebaucheryLibrarian
f1ff662be2 Fixed tags seed file. 2021-10-11 04:31:40 +02:00
DebaucheryLibrarian
2418fec9c9 1.199.10 2021-10-11 02:16:58 +02:00
DebaucheryLibrarian
aaaa0a6afb Updated proxy list. 2021-10-11 02:16:51 +02:00
DebaucheryLibrarian
afe22003e1 1.199.9 2021-10-10 00:04:28 +02:00
DebaucheryLibrarian
b6ad2903f3 Storing original tags. 2021-10-10 00:04:21 +02:00
DebaucheryLibrarian
6c298cd639 1.199.8 2021-09-28 20:45:30 +02:00
DebaucheryLibrarian
dbff3e9539 Added Brad Montana. Added teaser link to trailer video. 2021-09-28 20:45:22 +02:00
DebaucheryLibrarian
fcc6b33d07 1.199.7 2021-09-17 04:28:54 +02:00
DebaucheryLibrarian
0224c26ca0 Fixed alert searches. Added Teen Mega World campaigns. 2021-09-17 04:28:48 +02:00
DebaucheryLibrarian
294f15e694 1.199.6 2021-09-17 03:30:58 +02:00
DebaucheryLibrarian
522584711b Added Teen Mega World scraper. 2021-09-17 03:30:49 +02:00
DebaucheryLibrarian
ae40f33283 Added Transfixed to Adult Time. 2021-09-16 16:57:14 +02:00
DebaucheryLibrarian
172e01c80e Added Score to default ignored networks. 2021-09-15 02:01:13 +02:00
DebaucheryLibrarian
901acf1390 1.199.5 2021-09-14 00:42:59 +02:00
DebaucheryLibrarian
a316da3083 Retrieving Spizoo trailers. 2021-09-14 00:42:51 +02:00
DebaucheryLibrarian
0b46e27497 1.199.4 2021-09-13 01:29:47 +02:00
DebaucheryLibrarian
b164c5dad7 Added Spizoo. 2021-09-13 01:29:39 +02:00
DebaucheryLibrarian
2d6b285817 1.199.3 2021-09-12 23:21:45 +02:00
DebaucheryLibrarian
65c79567d2 Added unstash buttons to non-favorites stash items 2021-09-12 23:21:39 +02:00
DebaucheryLibrarian
d3633f31ac 1.199.2 2021-09-12 00:33:30 +02:00
DebaucheryLibrarian
73a9a11aa6 Added favorites link to user menu. 2021-09-12 00:33:24 +02:00
DebaucheryLibrarian
c21bd8b01e 1.199.1 2021-09-12 00:24:42 +02:00
DebaucheryLibrarian
14f4d24872 Fixed movie stash hover color. Added secondary release date sorting on stashed releases. 2021-09-12 00:24:36 +02:00
DebaucheryLibrarian
731abc79ee 1.199.0 2021-09-12 00:05:47 +02:00
DebaucheryLibrarian
d542889827 Added sections and pagination to stash page. 2021-09-12 00:05:45 +02:00
DebaucheryLibrarian
8c5ef21459 1.198.11 2021-08-30 01:37:22 +02:00
DebaucheryLibrarian
7bfca9596d Appending movie title if scene title only contains 'scene x' 2021-08-30 01:37:19 +02:00
DebaucheryLibrarian
7bd858f96b Generalized Adult Empire subsite scraper, added West Coast Productions. 2021-08-30 01:13:32 +02:00
DebaucheryLibrarian
b8657cb6e6 1.198.10 2021-08-26 01:14:58 +02:00
DebaucheryLibrarian
69a7e8f13d Fixed PornDoe scraper. 2021-08-26 01:14:54 +02:00
DebaucheryLibrarian
23537e0243 Removed Gamma console logs. 2021-08-24 02:56:59 +02:00
DebaucheryLibrarian
546f778b1b 1.198.9 2021-08-24 02:56:03 +02:00
DebaucheryLibrarian
245f69a122 Fixed movie bindings. 2021-08-24 02:54:40 +02:00
DebaucheryLibrarian
c6f3f7a239 1.198.8 2021-08-23 02:38:18 +02:00
DebaucheryLibrarian
f0a6e80e5a Reset pagination on search, scroll into view when paginating search. 2021-08-23 02:38:16 +02:00
DebaucheryLibrarian
0cbb985945 1.198.7 2021-08-23 01:54:24 +02:00
DebaucheryLibrarian
bb384029ac Disabled eager searching except for entities. Updating movie search document on store. 2021-08-23 01:54:22 +02:00
DebaucheryLibrarian
85c6b581cb Fixed movie limit. 2021-08-23 01:45:18 +02:00
DebaucheryLibrarian
dd18644796 1.198.6 2021-08-23 01:44:32 +02:00
DebaucheryLibrarian
1628e41d09 Fixed actors and movies pagination scroll. 2021-08-23 01:44:30 +02:00
DebaucheryLibrarian
a77d8f4cea 1.198.5 2021-08-23 01:35:48 +02:00
DebaucheryLibrarian
0a4152b4b7 Fixed actor search query. 2021-08-23 01:35:46 +02:00
DebaucheryLibrarian
3fc4dc15df 1.198.4 2021-08-23 01:29:49 +02:00
DebaucheryLibrarian
4ee0dcef9b Added country filter for actors. 2021-08-23 01:29:46 +02:00
DebaucheryLibrarian
011482ac9d 1.198.3 2021-08-22 22:25:23 +02:00
DebaucheryLibrarian
6a8c9d89cb Using paginated full text search for movies, combined actor search and fetch to allow combining search with filters. 2021-08-22 22:25:20 +02:00
DebaucheryLibrarian
e0905ab8fc 1.198.2 2021-08-22 03:14:08 +02:00
DebaucheryLibrarian
eb1f8f86fd Added search to tags. 2021-08-22 03:14:02 +02:00
DebaucheryLibrarian
959b5d9d0e 1.198.1 2021-08-22 01:26:12 +02:00
DebaucheryLibrarian
0c19a026ef Replaced alphabet index with search bar on actors page. 2021-08-22 01:26:09 +02:00
DebaucheryLibrarian
b24973eb19 1.198.0 2021-08-22 00:40:27 +02:00
DebaucheryLibrarian
4b18867883 Added pagination and search to movies page. 2021-08-22 00:40:22 +02:00
DebaucheryLibrarian
5e292a0880 1.197.4 2021-08-17 19:25:14 +02:00
DebaucheryLibrarian
f00e37490c Finished Cum Louder scraper, updated Vixen scraper. Added tag posters. 2021-08-17 19:25:10 +02:00
DebaucheryLibrarian
715e44cf21 1.197.3 2021-08-15 16:56:00 +02:00
DebaucheryLibrarian
25d1c1b229 Fixed incomplete scene tile studio link breaking search. 2021-08-15 16:55:54 +02:00
DebaucheryLibrarian
094226eeb5 Added new LegalPorno/AnalVids studios. 2021-08-15 15:28:13 +02:00
DebaucheryLibrarian
b08cb46ae5 Removed stray console log. 2021-08-15 13:21:02 +02:00
DebaucheryLibrarian
6b88cf1040 1.197.2 2021-08-15 13:16:50 +02:00
DebaucheryLibrarian
aacfd1b29d Updated object-merge-advanced API use. 2021-08-15 13:16:48 +02:00
DebaucheryLibrarian
0427e1e276 1.197.1 2021-08-15 04:28:59 +02:00
DebaucheryLibrarian
ee33bd8e63 Upgraded object-merge-advanced. 2021-08-15 04:28:56 +02:00
DebaucheryLibrarian
25b8bd689e Rebuild. 2021-08-14 22:42:50 +02:00
DebaucheryLibrarian
b6468b03a7 1.197.0 2021-08-09 10:31:25 +02:00
DebaucheryLibrarian
a848d6991b Added Diabolic and Cum Louder, added content type expect option to media sources to fix Vixen thumbnails. 2021-08-09 10:31:12 +02:00
DebaucheryLibrarian
65c3053b49 Fixed affiliate seed flush order. 2021-07-12 01:48:01 +02:00
DebaucheryLibrarian
1d686d7e40 1.196.6 2021-07-12 01:41:22 +02:00
DebaucheryLibrarian
488d1082e4 Added parameter affiliates. 2021-07-12 01:41:18 +02:00
DebaucheryLibrarian
8967907893 1.196.5 2021-07-06 00:01:51 +02:00
DebaucheryLibrarian
e527a67dc1 Merge branch 'experimental' 2021-07-06 00:01:47 +02:00
DebaucheryLibrarian
6847ef690c Added Arch Angel, updated BAM Visions scraper to accomodate Arch Angel (different network, same unidentified CMS). 2021-07-06 00:01:44 +02:00
DebaucheryLibrarian
96a2125248 Added tag photos. 2021-07-05 15:54:37 +02:00
DebaucheryLibrarian
c5e4310a6b 1.196.4 2021-07-05 00:06:26 +02:00
DebaucheryLibrarian
23b41fc4f3 Fixed Bang scraper. Added Kink affiliate, tag photos. 2021-07-05 00:06:18 +02:00
DebaucheryLibrarian
4fb41a4c35 1.196.3 2021-06-28 18:45:46 +02:00
DebaucheryLibrarian
12c0e8e828 Fixed login link in sidebar. 2021-06-28 18:45:38 +02:00
DebaucheryLibrarian
dce27e985e 1.196.2 2021-06-28 05:13:48 +02:00
DebaucheryLibrarian
0a343dfa98 Improved campaign component, added various banners. 2021-06-28 05:13:41 +02:00
DebaucheryLibrarian
729ca0f968 1.196.1 2021-06-28 02:50:10 +02:00
DebaucheryLibrarian
385dfb9f75 Removed affiliate table in favor of direct campaign URLs. 2021-06-28 02:50:06 +02:00
DebaucheryLibrarian
afbae24f43 1.196.0 2021-06-28 00:05:32 +02:00
DebaucheryLibrarian
eb7009832a Added rudimentary affiliate banner setup. Separated login and signup disable. Added various tag photos. 2021-06-28 00:05:24 +02:00
DebaucheryLibrarian
d1480da076 Added effective date column. Changed warning page theme. 2021-06-19 18:09:58 +02:00
DebaucheryLibrarian
10a2731caf 1.195.0 2021-06-13 16:49:42 +02:00
DebaucheryLibrarian
ab1329dd67 Updating entity ID for rescraped scenes with network entry IDs enabled. 2021-06-13 16:49:27 +02:00
DebaucheryLibrarian
e9a0700742 Improved upcoming update query. 2021-06-06 01:09:32 +02:00
DebaucheryLibrarian
3f473589ad Using bulk insert utility for alert notifications to prevent duplicate errors (fixed). 2021-06-04 03:22:40 +02:00
DebaucheryLibrarian
7a44c7aaaa 1.194.1 2021-06-04 03:10:47 +02:00
DebaucheryLibrarian
bed329cd8c Using bulk insert utility for alert notifications to prevent duplicate errors. 2021-06-04 03:10:41 +02:00
DebaucheryLibrarian
011bb4efa3 1.194.0 2021-06-02 03:27:37 +02:00
DebaucheryLibrarian
c979173422 Rescraping upcoming scenes. Fixed language and scene deep scraping for Dorcel scraper. 2021-06-02 03:27:32 +02:00
2154 changed files with 65732 additions and 16214 deletions

View File

@@ -2,7 +2,7 @@
"root": true,
"extends": ["airbnb-base", "plugin:vue/recommended"],
"parserOptions": {
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"ecmaVersion": 2019,
"sourceType": "module"
},
@@ -17,10 +17,11 @@
"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

3
.gitignore vendored
View File

@@ -2,9 +2,12 @@ node_modules/
dist/
log/
media/
html/
public/js/*
public/css/*
config/*
!config/default.js
assets/js/config/
!assets/js/config/default.js
*.heapprofile
*.heapsnapshot

2
.nvmrc
View File

@@ -1 +1 @@
14.15.4
16.13.0

View File

@@ -6,7 +6,7 @@
<div class="actor-header">
<h2 class="header-name">
<span v-if="actor.entity">{{ actor.name }} ({{ actor.entity.name }})</span>
<span v-else="">{{ actor.name }}</span>
<span v-else>{{ actor.name }}</span>
<Gender
:gender="actor.gender"
@@ -361,7 +361,10 @@
:available-actors="actor.actors"
/>
<Releases :releases="releases" />
<Releases
:releases="releases"
:done="done"
/>
<Pagination
:items-total="totalCount"
@@ -389,6 +392,8 @@ import Social from './social.vue';
import StashButton from '../stashes/button.vue';
async function fetchActor(scroll = true) {
this.done = false;
const { actor, releases, totalCount } = await this.$store.dispatch('fetchActorById', {
actorId: Number(this.$route.params.actorId),
limit: this.limit,
@@ -400,6 +405,7 @@ async function fetchActor(scroll = true) {
this.releases = releases;
this.totalCount = totalCount;
this.stashedBy = actor.stashes;
this.done = true;
if (this.$refs.filter && scroll) {
this.$refs.filter.$el.scrollIntoView();
@@ -463,6 +469,7 @@ export default {
return {
actor: null,
releases: null,
done: false,
totalCount: 0,
limit: 20,
pageTitle: null,

View File

@@ -1,216 +1,246 @@
<template>
<div class="actors">
<div
ref="content"
class="actors"
>
<nav
ref="filter"
class="filter"
ref="filters"
class="filters"
>
<ul class="genders nolist">
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'all', letter, pageNumber: 1 } }"
:class="{ selected: gender === 'all' }"
class="gender-link all"
>all</router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'female', letter, pageNumber: 1 } }"
: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', letter, pageNumber: 1 } }"
: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', letter, pageNumber: 1 } }"
: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', letter, pageNumber: 1 } }"
:class="{ selected: gender === 'other' }"
class="gender-link other"
replace
><Icon icon="question5" /></router-link>
</li>
</ul>
<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="letters nolist">
<li
v-for="letterX in letters"
:key="letterX"
class="letter"
>
<router-link
:to="{ name: 'actors', params: { gender, letter: letterX, pageNumber: 1 } }"
:class="{ selected: letterX === letter }"
class="letter-link"
replace
>{{ letterX || 'All' }}</router-link>
</li>
</ul>
</nav>
<ul class="filters-attributes nolist">
<li>
<Tooltip class="filter boobs">
<span
class="filter-trigger"
:class="{ enabled: ageRequired }"
><Icon icon="vcard" />Age</span>
<nav class="filter">
<ul class="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)"
<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)"
>
<input
v-model="dob"
:disabled="!dobRequired"
type="date"
class="input"
@change="updateFilters"
<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>
</div>
</template>
</Tooltip>
</li>
</template>
</Tooltip>
</li>
<li>
<Tooltip class="filter">
<span
class="filter-trigger boobs"
:class="{ enabled: boobSizeRequired || naturalBoobs !== 1 }"
><Icon icon="boobs" />Boobs</span>
<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"
<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)"
>
<span
class="toggle-label off"
@click="updateValue('naturalBoobs', 0)"
><Icon icon="leaf" /></span>
<template v-slot:start><Icon icon="boobs-small" /></template>
<template v-slot:end><Icon icon="boobs-big" /></template>
</RangeFilter>
<input
v-model.number="naturalBoobs"
class="toggle"
type="range"
min="0"
max="2"
@change="updateFilters"
<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>
<span
class="toggle-label on"
@click="updateValue('naturalBoobs', 2)"
><Icon icon="magic-wand2" /></span>
</span>
</div>
</template>
</Tooltip>
</li>
<input
v-model.number="naturalBoobs"
class="toggle"
type="range"
min="0"
max="2"
@change="updateFilters"
>
<li>
<Tooltip class="filter boobs">
<span
class="filter-trigger"
:class="{ enabled: heightRequired || weightRequired }"
><Icon icon="rulers" />Physique</span>
<span
class="toggle-label on"
@click="updateValue('naturalBoobs', 2)"
><Icon icon="magic-wand2" /></span>
</span>
</div>
</template>
</Tooltip>
</li>
<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>
<li>
<Tooltip class="filter boobs">
<span
class="filter-trigger"
:class="{ enabled: heightRequired || weightRequired }"
><Icon icon="rulers" />Physique</span>
<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>
</ul>
<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 class="tiles">
<div
ref="tiles"
class="tiles"
>
<Actor
v-for="actor in actors"
:key="`actor-${actor.id}`"
@@ -235,11 +265,14 @@ 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 = 'ABCDEFGHZ'.split('');
const boobSizes = 'ABCDEFGHIJKZ'.split('');
const topCountries = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
function updateFilters() {
this.$router.push({
@@ -247,15 +280,16 @@ function updateFilters() {
params: {
pageNumber: 1,
gender: this.gender,
letter: this.letter,
},
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,
},
});
}
@@ -271,29 +305,39 @@ function updateValue(prop, value, load = true) {
async function fetchActors(scroll) {
const curatedGender = this.gender.replace('trans', 'transsexual');
const { actors, totalCount } = await this.$store.dispatch('fetchActors', {
const { actors, countries, totalCount } = await this.$store.dispatch('fetchActors', {
limit: this.limit,
pageNumber: Number(this.$route.params.pageNumber) || 1,
letter: this.letter.replace('all', ''),
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.filter?.scrollIntoView();
this.$refs.tiles?.scrollIntoView();
}
}
function letter() {
return this.$route.params.letter || 'all';
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() {
@@ -302,8 +346,7 @@ function gender() {
async function route(to, from) {
const scroll = to.params.pageNumber !== from.params.pageNumber
|| to.params.gender !== from.params.gender
|| to.params.letter !== from.params.letter;
|| to.params.gender !== from.params.gender;
await this.fetchActors(scroll);
}
@@ -318,17 +361,21 @@ export default {
components: {
Actor,
Checkbox,
Countries,
Gender,
RangeFilter,
SearchBar,
Pagination,
},
data() {
return {
actors: [],
countries: [],
topCountries: [],
countryQuery: null,
pageTitle: null,
totalCount: 0,
limit: 50,
letters: ['all'].concat(Array.from({ length: 26 }, (value, index) => String.fromCharCode(index + 97).toUpperCase())),
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'),
@@ -336,6 +383,7 @@ export default {
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,
@@ -344,8 +392,8 @@ export default {
};
},
computed: {
letter,
gender,
filteredCountries,
},
watch: {
$route: route,
@@ -393,42 +441,53 @@ export default {
.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;
}
.filter {
display: flex;
justify-content: center;
align-items: center;
padding: 0 1rem;
margin: 0 0 1rem 0;
.search {
width: 0;
justify-content: flex-end;
flex-grow: 1;
box-sizing: border-box;
padding: 0 1rem;
}
&:first-child {
margin: 1rem 0;
}
.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;
border-right: solid 1px var(--shadow-hint);
margin: 0 1rem 0 0;
}
.letter,
.gender {
display: inline-block;
}
.letter-link,
.gender-link {
width: 2.5rem;
height: 2.5rem;
@@ -478,7 +537,8 @@ export default {
color: var(--shadow);
font-weight: bold;
.icon {
.icon,
.flag {
fill: var(--shadow);
width: 1rem;
height: 1rem;
@@ -661,10 +721,45 @@ export default {
}
}
@media(max-width: $breakpoint) {
.genders {
flex-direction: column;
@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) {

View 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>

View File

@@ -3,7 +3,7 @@
v-if="actor"
class="actor"
>
<router-link
<RouterLink
:to="{ name: 'actor', params: { actorId: actor.id, actorSlug: actor.slug } }"
class="link"
>
@@ -15,7 +15,7 @@
class="name"
>{{ actor.name }}</span>
<router-link
<RouterLink
v-if="actor.entity"
v-tooltip="actor.entity.name"
:to="{ name: actor.entity.type, params: { entitySlug: actor.entity.slug, range: 'new', pageNumber: 1 } }"
@@ -25,7 +25,7 @@
:src="`/img/logos/${actor.entity.slug}/favicon_dark.png`"
class="favicon-icon"
>
</router-link>
</RouterLink>
<Icon
v-if="alias"
@@ -60,19 +60,26 @@
></span>
<Icon
v-show="favorited"
v-show="(!stash || stash.primary) && favorited"
icon="heart7"
class="stash stashed"
@click.prevent.native="unstashActor"
/>
<Icon
v-show="favorited === false"
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"
>
@@ -122,7 +129,7 @@
/>
</span>
</div>
</router-link>
</RouterLink>
</div>
</template>
@@ -145,12 +152,14 @@ async function stashActor() {
}
async function unstashActor() {
this.favorited = false;
if (!this.stash || this.stash.primary) {
this.favorited = false;
}
try {
await this.$store.dispatch('unstashActor', {
actorId: this.actor.id,
stashId: this.$store.getters.favorites.id,
stashId: this.stash?.id || this.$store.getters.favorites.id,
});
this.$emit('stash', false);
@@ -176,6 +185,10 @@ export default {
type: Object,
default: null,
},
stash: {
type: Object,
default: null,
},
},
emits: ['stash'],
data() {
@@ -201,7 +214,7 @@ export default {
display: inline-block;
position: relative;
box-shadow: 0 0 3px var(--darken-weak);
background: var(--background);
background: var(--info);
overflow: hidden;
&::before {
@@ -209,6 +222,11 @@ export default {
display: inline-block;
padding-bottom: 150%;
}
&:hover .unstashed,
&:hover .unstash {
fill: var(--lighten);
}
}
.link {
@@ -219,12 +237,8 @@ export default {
position: absolute;
top: 0;
left: 0;
color: var(--text);
color: var(--text-light);
text-decoration: none;
&:hover {
color: var(--primary);
}
}
.handle {
@@ -235,7 +249,8 @@ export default {
box-shadow: 0 0 3px var(--shadow);
.name {
padding: .5rem;
padding: .25rem .5rem;
font-size: .9rem;
}
.alias {
@@ -298,8 +313,8 @@ export default {
}
.stash {
width: 1.25rem;
height: 1.25rem;
width: 1.5rem;
height: 1.5rem;
padding: .25rem .25rem .5rem .5rem;
position: absolute;
top: 0;
@@ -307,10 +322,15 @@ export default {
fill: var(--lighten-weak);
filter: drop-shadow(0 0 2px var(--darken));
&:hover,
&:hover.unstashed,
&.stashed {
fill: var(--primary);
}
&:hover.unstash {
fill: var(--text-light);
filter: drop-shadow(0 0 2px var(--darken-weak));
}
}
.details {

View File

@@ -11,43 +11,56 @@
/>
</div>
<div
class="album-items"
:class="{ portrait }"
>
<div class="album-body">
<div
v-for="item in albumItems"
:key="item.id"
class="item-container"
v-if="entity || tag"
class="campaign-container"
>
<a
:href="getPath(item, null, { local })"
class="item-link"
target="_blank"
<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"
>
<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"
<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" />
<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>
<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>
<span
v-else-if="comments && item.title"
class="item-comment"
>{{ item.title }}</span>
</a>
</div>
</div>
</div>
</div>
@@ -56,6 +69,7 @@
<script>
import Logo from './logo.vue';
import Campaign from '../campaigns/campaign.vue';
function albumItems() {
return this.items
@@ -69,6 +83,7 @@ function albumItems() {
export default {
components: {
Logo,
Campaign,
},
props: {
items: {
@@ -91,6 +106,14 @@ export default {
type: Boolean,
default: true,
},
entity: {
type: Object,
default: null,
},
tag: {
type: Object,
default: null,
},
},
computed: {
albumItems,
@@ -146,15 +169,30 @@ export default {
}
}
.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;
overflow-y: auto;
&.portrait {
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));

View File

@@ -46,7 +46,7 @@
/>
</li>
<template v-slot:tooltip>
<template #tooltip>
<Search
content="actors"
@select="actor => addActor(actor)"
@@ -88,7 +88,7 @@
/>
</li>
<template v-slot:tooltip>
<template #tooltip>
<Search
content="tags"
:defaults="['anal', 'blowbang', 'mfm', 'dp', 'gangbang', 'airtight']"
@@ -126,7 +126,7 @@
/>
</div>
<template v-slot:tooltip>
<template #tooltip>
<Search
label="Search channels"
content="entities"
@@ -181,7 +181,7 @@
/>
</li>
<template v-slot:tooltip>
<template #tooltip>
<Search
content="stashes"
@select="stash => addStash(stash)"
@@ -194,6 +194,7 @@
<div class="dialog-actions right">
<button
:disabled="actors.length === 0 && tags.length === 0 && !entity"
type="submit"
class="button button-primary"
>Add alert</button>
@@ -210,19 +211,19 @@ 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),
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),
stashes: this.stashes.map((stash) => stash.id),
});
this.$emit('close', true);
}
function addActor(actor) {
if (!this.actors.some(selectedActor => selectedActor.id === actor.id)) {
if (!this.actors.some((selectedActor) => selectedActor.id === actor.id)) {
this.actors = this.actors.concat(actor);
}
@@ -235,7 +236,7 @@ function addEntity(entity) {
}
function addTag(tag) {
if (!this.tags.some(selectedTag => selectedTag.id === tag.id)) {
if (!this.tags.some((selectedTag) => selectedTag.id === tag.id)) {
this.tags = this.tags.concat(tag);
}
@@ -243,7 +244,7 @@ function addTag(tag) {
}
function removeActor(actor) {
this.actors = this.actors.filter(listedActor => listedActor.id !== actor.id);
this.actors = this.actors.filter((listedActor) => listedActor.id !== actor.id);
}
function removeEntity() {
@@ -251,11 +252,11 @@ function removeEntity() {
}
function removeTag(tag) {
this.tags = this.tags.filter(listedTag => listedTag.id !== tag.id);
this.tags = this.tags.filter((listedTag) => listedTag.id !== tag.id);
}
function addStash(stash) {
if (!this.stashes.some(selectedStash => selectedStash.id === stash.id)) {
if (!this.stashes.some((selectedStash) => selectedStash.id === stash.id)) {
this.stashes = this.stashes.concat(stash);
}
@@ -263,7 +264,7 @@ function addStash(stash) {
}
function removeStash(stash) {
this.stashes = this.stashes.filter(listedStash => listedStash.id !== stash.id);
this.stashes = this.stashes.filter((listedStash) => listedStash.id !== stash.id);
}
export default {
@@ -273,6 +274,7 @@ export default {
Entity,
Search,
},
emits: ['close'],
data() {
return {
actors: [],
@@ -284,7 +286,6 @@ export default {
availableStashes: this.$store.state.auth.user.stashes,
};
},
emits: ['close'],
methods: {
addActor,
addAlert,

View File

@@ -43,11 +43,12 @@
<script>
async function search() {
if (this.content === 'actors') {
this.results = await this.$store.dispatch('searchActors', {
const { actors } = await this.$store.dispatch('fetchActors', {
query: this.query,
minLength: 1,
limit: 10,
});
this.results = actors;
}
if (this.content === 'entities') {

View File

@@ -35,10 +35,11 @@
class="button button-primary"
>Log in</button>
<router-link
<RouterLink
v-if="$store.state.auth.signup"
:to="{ name: 'signup', query: { ref: $route.query.ref } }"
class="link link-external signup"
>Sign up</router-link>
>Sign up</RouterLink>
</template>
</form>
</template>
@@ -65,7 +66,7 @@ async function login() {
}
function mounted() {
if (!this.$store.state.auth.enabled) {
if (!this.$store.state.auth.login) {
this.$router.replace({ name: 'not-found' });
}
}

View File

@@ -43,10 +43,11 @@
class="button button-primary"
>Sign up</button>
<router-link
<RouterLink
v-if="$store.state.auth.login"
:to="{ name: 'login', query: { ref: $route.query.ref } }"
class="link link-external login"
>Log in</router-link>
>Log in</RouterLink>
</template>
</form>
</template>
@@ -74,7 +75,7 @@ async function signup() {
}
function mounted() {
if (!this.$store.state.auth.enabled) {
if (!this.$store.state.auth.signup) {
this.$router.replace({ name: 'not-found' });
}
}

View 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>

54
assets/components/container/container.vue Normal file → Executable file
View File

@@ -8,35 +8,40 @@
/>
<transition name="slide">
<Sidebar
v-if="showSidebar"
@toggle-sidebar="(state) => toggleSidebar(state)"
@show-filters="(state) => toggleFilters(state)"
/>
<Sidebar v-if="showSidebar" />
</transition>
<Header
@toggle-sidebar="(state) => toggleSidebar(state)"
@show-filters="(state) => toggleFilters(state)"
/>
<Header />
<p
v-if="config.showDisclaimer"
class="disclaimer"
>{{ config.disclaimer }}</p>
v-html="config.disclaimer"
/>
<p
v-if="config.showAnnouncement"
class="announcement"
v-html="config.announcement"
/>
<div
ref="content"
class="content"
@scroll="scroll"
>
<router-view @scroll="scrollToTop" />
<RouterView @scroll="scrollToTop" />
</div>
<Filters
v-if="showFilters"
@close="toggleFilters(false)"
/>
<Settings
v-if="showSettings"
@close="toggleSettings(false)"
/>
</div>
</template>
@@ -45,6 +50,7 @@ import Warning from './warning.vue';
import Header from '../header/header.vue';
import Sidebar from '../sidebar/sidebar.vue';
import Filters from '../filters/filters.vue';
import Settings from '../settings/settings.vue';
function toggleSidebar(state) {
this.showSidebar = typeof state === 'boolean' ? state : !this.showSidebar;
@@ -55,6 +61,11 @@ function toggleFilters(state) {
this.showSidebar = false;
}
function toggleSettings(state) {
this.showSettings = state;
this.showSidebar = false;
}
async function setConsent(consent, includeQueer) {
if (consent) {
this.showWarning = false;
@@ -62,7 +73,7 @@ async function setConsent(consent, includeQueer) {
}
if (includeQueer) {
this.$store.dispatch('setTagFilter', this.$store.state.ui.tagFilter.filter(tag => !['gay', 'bisexual', 'transsexual'].includes(tag)));
this.$store.dispatch('setTagFilter', this.$store.state.ui.tagFilter.filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag)));
return;
}
@@ -88,6 +99,9 @@ function scrollToTop() {
function mounted() {
document.addEventListener('click', this.blur);
window.addEventListener('resize', this.resize);
this.events.on('toggleSettings', this.toggleSettings);
this.events.on('toggleSidebar', this.toggleSidebar);
}
function beforeUnmount() {
@@ -101,12 +115,15 @@ export default {
Sidebar,
Warning,
Filters,
Settings,
},
data() {
return {
showSidebar: false,
showWarning: localStorage.getItem('consent') !== window.env.sessionId,
showFilters: false,
showSettings: false,
selected: null,
};
},
mounted,
@@ -114,6 +131,7 @@ export default {
methods: {
toggleSidebar,
toggleFilters,
toggleSettings,
setConsent,
blur,
resize,
@@ -183,13 +201,21 @@ export default {
</style>
<style lang="scss" scoped>
.disclaimer {
.disclaimer,
.announcement {
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;
}
.disclaimer {
background: var(--warn);
}
.announcement {
background: var(--notice);
}
</style>

View File

@@ -67,12 +67,13 @@ export default {
justify-content: center;
position: absolute;
z-index: 10;
background: var(--darken-censor);
background: var(--background-censor);
backdrop-filter: blur(.25rem);
overflow-y: auto;
}
.warning {
color: var(--text-light);
color: var(--text);
width: 50rem;
max-height: 100%;
max-width: 100%;
@@ -87,8 +88,8 @@ export default {
.title {
display: block;
font-size: 2rem;
margin: 1rem 0 0 0;
color: var(--text-light);
margin: 1rem 0;
color: var(--text);
text-align: center;
}
@@ -113,7 +114,7 @@ export default {
.rules {
margin: 0 0 0 1rem;
text-align: left;
text-shadow: 0 0 3px var(--darken-extreme);
text-shadow: 0 0 3px var(--highlight-strong);
}
.rule {
@@ -134,16 +135,16 @@ export default {
align-items: center;
justify-content: center;
border: none;
border-radius: .25rem;
padding: 0;
position: relative;
border-radius: 1rem;
color: var(--lighten-strong);
cursor: pointer;
font-size: 1.5rem;
text-decoration: none;
transition: border .5s ease;
transition: border-radius .2s ease;
&.leave {
color: var(--shadow-strong);
flex-direction: row;
padding: 1rem;
@@ -151,7 +152,11 @@ export default {
width: 1.5rem;
height: 1.5rem;
margin: 0 1rem 0 0;
fill: var(--lighten);
fill: var(--shadow);
}
&:hover {
color: var(--text);
}
}
@@ -161,6 +166,7 @@ export default {
&.straight,
&.queer {
color: var(--lighten-strong);
background: var(--darken-censor);
&:before {
@@ -169,10 +175,18 @@ export default {
height: calc(100% + .25rem);
position: absolute;
z-index: -1;
border-radius: 1.1rem;
filter: blur(.25rem);
transition: filter .2s ease;
}
&:hover {
color: var(--text-light);
border-radius: 0;
.button-sub {
color: var(--text-light);
}
}
}
&.straight:before {
@@ -188,14 +202,8 @@ export default {
}
&:hover {
color: var(--text-light);
.button-sub {
color: var(--lighten-strong);
}
.icon {
fill: var(--text-light);
fill: var(--text);
}
&.straight:before,
@@ -225,7 +233,7 @@ export default {
}
.preferences {
color: var(--lighten);
color: var(--shadow);
display: block;
padding: .5rem 0 1rem 0;
text-align: center;
@@ -234,11 +242,11 @@ export default {
@media(max-width: $breakpoint) {
.title {
font-size: 1.5rem;
font-size: 1.75rem;
}
.logo {
width: 5rem;
width: 5.75rem;
margin: 0 .5rem 0 0;
}
}

View File

@@ -116,6 +116,7 @@ export default {
::v-deep(.dialog-body) {
padding: 1rem;
flex-grow: 1;
overflow-y: auto;
}
::v-deep(.dialog-section:not(:last-child)) {

View File

@@ -5,7 +5,7 @@
>
<div class="info">
<a
:href="entity.url"
:href="entityUrl"
target="_blank"
rel="noopener"
class="link link-child"
@@ -51,7 +51,7 @@
>{{ tag.name }}</li>
</ul>
<router-link
<RouterLink
v-if="entity.parent"
:to="`/${entity.parent.type}/${entity.parent.slug}`"
class="link link-parent"
@@ -72,7 +72,9 @@
v-else
class="name parent-name"
>{{ entity.parent.name }}</h3>
</router-link>
<Icon icon="device_hub" />
</RouterLink>
</div>
<div
@@ -94,6 +96,13 @@
/>
</Scroll>
<div class="campaign-container">
<Campaign
:entity="entity"
:min-ratio="3"
/>
</div>
<FilterBar
ref="filter"
:fetch-releases="fetchEntity"
@@ -102,7 +111,10 @@
/>
<div class="releases">
<Releases :releases="entity.releases" />
<Releases
:releases="entity.releases"
:done="done"
/>
<Pagination
:items-total="totalCount"
@@ -122,8 +134,12 @@ 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,
@@ -137,6 +153,17 @@ async function fetchEntity(scroll = true) {
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();
}
@@ -160,14 +187,17 @@ export default {
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: {
@@ -192,7 +222,7 @@ export default {
}
.link {
max-width: 20rem;
max-width: 15rem;
display: flex;
align-items: center;
box-sizing: border-box;
@@ -201,6 +231,8 @@ export default {
}
.link-child {
flex-shrink: 0;
.icon {
fill: var(--lighten);
margin: 0 0 0 1rem;
@@ -213,7 +245,15 @@ export default {
.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 {
@@ -223,7 +263,7 @@ export default {
}
.logo-child {
height: 2rem;
height: 3rem;
}
.logo-parent {
@@ -232,6 +272,7 @@ export default {
.favicon {
height: 1rem;
flex-shrink: 0;
}
.content-inner {
@@ -243,6 +284,12 @@ export default {
background: var(--profile);
}
.campaign-container {
background: var(--background-dim);
text-align: center;
padding: .5rem;
}
.releases {
flex-grow: 1;
}

View File

@@ -4,39 +4,49 @@
:title="entity.name"
class="tile"
>
<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)"
>
<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-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
<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
:src="`/img/logos/${entity.slug}/thumbs/${entity.slug}.png`"
:alt="entity.name"
loading="lazy"
class="logo"
@load="$emit('load', $event)"
>
</template>
class="name"
>{{ entity.name }}</span>
</div>
<span
v-else
class="name"
>{{ entity.name }}</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>
@@ -59,15 +69,28 @@ export default {
height: 100%;
background: var(--tile);
display: flex;
flex-direction: column;
flex-shrink: 0;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: .5rem 1rem;
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 {
@@ -75,6 +98,7 @@ export default {
max-height: 100%;
display: flex;
align-items: center;
align-self: center;
}
.name {
@@ -82,4 +106,20 @@ export default {
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>

View File

@@ -1,41 +1,33 @@
<template>
<div class="filter-bar noselect">
<div class="sort">
<router-link
:to="{ params: { range: 'latest', pageNumber: 1 }, query: $route.query }"
:class="{ active: $route.name === 'latest' || range === 'latest' }"
<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"
>Latest</router-link>
<router-link
:to="{ params: { range: 'upcoming', pageNumber: 1 }, query: $route.query }"
:class="{ active: $route.name === 'upcoming' || range === 'upcoming' }"
class="range-button"
>Upcoming</router-link>
<router-link
:to="{ params: { range: 'new', pageNumber: 1 }, query: $route.query }"
:class="{ active: $route.name === 'new' || range === 'new' }"
class="range-button"
>New</router-link>
>{{ 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"
:filter="filter"
:available-actors="availableActors"
/>
<ChannelFilter
class="filters-filter"
:filter="filter"
:available-channels="availableChannels"
/>
<TagFilter
class="filters-filter"
:filter="filter"
:available-tags="availableTags"
/>
</div>
@@ -49,10 +41,6 @@ import ActorFilter from './actor-filter.vue';
import ChannelFilter from './channel-filter.vue';
import TagFilter from './tag-filter.vue';
function filter(state) {
return state.ui.filter;
}
function range() {
return this.$route.params.range;
}
@@ -88,6 +76,10 @@ export default {
type: Boolean,
default: false,
},
showTotal: {
type: Boolean,
default: true,
},
itemsTotal: {
type: Number,
default: 0,
@@ -96,6 +88,10 @@ export default {
type: Number,
default: 10,
},
ranges: {
type: Array,
default: () => ['latest', 'upcoming', 'new'],
},
availableTags: {
type: Array,
default: () => [],
@@ -111,7 +107,6 @@ export default {
},
computed: {
...mapState({
filter,
range,
batch,
}),
@@ -126,6 +121,43 @@ export default {
<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);
@@ -246,6 +278,10 @@ export default {
.filters-filter:last-child .filter {
padding: .5rem 0 .5rem .5rem;
}
.total {
display: none;
}
}
</style>
@@ -267,43 +303,6 @@ export default {
}
}
::v-deep(.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);
}
}
}
.filters {
flex-shrink: 0;
}
@@ -324,6 +323,7 @@ export default {
text-decoration: none;
border: solid 1px transparent;
border-bottom: none;
text-transform: capitalize;
&:hover:not(.active) {
color: var(--shadow-strong);
@@ -356,4 +356,11 @@ export default {
margin: 0 1rem 0 0;
}
}
.total {
flex-shrink: 0;
padding: 0 1rem;
color: var(--shadow);
font-size: .8rem;
}
</style>

View File

@@ -14,7 +14,7 @@
>Tags</div>
</div>
<template v-slot:tooltip>
<template #tooltip>
<div
class="filter-options"
@click.stop
@@ -70,7 +70,7 @@
<script>
function getNewRange(tag) {
if (this.selectedTags.includes(tag)) {
return { tags: this.selectedTags.filter(selectedTag => selectedTag !== tag).join(',') || undefined };
return { tags: this.selectedTags.filter((selectedTag) => selectedTag !== tag).join(',') || undefined };
}
return { tags: this.selectedTags.concat(tag).join(',') };
@@ -82,10 +82,6 @@ function selectedTags() {
export default {
props: {
filter: {
type: Array,
default: () => [],
},
compact: {
type: Boolean,
default: false,

View File

@@ -16,7 +16,7 @@
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/actors"
:to="{ name: 'actors', params: { pageNumber: 1 } }"
custom
>
<a
@@ -31,7 +31,7 @@
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/channels"
:to="{ name: 'channels' }"
custom
>
<a
@@ -46,7 +46,7 @@
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/tags"
:to="{ name: 'tags' }"
custom
>
<a
@@ -61,7 +61,7 @@
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/movies"
:to="{ name: 'movies', params: { range: 'latest', pageNumber: 1 } }"
custom
>
<a

View File

@@ -1,28 +1,26 @@
<template>
<div class="menu">
<ul class="menu-items noselect">
<router-link
v-if="auth && me"
<RouterLink
v-if="login && me"
:to="{ name: 'user', params: { username: me.username } }"
class="menu-username"
>{{ me.username }}</router-link>
>{{ me.username }}</RouterLink>
<router-link
v-else-if="auth"
<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
</router-link>
<li
v-if="auth && me"
class="menu-item"
@click.stop="$store.dispatch('logout')"
>
<Icon icon="exit2" />Log out
</li>
</RouterLink>
<li
v-show="!sfw"
@@ -74,6 +72,14 @@
>
<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>
@@ -89,8 +95,16 @@ function theme(state) {
return state.ui.theme;
}
function auth(state) {
return state.auth.enabled;
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) {
@@ -108,11 +122,13 @@ function setSfw(enabled) {
export default {
computed: {
...mapState({
auth,
login,
signup,
sfw,
theme,
me,
}),
favorites,
},
emits: ['showFilters'],
methods: {

View File

@@ -8,9 +8,13 @@
:items-total="totalCount"
:items-per-page="limit"
:content="$refs.content"
:show-total="false"
/>
<Releases :releases="releases" />
<Releases
:releases="releases"
:done="done"
/>
<Pagination
v-if="totalCount > 0"
@@ -30,6 +34,8 @@ import Releases from '../releases/releases.vue';
import Pagination from '../pagination/pagination.vue';
async function fetchReleases(scroll = true) {
this.done = false;
const { releases, totalCount } = await this.$store.dispatch('fetchReleases', {
limit: this.limit,
range: this.$route.params.range,
@@ -38,6 +44,7 @@ async function fetchReleases(scroll = true) {
this.totalCount = totalCount;
this.releases = releases;
this.done = true;
if (scroll) {
this.$refs.filter?.$el.scrollIntoView();
@@ -64,6 +71,7 @@ export default {
limit: 30,
totalCount: 0,
from: null,
done: false,
};
},
watch: {

View 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>

View File

@@ -1,44 +1,26 @@
<template>
<div class="networks">
<form
class="search"
@submit.prevent="searchEntities"
>
<input
v-model="query"
:placeholder="`Find ${channelCount} channels in ${entities.length} networks`"
class="query"
@input="searchEntities"
<div class="content-inner">
<SearchBar
:placeholder="`Search ${channelCount} channels in ${entities.length} networks`"
:eager="true"
/>
<span
v-if="done && entities.length === 0"
class="empty"
>No results for "{{ $route.query.query }}"</span>
<div
v-else
class="entity-tiles"
>
<button
type="submit"
class="search-button"
><Icon icon="search" /></button>
</form>
<div
v-if="query.length > 0"
class="entity-tiles"
>
<Entity
v-for="entity in searchResults"
:key="`${entity.type}-tile-${entity.slug}`"
:entity="entity"
/>
<span v-if="searchResults.length === 0">No results for "{{ query }}"</span>
</div>
<div
v-else
class="entity-tiles"
>
<Entity
v-for="entity in entities"
:key="`entity-tile-${entity.slug}`"
:entity="entity"
/>
<Entity
v-for="entity in entities"
:key="entity.parent ? `entity-tile-${entity.parent.slug}-${entity.slug}` : `entity-tile-${entity.slug}`"
:entity="entity"
/>
</div>
</div>
<Footer />
@@ -47,21 +29,39 @@
<script>
import Entity from '../entities/tile.vue';
import SearchBar from '../search/bar.vue';
async function searchEntities() {
this.searchResults = await this.$store.dispatch('searchEntities', {
query: this.query,
limit: 50,
});
}
async function fetchEntities() {
if (this.$route.query.query) {
await this.searchEntities();
return;
}
this.done = false;
async function mounted() {
this.entities = await this.$store.dispatch('fetchEntities', {
type: 'network',
entitySlugs: [],
});
this.pageTitle = 'Networks';
this.done = true;
}
async function searchEntities() {
this.done = false;
this.entities = await this.$store.dispatch('searchEntities', {
query: this.$route.query.query,
limit: 20,
});
this.done = true;
}
async function mounted() {
this.pageTitle = 'Channels';
await this.fetchEntities();
}
function channelCount() {
@@ -71,20 +71,24 @@ function channelCount() {
export default {
components: {
Entity,
SearchBar,
},
data() {
return {
query: '',
done: false,
pageTitle: null,
entities: [],
searchResults: [],
};
},
computed: {
channelCount,
},
watch: {
$route: fetchEntities,
},
mounted,
methods: {
fetchEntities,
searchEntities,
},
};
@@ -94,49 +98,17 @@ export default {
@import 'theme';
.networks {
padding: 0 1rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.content-inner {
padding: 0 1rem;
}
.search {
display: flex;
width: 100%;
max-width: 40rem;
}
.query {
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);
margin: 1rem 0;
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);
}
}
margin: 1rem 0 0 0;
}
.entity-tiles {
@@ -150,6 +122,14 @@ export default {
}
}
.empty {
display: block;
margin: 1rem 0;
color: var(--shadow);
font-size: 1.25rem;
font-weight: bold;
}
@media(max-width: $breakpoint2) {
.entity-tiles {
grid-gap: .5rem;

View File

@@ -2,7 +2,7 @@
<div class="media-container">
<div
class="media"
:class="{ center: release.photos.length < 2 }"
:class="{ center: release.photos.length < 2, preview: !me }"
>
<div
v-if="release.trailer || release.teaser"
@@ -38,15 +38,30 @@
class="item trailer"
>
<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
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"
@@ -112,9 +127,9 @@
v-if="!me"
class="item-container item-more"
><router-link
:to="{ name: 'signup', query: { ref: $route.path } }"
:to="{ name: 'login', query: { ref: $route.path } }"
class="link"
>Sign up</router-link>&nbsp;for more photos, trailers and features!</div>
>Log in</router-link>&nbsp;for more photos, trailers and features!</div>
</div>
</div>
</template>
@@ -217,11 +232,16 @@ export default {
margin: 0 auto;
}
.poster-link {
.poster-links {
display: flex;
position: absolute;
top: .5rem;
right: .5rem;
top: 0;
right: 0;
}
.poster-link {
transition: opacity .1s ease;
padding: .5rem;
.icon {
width: 1.5rem;
@@ -234,6 +254,14 @@ export default {
opacity: 0;
}
&:first-child {
padding-left: .75rem;
}
&:last-child {
padding-right: .75rem;
}
&:hover {
cursor: pointer;
opacity: 1;
@@ -337,7 +365,7 @@ export default {
}
@media(max-width: $breakpoint-micro) {
.media.center {
.media.center.preview {
flex-direction: column;
}

View File

@@ -3,7 +3,7 @@
<Details :release="movie" />
<div class="movie">
<router-link
<RouterLink
:to="{ name: 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }"
class="cover"
>
@@ -13,15 +13,42 @@
:style="{ 'background-image': getBgPath(movie.covers[0], 'lazy') }"
loading="lazy"
>
</router-link>
<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">
<router-link
<RouterLink
:to="{ name: 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }"
class="title-link"
>
<h3 class="title">{{ movie.title }}</h3>
</router-link>
</RouterLink>
<ul
class="actors nolist"
@@ -31,10 +58,10 @@
v-for="actor in movie.actors"
:key="`tag-${movie.id}-${actor.id}`"
class="actor"
><router-link
><RouterLink
:to="`/actor/${actor.id}/${actor.slug}`"
class="actor-link"
>{{ actor.name }}</router-link></li>
>{{ actor.name }}</RouterLink></li>
</ul>
<ul
@@ -45,10 +72,10 @@
v-for="tag in movie.tags"
:key="`tag-${movie.id}-${tag.id}`"
class="tag"
><router-link
><RouterLink
:to="`/tag/${tag.slug}`"
class="tag-link"
>{{ tag.name }}</router-link></li>
>{{ tag.name }}</RouterLink></li>
</ul>
</div>
</div>
@@ -58,6 +85,38 @@
<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;
}
@@ -71,10 +130,23 @@ export default {
type: Object,
default: null,
},
stash: {
type: Object,
default: null,
},
},
data() {
return {
favorited: this.movie.isFavorited,
};
},
computed: {
sfw,
},
methods: {
stashMovie,
unstashMovie,
},
};
</script>
@@ -87,10 +159,14 @@ export default {
background: var(--background);
box-shadow: 0 0 3px var(--darken-weak);
font-size: 0;
&:hover .unstashed,
&:hover .unstash {
fill: var(--lighten);
}
}
.movie {
height: 16rem;
display: flex;
}
@@ -100,17 +176,30 @@ export default {
}
.cover {
height: 100%;
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%;
max-width: 12rem;
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 {
@@ -179,9 +268,31 @@ export default {
}
}
@media(max-width: $breakpoint) {
.movie {
.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 */

View File

@@ -1,13 +1,31 @@
<template>
<div class="movies">
<div class="content-inner">
<div class="tiles">
<SearchBar :placeholder="`Search ${totalCount} movies`" />
<TagFilter
class="filters-filter"
:filter="filter"
:available-tags="availableTags"
/>
<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 />
@@ -16,27 +34,69 @@
<script>
import MovieTile from './movie-tile.vue';
import SearchBar from '../search/bar.vue';
import Pagination from '../pagination/pagination.vue';
import TagFilter from '../filters/tag-filter.vue';
async function fetchMovies() {
if (this.$route.query.query) {
await this.searchMovies();
return;
}
async function mounted() {
const { movies, totalCount } = await this.$store.dispatch('fetchMovies', {
limit: 30,
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,
TagFilter,
},
data() {
return {
movies: [],
totalCount: 0,
limit: 20,
};
},
watch: {
$route: fetchMovies,
},
mounted,
methods: {
fetchMovies,
searchMovies,
},
};
</script>
@@ -49,14 +109,22 @@ export default {
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;
padding: 1rem 0;
}
@media(max-width: $breakpoint) {
@media(max-width: $breakpoint-kilo) {
.tiles {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}

View File

@@ -39,7 +39,10 @@
<h2
v-if="release.title"
class="title"
>{{ release.title }}</h2>
>
{{ release.title }}
<template v-if="release.movies?.[0]?.title && /^scene \d+$/i.test(release.title)"><span class="title-composed">&nbsp;from&nbsp;</span>{{ release.movies[0].title }}</template>
</h2>
<h2
v-else-if="release.actors.length > 0"
@@ -275,7 +278,7 @@ function bannerBackground() {
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));
|| (this.release.actors.length > 0 ? `${this.release.actors.map((actor) => actor.name).join(', ')} for ${this.release.entity.name}` : null));
}
function showAlbum() {

View File

@@ -5,8 +5,10 @@
class="heading"
><span class="range">{{ range }}</span> releases for '{{ context }}'</h3>
<Ellipsis v-if="!done" />
<ul
v-if="releases.length > 0"
v-else-if="releases.length > 0"
:key="sfw"
class="nolist tiles"
>
@@ -18,13 +20,14 @@
:release="release"
:referer="referer"
:index="index"
:stash="stash"
@stash="isStashed => $emit('stash', isStashed)"
/>
</li>
</ul>
<span
v-if="releases.length === 0 && range !== 'all'"
v-else-if="releases.length === 0 && range !== 'all'"
class="empty"
>No {{ range }} releases</span>
@@ -36,6 +39,7 @@
</template>
<script>
import Ellipsis from '../loading/ellipsis.vue';
import SceneTile from './scene-tile.vue';
function range() {
@@ -48,6 +52,7 @@ function sfw() {
export default {
components: {
Ellipsis,
SceneTile,
},
props: {
@@ -59,10 +64,18 @@ export default {
type: String,
default: null,
},
done: {
type: Boolean,
default: true,
},
referer: {
type: String,
default: null,
},
stash: {
type: Object,
default: null,
},
},
emits: ['stash'],
computed: {
@@ -113,10 +126,29 @@ export default {
font-weight: bold;
}
@media(max-width: $breakpoint-mega) {
.tiles {
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-fill, minmax(16rem, 1fr));
}
}
@media(max-width: $breakpoint-micro) {
.tiles {
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
}
}
</style>

View File

@@ -46,18 +46,25 @@
><Icon icon="blocked" />No thumbnail available</div>
<Icon
v-show="favorited"
v-show="(!stash || stash.primary) && favorited"
icon="heart7"
class="stash stashed"
@click.prevent.native="unstashScene"
/>
<Icon
v-show="favorited === false"
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>
@@ -78,7 +85,9 @@
v-tooltip.bottom="release.title"
:title="release.title"
class="title"
>{{ release.title }}</h3>
>
{{ release.title }}<template v-if="release.movies?.[0]?.title && /^scene \d+$/i.test(release.title)"><span class="title-composed">&nbsp;from&nbsp;</span>{{ release.movies[0].title }}</template>
</h3>
<h3
v-else-if="release.actors.length > 0"
@@ -104,21 +113,22 @@
:key="actor.id"
class="actor"
>
<router-link
<RouterLink
:to="{ name: 'actor', params: { actorId: actor.id, actorSlug: actor.slug } }"
:class="{ [actor.gender]: !!actor.gender }"
class="actor-link"
>{{ actor.name }}</router-link>
>{{ actor.name }}</RouterLink>
</li>
</ul>
</span>
<div class="labels">
<router-link
<RouterLink
v-if="release.shootId && release.studio"
:to="{ name: 'studio', params: { entitySlug: release.studio.slug } }"
:to="`/studio/${release.studio.slug}`"
:title="release.studio && release.studio.name"
class="shoot nolink"
>{{ release.shootId }}</router-link>
>{{ release.shootId }}</RouterLink>
<span
v-else-if="release.shootId"
@@ -136,10 +146,10 @@
:key="`tag-${tag.slug}`"
class="tag"
>
<router-link
<RouterLink
:to="`/tag/${tag.slug}`"
class="tag-link"
>{{ tag.name }}</router-link>
>{{ tag.name }}</RouterLink>
</li>
</ul>
</div>
@@ -167,12 +177,14 @@ async function stashScene() {
}
async function unstashScene() {
this.favorited = false;
if (!this.stash || this.stash.primary) {
this.favorited = false;
}
try {
await this.$store.dispatch('unstashScene', {
sceneId: this.release.id,
stashId: this.$store.getters.favorites.id,
stashId: this.stash?.id || this.$store.getters.favorites.id,
});
this.$emit('stash', false);
@@ -190,6 +202,10 @@ export default {
type: Object,
default: null,
},
stash: {
type: Object,
default: null,
},
},
emits: ['stash'],
data() {
@@ -218,23 +234,27 @@ export default {
box-shadow: 0 0 3px var(--darken-weak);
&.new .poster::after {
content: '';
content: 'new';
position: absolute;
top: 0;
left: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
bottom: 0;
right: .5rem;
width: 2rem;
box-sizing: border-box;
margin: .25rem;
border-radius: 0 0 .5rem 0;
color: var(--primary);
font-size: 1rem;
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;
text-shadow: 0 0 2px var(--darken-weak);
box-shadow: 0 0 3px var(--shadow);
}
&:hover .unstashed {
&:hover .unstashed,
&:hover .unstash {
fill: var(--lighten);
}
}
@@ -279,8 +299,8 @@ export default {
}
.stash {
width: 1.25rem;
height: 1.25rem;
width: 1.5rem;
height: 1.5rem;
position: absolute;
top: 0;
right: 0;
@@ -293,20 +313,26 @@ export default {
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;
margin: 0 0 .1rem 0;
}
.info {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
overflow: hidden;
}
@@ -318,7 +344,7 @@ export default {
.title {
margin: 0;
color: var(--text);
font-size: 1rem;
font-size: .9rem;
line-height: 1.5;
white-space: nowrap;
text-overflow: ellipsis;
@@ -344,8 +370,9 @@ export default {
}
.actor-link {
color: var(--link);
color: var(--text);
text-decoration: none;
font-size: .9rem;
&:hover {
color: var(--primary);
@@ -353,7 +380,7 @@ export default {
}
.labels {
padding: 0 .5rem 1.5rem .5rem;
padding: 0 .5rem 1.25rem .5rem;
max-height: .5rem;
overflow-y: hidden;
font-size: 0;
@@ -384,11 +411,11 @@ export default {
}
.tag-link {
background: var(--background);
background: var(--shadow-touch);
color: var(--shadow);
display: inline-block;
padding: .25rem .5rem;
border: solid 1px var(--shadow-hint);
margin: 0 1px 0 0;
font-size: .75rem;
font-weight: bold;
text-decoration: none;
@@ -400,14 +427,20 @@ export default {
}
.details-wide {
margin: 0 0 .5rem 0;
margin: 0 0 .4rem 0;
}
.details-compact {
display: none;
}
@media(max-width: $breakpoint-kilo) {
@media(max-width: $breakpoint) {
.thumbnail {
height: 11rem;
}
}
@media(max-width: $breakpoint-micro) {
.tile-body {
flex-direction: row;
}
@@ -453,14 +486,15 @@ export default {
}
.tile.new .poster::after {
bottom: 0;
top: auto;
margin: .1rem .25rem;
top: 0;
right: .25rem;
bottom: auto;
border-radius: 0 0 .25rem .25rem;
padding: .05rem 0 .1rem 0;
}
.stash {
left: 0;
right: auto;
padding: .25rem .5rem .5rem .25rem;
}
}

View File

@@ -57,10 +57,7 @@
target="_blank"
rel="noopener noreferrer"
class="date"
><Icon
v-if="release.url"
icon="share2"
/>{{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }}</a>
>{{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }}</a>
<a
v-else
@@ -91,27 +88,17 @@ export default {
width: 100%;
display: flex;
justify-content: space-between;
z-index: 1;
white-space: nowrap;
background: var(--profile);
background: var(--info);
font-size: 0;
font-weight: bold;
.favicon {
width: 2rem;
box-sizing: border-box;
padding: .5rem;
padding: .4rem .5rem;
}
/*
&.new .date::before {
content: '';
width: .5rem;
height: .5rem;
border-radius: 50%;
margin: 0 .25rem 0 0;
background: var(--primary);
}
*/
}
.site,
@@ -120,7 +107,7 @@ export default {
align-items: center;
position: relative;
font-size: .8rem;
padding: .5rem;
padding: .4rem .5rem;
color: var(--text-light);
text-decoration: none;

View 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>

View File

@@ -131,7 +131,7 @@
<div class="sidebar-section controls noselect">
<label
v-if="auth && me"
v-if="login && me"
@click="$emit('toggleSidebar', false)"
>
<router-link
@@ -142,7 +142,7 @@
<div class="toggles noselect">
<label
v-if="auth && !me"
v-if="login && !me"
@click="$emit('toggleSidebar', false)"
>
<router-link
@@ -152,7 +152,7 @@
</label>
<label
v-if="auth && me"
v-if="login && me"
class="toggle"
@click.stop="$store.dispatch('logout')"
><Icon icon="exit2" />Log out</label>
@@ -206,8 +206,12 @@ function theme(state) {
return state.ui.theme;
}
function auth(state) {
return state.auth.enabled;
function login(state) {
return state.auth.login;
}
function signup(state) {
return state.auth.signup;
}
function me(state) {
@@ -234,7 +238,8 @@ export default {
},
computed: {
...mapState({
auth,
login,
signup,
me,
sfw,
theme,

View File

@@ -10,11 +10,11 @@
>{{ stash.name }}</h2>
<span class="header-section">
<router-link
<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></router-link>
><Icon icon="user3" /><span class="username-name">{{ stash.user.username }}</span></RouterLink>
<label
v-if="isMine"
@@ -55,22 +55,50 @@
</div>
<div class="content-inner">
<FilterBar :ranges="['scenes', 'actors', 'movies']" />
<Releases
v-if="stash.scenes?.length > 0"
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="stash.actors?.length > 0"
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" /></li>
><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>
@@ -78,12 +106,35 @@
<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', this.$route.params.stashId);
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) {
@@ -109,23 +160,32 @@ async function removeStash(removed) {
}
async function mounted() {
this.fetchStash();
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,
@@ -199,24 +259,29 @@ export default {
}
}
.stash-section:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint);
}
.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-scenes {
flex-grow: 0;
.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);
}
.tiles {
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
}
.stash-scenes .tiles {
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
grid-template-rows: min-content;
}
@media(max-width: $breakpoint-small) {

View File

@@ -1,5 +1,13 @@
<template>
<div class="photos">
<Campaign
:tag="tag"
:min-height="240"
:max-ratio="1.5"
class="photo-link photo"
@campaign="campaign => $emit('campaign', campaign)"
/>
<a
v-for="photo in photos"
:key="`photo-${photo.id}`"
@@ -37,6 +45,7 @@
<script>
import Logo from '../album/logo.vue';
import Campaign from '../campaigns/campaign.vue';
function photos() {
if (this.tag.poster && this.$store.state.ui.sfw) {
@@ -57,6 +66,7 @@ function photos() {
export default {
components: {
Logo,
Campaign,
},
props: {
tag: {
@@ -64,7 +74,7 @@ export default {
default: null,
},
},
emits: ['load'],
emits: ['load', 'campaign'],
computed: {
photos,
},
@@ -150,4 +160,9 @@ export default {
transform: translateY(100%);
transition: transform .25s ease;
}
::v-deep(.campaign) .campaign-banner {
max-height: 15rem;
width: auto;
}
</style>

View File

@@ -29,6 +29,7 @@
:tag="tag"
:class="{ expanded }"
@load="scroll.loaded"
@campaign="campaign => showBannerCampaign = !campaign"
/>
</Scroll>
@@ -43,16 +44,30 @@
:items="[tag.poster, ...tag.photos]"
:title="tag.name"
:local="true"
:tag="tag"
class="portrait"
@close="$router.replace({ hash: undefined })"
/>
<div
v-if="showBannerCampaign"
class="campaign-container"
>
<Campaign
:tag="tag"
:min-ratio="3"
/>
</div>
<FilterBar
ref="filter"
:fetch-releases="fetchReleases"
/>
<Releases :releases="releases" />
<Releases
:releases="releases"
:done="done"
/>
<Pagination
:items-total="totalCount"
@@ -76,10 +91,13 @@ 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(scroll = true) {
this.done = false;
const { tag, releases, totalCount } = await this.$store.dispatch('fetchTagBySlug', {
tagSlug: this.$route.params.tagSlug,
pageNumber: Number(this.$route.params.pageNumber),
@@ -94,6 +112,12 @@ async function fetchReleases(scroll = true) {
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();
}
@@ -122,17 +146,20 @@ export default {
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: {
@@ -172,12 +199,14 @@ export default {
@import 'theme';
.header {
display: flex;
background: var(--profile);
color: var(--text-light);
justify-content: space-between;
}
.title {
display: inline-block;
padding: .5rem 1rem;
margin: 0;
flex-shrink: 0;
@@ -200,4 +229,11 @@ export default {
.scroll {
background: var(--background-dim);
}
.campaign-container {
max-height: 90px;
padding: .5rem 1rem 0 1rem;
background: var(--background-dim);
text-align: center;
}
</style>

View File

@@ -1,22 +1,29 @@
<template>
<div class="tags">
<div
v-for="(tags, category) in categories"
:key="category"
class="category"
>
<h3 class="heading">{{ category }}</h3>
<div class="content-inner">
<SearchBar
:placeholder="'Search tags'"
class="search"
/>
<div
:key="sfw"
class="tiles"
v-for="(tags, category) in categories"
:key="category"
class="category"
>
<Tag
v-for="tag in tags"
:key="`tag-${tag.id}`"
:tag="tag"
:lazy="true"
/>
<h3 class="heading">{{ category }}</h3>
<div
:key="sfw"
class="tiles"
>
<Tag
v-for="tag in tags"
:key="`tag-${tag.id}`"
:tag="tag"
:lazy="true"
/>
</div>
</div>
</div>
@@ -26,121 +33,135 @@
<script>
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',
'fisting-dp',
],
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',
],
toys: [
'toys',
'toy-anal',
'toy-dp',
'double-dildo',
'double-dildo-blowjob',
'double-dildo-kiss',
'double-dildo-anal',
'double-dildo-dp',
],
roleplay: [
'family',
'parody',
'schoolgirl',
'nurse',
'maid',
'nun',
],
fetish: [
'bdsm',
'femdom',
'bondage',
'free-use',
'latex',
'blindfold',
],
extreme: [
'dp',
'airtight',
'dap',
'dvp',
'da-tp',
'dv-tp',
'tap',
'tvp',
],
misc: [
'gaping',
'squirting',
'oil',
'vr',
'bts',
],
};
function sfw() {
return this.$store.state.ui.sfw;
}
async function mounted() {
const tagSlugsByCategory = {
popular: [
'anal',
'lesbian',
'interracial',
'mff',
'mfm',
'teen',
'milf',
'blowjob',
'dp',
'gangbang',
'facial',
'creampie',
'squirting',
],
appearance: [
'asian',
'black',
'latina',
'white',
'natural-boobs',
'enhanced-boobs',
'blonde',
'brunette',
'redhead',
'tattoos',
'piercings',
],
oral: [
'blowjob',
'pussy-eating',
'ass-eating',
'69',
'double-blowjob',
'deepthroat',
'facefucking',
'atm',
],
manual: [
'handjob',
'fingering',
'anal-fingering',
'titty-fucking',
'fisting',
'anal-fisting',
'fisting-dp',
],
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',
],
toys: [
'toys',
'toy-anal',
'toy-dp',
'double-dildo',
'double-dildo-blowjob',
'double-dildo-kiss',
'double-dildo-anal',
'double-dildo-dp',
],
roleplay: [
'family',
'parody',
'schoolgirl',
'nurse',
'maid',
'nun',
],
fetish: [
'bdsm',
'femdom',
'bondage',
'latex',
'blindfold',
],
extreme: [
'dp',
'airtight',
'dap',
'dvp',
'da-tp',
'dv-tp',
'tap',
],
misc: [
'gaping',
'squirting',
'oil',
'vr',
'bts',
],
};
async function fetchTags() {
if (this.$route.query.query) {
await this.searchTags();
return;
}
const tags = await this.$store.dispatch('fetchTags', {
slugs: Object.values(tagSlugsByCategory).flat(),
@@ -148,17 +169,42 @@ async function mounted() {
const tagsBySlug = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag }), {});
this.categories = Object.entries(tagSlugsByCategory).reduce((acc, [category, tagSlugs]) => ({
...acc,
[category]: tagSlugs.map(tagSlug => tagsBySlug[tagSlug]),
}), {});
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() {
this.pageTitle = 'Tags';
await this.fetchTags();
}
export default {
components: {
Tag,
SearchBar,
},
data() {
return {
@@ -169,14 +215,28 @@ export default {
computed: {
sfw,
},
watch: {
$route: fetchTags,
'$store.state.ui.tagFilter': fetchTags,
},
mounted,
methods: {
fetchTags,
searchTags,
},
};
</script>
<style lang="scss" scoped>
@import 'theme';
@import 'breakpoints';
.tags {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.content-inner {
padding: 1rem 1rem 0 1rem;
}
@@ -184,35 +244,97 @@ export default {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20rem, .33fr));
grid-gap: 1rem;
margin: 0 0 1.5rem 0;
}
.heading {
text-transform: capitalize;
padding: 0 0 0 .5rem;
margin: 2rem 0 1rem 0;
margin: 1.25rem 0 1rem 0;
}
.category:first-child .heading {
margin: .5rem 0 1rem 0;
}
@media(max-width: $breakpoint3) {
.search {
margin: 0 0 .25rem 0;
}
@media(max-width: $breakpoint-mega) {
.tiles {
grid-template-columns: repeat(auto-fill, minmax(18rem, .5fr));
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(15rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(14.5rem, 1fr));
grid-gap: .5rem;
}
::v-deep(.poster),
::v-deep(.blank) {
height: 11rem;
}
}
@media(max-width: $breakpoint0) {
@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(12rem, 1fr));
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>

View File

@@ -1,55 +1,62 @@
<template>
<div
v-if="tag.poster"
class="tile"
>
<router-link
:to="`/tag/${tag.slug}`"
:title="tag.name"
:class="{ blank: !tag.poster }"
class="poster-link"
>
<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"
>
<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.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.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"
>
<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
<Logo
v-if="!sfw"
:photo="tag.poster"
favicon
/>
</template>
<Icon
v-else
icon="price-tag2"
/>
</router-link>
@@ -59,11 +66,6 @@
:title="tag.name"
>{{ tag.name }}</router-link>
</div>
<span
v-else
class="title"
>{{ tag.name }}</span>
</template>
<script>
@@ -100,13 +102,16 @@ export default {
<style lang="scss" scoped>
.tile {
display: flex;
flex-direction: column;
box-sizing: border-box;
position: relative;
text-decoration: none;
font-size: 0;
&:hover {
.poster {
.poster,
.blank {
box-shadow: 0 0 2px var(--darken);
}
@@ -119,15 +124,30 @@ export default {
.poster {
display: inline-block;
width: 100%;
height: 13.5rem;
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 {

View File

@@ -16,16 +16,19 @@
:style="{ transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)` }"
@click.stop
>
<div class="tooltip-inner">
<div
class="tooltip-inner"
:style="{ 'max-height': `calc(100vh - ${tooltipY}px - 1rem)` }"
>
<div class="tooltip">
<slot name="tooltip" />
</div>
<div
class="tooltip-arrow"
:style="{ transform: `translate3d(${arrowOffset}px, 0, 0)` }"
/>
</div>
<div
class="tooltip-arrow"
:style="{ transform: `translate3d(${arrowOffset}px, 0, 0)` }"
/>
</div>
</teleport>
</div>
@@ -155,6 +158,8 @@ export default {
.tooltip-inner {
position: relative;
overflow-y: auto;
background: var(--background);
box-shadow: 0 0 .5rem var(--darken);
}

View File

@@ -2,7 +2,7 @@
<div class="stash">
<div class="stash-section stash-header">
<router-link
:to="{ name: 'stash', params: { stashId: stash.id, stashSlug: stash.slug } }"
: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>

View File

@@ -1,5 +1,6 @@
$breakpoint-pico: 270px;
$breakpoint-nano: 320px;
$breakpoint-mini: 400px;
$breakpoint-micro: 540px;
$breakpoint-small: 620px;
$breakpoint: 720px;

View File

@@ -44,6 +44,11 @@
&:hover {
background: var(--primary-strong);
}
&:disabled {
background: var(--shadow-weak);
cursor: default;
}
}
.button-secondary {

View File

@@ -5,14 +5,21 @@ $breakpoint3: 1200px;
$breakpoint4: 1500px;
:root {
--primary: #f65596;
--primary-strong: #f90071;
--primary-faded: #ffcce4;
/*
--primary: #f04288;
--primary: #ff6c88;
--primary-strong: #ff4166;
--primary-faded: #ffdfee;
*/
--primary: #f28;
--primary-strong: #f90071;
--primary-faded: #ffcce4;
*/
--text-dark: #222;
--text-light: #fff;
@@ -21,16 +28,17 @@ $breakpoint4: 1500px;
--background-dark: #222;
--darken: rgba(0, 0, 0, .5);
--darken-strong: rgba(0, 0, 0, .7);
--darken-extreme: rgba(0, 0, 0, .9);
--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: rgba(255, 255, 255, .5);
--lighten-strong: rgba(255, 255, 255, .7);
--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);
@@ -38,6 +46,8 @@ $breakpoint4: 1500px;
--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;
@@ -58,6 +68,7 @@ $breakpoint4: 1500px;
--text-contrast: #fff;
--background: var(--background-light);
--background-censor: rgba(255, 255, 255, .95);
--background-dim: #f5f5f5;
--background-soft: #fdfdfd;
@@ -71,6 +82,7 @@ $breakpoint4: 1500px;
--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);
@@ -91,6 +103,7 @@ $breakpoint4: 1500px;
--text-contrast: #222;
--background: #181818;
--background-censor: rgba(0, 0, 0, .95);
--background-dim: #111;
--background-soft: #000;

View File

@@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>film4</title>
<path d="M0 2v12h16v-12h-16zM3 13h-2v-2h2v2zM3 9h-2v-2h2v2zM3 5h-2v-2h2v2zM12 13h-8v-10h8v10zM15 13h-2v-2h2v2zM15 9h-2v-2h2v2zM15 5h-2v-2h2v2zM6 5v6l4-3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>presentation</title>
<path d="M14.25 0h-13.5c-0.412 0-0.75 0.338-0.75 0.75v11.5c0 0.412 0.338 0.75 0.75 0.75h4.455l-2.040 2.040c-0.22 0.22-0.22 0.576 0 0.795 0.11 0.11 0.254 0.165 0.398 0.165s0.288-0.055 0.398-0.165l2.835-2.835h0.205v2.5c0 0.276 0.224 0.5 0.5 0.5s0.5-0.224 0.5-0.5v-2.5h0.205l2.835 2.835c0.11 0.11 0.254 0.165 0.398 0.165s0.288-0.055 0.398-0.165c0.22-0.22 0.22-0.576 0-0.795l-2.040-2.040h4.455c0.412 0 0.75-0.338 0.75-0.75v-11.5c0-0.412-0.338-0.75-0.75-0.75zM6 3l4 2.5-4 2.5v-5zM12.5 11h-10c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -281,7 +281,7 @@ function initActorActions(store, router) {
return {
actor: curateActor(actor, null, curateRelease),
releases: actor.scenesConnection.releases.map(release => curateRelease(release)),
releases: actor.scenesConnection.releases.map((release) => curateRelease(release)),
totalCount: actor.scenesConnection.totalCount,
};
}
@@ -289,9 +289,10 @@ function initActorActions(store, router) {
async function fetchActors({ _commit }, {
limit = 10,
pageNumber = 1,
letter,
query,
gender,
age,
country,
dob,
naturalBoobs,
boobSize,
@@ -302,7 +303,8 @@ function initActorActions(store, router) {
const genderFilter = (gender === null && 'gender: { isNull: true }')
|| (gender === 'all' && ' ')
|| `gender: { equalTo: "${gender}" }`;
|| (gender && `gender: { equalTo: "${gender}" }`)
|| '';
const ageFilter = age ? `
or: [
@@ -325,37 +327,38 @@ function initActorActions(store, router) {
` : '';
const dobFilter = dob ? `dateOfBirth: { equalTo: "${dob}" }` : '';
const countryFilter = country ? `countryByBirthCountryAlpha2: { alpha2: { equalTo: "${country}" } }` : '';
const heightFilter = height ? `height: { greaterThanOrEqualTo: ${height[0]}, lessThanOrEqualTo: ${height[1]} }` : '';
const weightFilter = weight ? `weight: { greaterThanOrEqualTo: ${weight[0]}, lessThanOrEqualTo: ${weight[1]} }` : '';
const cupFilter = boobSize ? `cup: { greaterThanOrEqualTo: "${boobSize[0]}", lessThanOrEqualTo: "${boobSize[1]}" }` : '';
const { connection: { actors, totalCount } } = await graphql(`
const { connection: { actors, totalCount }, countries } = await graphql(`
query Actors(
$query: String
$limit: Int,
$offset: Int = 0,
$letter: String! = "",
$naturalBoobs: Boolean,
$userId: Int,
$hasAuth: Boolean!,
) {
connection: actorsConnection(
connection: searchActorsConnection(
query: $query
first: $limit,
offset: $offset
orderBy: NAME_ASC
minLength: 0
filter: {
aliasFor: {
isNull: true
}
name: {
startsWith: $letter
}
${genderFilter}
${ageFilter}
${dobFilter}
${heightFilter}
${weightFilter}
${cupFilter}
${countryFilter}
naturalBoobs: {
equalTo: $naturalBoobs
}
@@ -408,110 +411,30 @@ function initActorActions(store, router) {
${actorStashesFields}
}
}
countries(
orderBy: NAME_ASC,
) {
alpha2
name
alias
}
}
`, {
query,
offset: Math.max(0, (pageNumber - 1)) * limit,
limit,
letter,
naturalBoobs,
naturalBoobs: naturalBoobs || null,
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
});
return {
actors: actors.map(actor => curateActor(actor)),
actors: actors.map((actor) => curateActor(actor)),
totalCount,
countries,
};
}
async function searchActors({ _commit }, { query, limit = 20, minLength = 2 }) {
const { actors } = await graphql(`
query SearchActors(
$query: String!
$limit: Int = 20
$minLength: Int = 2
$hasAuth: Boolean!
$userId: Int
) {
actors: searchActors(
search: $query,
minLength: $minLength
first: $limit
) {
id
name
slug
age
ageAtDeath
dateOfBirth
dateOfDeath
gender
aliasFor: actorByAliasFor {
id
name
slug
age
ageAtDeath
dateOfBirth
dateOfDeath
gender
entity {
id
name
slug
}
avatar: avatarMedia {
id
path
thumbnail
lazy
isS3
width
height
comment
credit
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
}
entity {
id
name
slug
}
avatar: avatarMedia {
id
path
thumbnail
lazy
isS3
width
height
comment
credit
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
${actorStashesFields}
}
}
`, {
query,
limit,
minLength,
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
});
return actors.map(actor => curateActor(actor));
}
async function fetchActorReleases({ _commit }, actorId) {
const releases = await get(`/actors/${actorId}/releases`, {
filter: store.state.ui.filter,
@@ -526,7 +449,6 @@ function initActorActions(store, router) {
fetchActorById,
fetchActors,
fetchActorReleases,
searchActors,
};
}

View File

@@ -1,5 +1,5 @@
function favoritesStash(state) {
return state.user.stashes.find(stash => stash.slug === 'favorites');
return state.user.stashes.find((stash) => stash.slug === 'favorites');
}
module.exports = {

View File

@@ -1,4 +1,5 @@
export default {
enabled: window.env.auth,
login: window.env.login,
signup: window.env.signup,
user: null,
};

View File

@@ -31,8 +31,8 @@ function curateActor(actor, release) {
if (actor.profiles) {
const photos = actor.profiles
.map(profile => ({ entity: profile.entity, ...profile.avatar }))
.filter(avatar => avatar.id && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash));
.map((profile) => ({ entity: profile.entity, ...profile.avatar }))
.filter((avatar) => avatar.id && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash));
const descriptions = actor.profiles.reduce((acc, profile) => ({
...acc,
@@ -57,10 +57,10 @@ function curateActor(actor, release) {
}
if (actor.stashes) {
curatedActor.stashes = actor.stashes.filter(Boolean).map(stash => curateStash(stash.stash || stash)); // eslint-disable-line no-use-before-define
curatedActor.stashes = actor.stashes.filter(Boolean).map((stash) => curateStash(stash.stash || stash)); // eslint-disable-line no-use-before-define
}
curatedActor.stashes = actor.stashes?.map(stash => stash.stash || stash) || [];
curatedActor.stashes = actor.stashes?.map((stash) => stash.stash || stash) || [];
return curatedActor;
}
@@ -70,21 +70,21 @@ function curateRelease(release) {
...release,
actors: [],
poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map(tag => tag.tag || tag) : [],
tags: release.tags ? release.tags.map((tag) => tag.tag || tag) : [],
};
if (release.scenes) curatedRelease.scenes = release.scenes.filter(Boolean).map(({ scene }) => curateRelease(scene));
if (release.movies) curatedRelease.movies = release.movies.filter(Boolean).map(({ movie }) => curateRelease(movie));
if (release.chapters) curatedRelease.chapters = release.chapters.filter(Boolean).map(chapter => curateRelease(chapter));
if (release.photos) curatedRelease.photos = release.photos.filter(Boolean).map(photo => photo.media || photo);
if (release.chapters) curatedRelease.chapters = release.chapters.filter(Boolean).map((chapter) => curateRelease(chapter));
if (release.photos) curatedRelease.photos = release.photos.filter(Boolean).map((photo) => photo.media || photo);
if (release.covers) curatedRelease.covers = release.covers.filter(Boolean).map(({ media }) => media);
if (release.trailer) curatedRelease.trailer = release.trailer.media;
if (release.teaser) curatedRelease.teaser = release.teaser.media;
if (release.actors) curatedRelease.actors = release.actors.filter(Boolean).map(actor => curateActor(actor.actor || actor, curatedRelease));
if (release.directors) curatedRelease.directors = release.directors.filter(Boolean).map(director => curateActor(director.director || director, curatedRelease));
if (release.actors) curatedRelease.actors = release.actors.filter(Boolean).map((actor) => curateActor(actor.actor || actor, curatedRelease));
if (release.directors) curatedRelease.directors = release.directors.filter(Boolean).map((director) => curateActor(director.director || director, curatedRelease));
if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.filter(Boolean).map(({ tag }) => tag);
if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.filter(Boolean).map(({ actor }) => curateActor(actor, curatedRelease));
if (release.stashes) curatedRelease.stashes = release.stashes.filter(Boolean).map(stash => curateStash(stash.stash || stash)); // eslint-disable-line no-use-before-define
if (release.stashes) curatedRelease.stashes = release.stashes.filter(Boolean).map((stash) => curateStash(stash.stash || stash)); // eslint-disable-line no-use-before-define
if (release.productionLocation) {
curatedRelease.productionLocation = {
@@ -108,14 +108,16 @@ function curateEntity(entity, parent, releases) {
if (entity.children) {
if (entity.children.nodes) {
curatedEntity.children = entity.children.nodes.map(childEntity => curateEntity(childEntity, curatedEntity));
curatedEntity.children = entity.children.nodes.map((childEntity) => curateEntity(childEntity, curatedEntity));
}
curatedEntity.childrenTotal = entity.children.totalCount;
}
if (entity.parent || parent) curatedEntity.parent = curateEntity(entity.parent || parent);
if (releases) curatedEntity.releases = releases.map(release => curateRelease(release));
if (releases) curatedEntity.releases = releases.map((release) => curateRelease(release));
curatedEntity.sceneTotal = entity.sceneTotal;
return curatedEntity;
}
@@ -126,6 +128,7 @@ function curateTag(tag) {
};
if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release));
if (tag.banners) curatedTag.banners = tag.banners.map(({ banner }) => banner);
if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media);
if (tag.poster) curatedTag.poster = tag.poster.media;
@@ -135,20 +138,30 @@ function curateTag(tag) {
function curateStash(stash) {
const curatedStash = stash;
if (stash.scenes) {
curatedStash.scenes = stash.scenes.map(item => ({
if (stash.scenes || stash.scenesConnection?.scenes) {
curatedStash.sceneTotal = stash.scenesConnection?.totalCount || null;
curatedStash.scenes = (stash.scenesConnection?.scenes || stash.scenes).map((item) => ({
...item,
scene: curateRelease(item.scene),
}));
}
if (stash.actors) {
curatedStash.actors = stash.actors.map(item => ({
if (stash.actors || stash.actorsConnection?.actors) {
curatedStash.actorTotal = stash.actorsConnection?.totalCount || null;
curatedStash.actors = (stash.actorsConnection?.actors || stash.actors).map((item) => ({
...item,
actor: curateActor(item.actor),
}));
}
if (stash.movies || stash.moviesConnection?.movies) {
curatedStash.movieTotal = stash.moviesConnection?.totalCount || null;
curatedStash.movies = (stash.moviesConnection?.movies || stash.movies).map((item) => ({
...item,
movie: curateRelease(item.movie),
}));
}
return curatedStash;
}
@@ -160,11 +173,11 @@ function curateAlert(alert) {
const curatedAlert = alert;
if (alert.actors) {
curatedAlert.actors = alert.actors.map(actor => curateActor(actor.actor || actor));
curatedAlert.actors = alert.actors.map((actor) => curateActor(actor.actor || actor));
}
if (alert.tags) {
curatedAlert.tags = alert.tags.map(tag => curateTag(tag.tag || tag));
curatedAlert.tags = alert.tags.map((tag) => curateTag(tag.tag || tag));
}
if (alert.entity) {
@@ -172,7 +185,7 @@ function curateAlert(alert) {
}
if (alert.stashes) {
curatedAlert.stashes = alert.stashes.map(stash => curateStash(stash.stash || stash));
curatedAlert.stashes = alert.stashes.map((stash) => curateStash(stash.stash || stash));
}
return curatedAlert;
@@ -186,11 +199,11 @@ function curateUser(user) {
const curatedUser = user;
if (user.stashes) {
curatedUser.stashes = user.stashes.map(stash => curateStash(stash.stash || stash));
curatedUser.stashes = user.stashes.map((stash) => curateStash(stash.stash || stash));
}
if (user.alerts) {
curatedUser.alerts = user.alerts.map(alert => curateAlert(alert.alert || alert));
curatedUser.alerts = user.alerts.map((alert) => curateAlert(alert.alert || alert));
}
return curatedUser;

View File

@@ -1,6 +1,6 @@
import { graphql } from '../api';
// import { sitesFragment, releaseFields } from '../fragments';
import { releaseFields } from '../fragments';
import { releaseFields, campaignsFragment } from '../fragments';
import { curateEntity } from '../curate';
import getDateRange from '../get-date-range';
@@ -14,7 +14,7 @@ function initEntitiesActions(store, router) {
}) {
const { before, after, orderBy } = getDateRange(range);
const { entity, connection: { releases, totalCount } } = await graphql(`
const { entity } = await graphql(`
query Entity(
$entitySlug: String!
$entityType: String! = "channel"
@@ -22,8 +22,6 @@ function initEntitiesActions(store, router) {
$offset: Int = 0,
$after: Datetime = "1900-01-01",
$before: Datetime = "2100-01-01",
$afterTime: Datetime = "1900-01-01",
$beforeTime: Datetime = "2100-01-01",
$orderBy: [ReleasesOrderBy!]
$exclude: [String!]
$hasAuth: Boolean!
@@ -63,8 +61,14 @@ function initEntitiesActions(store, router) {
priority
independent
hasLogo
${campaignsFragment}
sceneTotal
children: childEntitiesConnection {
totalCount
}
}
}
${campaignsFragment}
parent {
id
name
@@ -73,83 +77,46 @@ function initEntitiesActions(store, router) {
url
independent
hasLogo
${campaignsFragment}
}
}
connection: releasesConnection(
first: $limit
offset: $offset
orderBy: $orderBy
filter: {
and: [
{
entity: {
or: [
{
slug: { equalTo: $entitySlug }
},
{
parent: {
slug: { equalTo: $entitySlug }
type: { equalTo: $entityType }
}
},
{
parent: {
parent: {
slug: { equalTo: $entitySlug }
type: { equalTo: $entityType }
}
}
}
{
parent: {
parent: {
parent: {
slug: { equalTo: $entitySlug }
type: { equalTo: $entityType }
}
},
},
},
]
}
}
{
or: [
{
date: {
lessThan: $before,
greaterThan: $after
}
},
{
date: {
isNull: true
},
createdAt: {
lessThan: $beforeTime,
greaterThan: $afterTime,
}
connection: scenesConnection(
first: $limit
offset: $offset
orderBy: $orderBy
filter: {
or: [
{
date: {
isNull: ${entityType !== 'network'}
}
]
}
{
date: {
isNull: false
}
}
]
date: {
lessThan: $before,
greaterThan: $after
}
]
releasesTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
releasesTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
}
}
}
}
}
) {
releases: nodes {
${releaseFields}
}
totalCount
}
) {
releases: nodes {
${releaseFields}
}
totalCount
}
}
}
`, {
entitySlug,
@@ -159,8 +126,6 @@ function initEntitiesActions(store, router) {
after,
before,
orderBy,
afterTime: store.getters.after,
beforeTime: store.getters.before,
exclude: store.state.ui.tagFilter,
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
@@ -172,8 +137,8 @@ function initEntitiesActions(store, router) {
}
return {
entity: curateEntity(entity, null, releases),
totalCount,
entity: curateEntity(entity, null, entity.connection.releases),
totalCount: entity.connection.totalCount,
};
}
@@ -237,17 +202,17 @@ function initEntitiesActions(store, router) {
entitySlugs,
});
return entities.map(entity => curateEntity(entity));
return entities.map((entity) => curateEntity(entity));
}
async function searchEntities({ _commit }, { query, limit = 20 }) {
const { entities } = await graphql(`
query SearchEntities(
$query: String!
$limit:Int = 20,
$limit: Int = 20,
) {
entities: searchEntities(
search: $query,
search: $query
first: $limit
) {
id
@@ -271,7 +236,7 @@ function initEntitiesActions(store, router) {
limit,
});
return entities.map(entity => curateEntity(entity));
return entities.map((entity) => curateEntity(entity));
}
return {

View File

@@ -5,7 +5,7 @@ export function formatDuration(duration, forceHours) {
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
const [formattedHours, formattedMinutes, formattedSeconds] = [hours, minutes, seconds].map(segment => segment.toString().padStart(2, '0'));
const [formattedHours, formattedMinutes, formattedSeconds] = [hours, minutes, seconds].map((segment) => segment.toString().padStart(2, '0'));
if (duration >= 3600 || forceHours) {
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;

View File

@@ -96,6 +96,137 @@ const actorFields = `
${actorStashesFields}
`;
const movieFields = `
id
title
url
slug
date
datePrecision
actors {
id
name
slug
}
tags {
id
name
slug
}
entity {
id
name
slug
type
parent {
id
name
slug
type
}
}
covers: moviesCovers(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
media {
id
path
thumbnail
lazy
width
height
thumbnailWidth
thumbnailHeight
isS3
sfw: sfwMedia {
id
path
thumbnail
lazy
comment
}
}
}
isFavorited
isStashed(includeFavorites: false)
stashes: stashesMovies(
filter: {
stash: {
userId: {
equalTo: $userId
}
}
}
) @include(if: $hasAuth) {
stash {
id
name
slug
primary
}
}
`;
const campaignsFragment = `
campaigns(filter: {
or: [
{
banner: {
bannersTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
}
}
}
}
}
}
{
bannerExists: false
}
]
}) {
id
url
affiliate {
id
url
parameters
}
banner {
id
type
width
height
ratio
entity {
id
type
name
slug
parent {
id
type
name
slug
}
}
}
entity {
id
type
name
slug
parent {
id
type
name
slug
}
}
}
`;
const releaseActorsFragment = `
actors: releasesActors(orderBy: ACTOR_BY_ACTOR_ID__GENDER_ASC) {
actor {
@@ -113,7 +244,12 @@ const releaseDirectorFragment = `
`;
const releaseTagsFragment = `
tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tags: releasesTags(
orderBy: TAG_BY_TAG_ID__PRIORITY_DESC
filter: {
tagId: { isNull: false }
}
) {
tag {
name
priority
@@ -124,7 +260,7 @@ const releaseTagsFragment = `
`;
const releasePosterFragment = `
poster: releasesPosterByReleaseId {
poster: releasesPoster {
media {
id
index
@@ -199,7 +335,7 @@ const releasePhotosFragment = `
`;
const releaseTrailerFragment = `
trailer: releasesTrailerByReleaseId {
trailer: releasesTrailer {
media {
id
index
@@ -213,7 +349,7 @@ const releaseTrailerFragment = `
`;
const releaseTeaserFragment = `
teaser: releasesTeaserByReleaseId {
teaser: releasesTeaser {
media {
id
index
@@ -249,6 +385,13 @@ const releaseFields = `
slug
url
}
movies: moviesScenesBySceneId {
movie {
id
title
slug
}
}
isNew
isFavorited
isStashed(includeFavorites: false)
@@ -344,7 +487,7 @@ const releaseFragment = `
slug
}
}
poster: chaptersPosterByChapterId {
poster: chaptersPoster {
media {
id
index
@@ -373,7 +516,7 @@ const releaseFragment = `
id
title
slug
covers: moviesCovers {
covers: moviesCovers(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
media {
id
index
@@ -480,6 +623,8 @@ function getIncludedActors(router) {
export {
actorFields,
actorStashesFields,
campaignsFragment,
movieFields,
releaseActorsFragment,
releaseFields,
releaseTagsFragment,

View File

@@ -7,12 +7,12 @@ const dateRanges = {
latest: () => ({
after: '1900-01-01',
before: dayjs.utc().toDate(),
orderBy: ['DATE_DESC', 'CREATED_AT_DESC'],
orderBy: ['DATE_DESC'],
}),
upcoming: () => ({
after: dayjs.utc().toDate(),
before: '2100-01-01',
orderBy: ['DATE_ASC', 'CREATED_AT_ASC'],
orderBy: ['DATE_DESC'],
}),
new: () => ({
after: '1900-01-01 00:00:00',
@@ -22,7 +22,7 @@ const dateRanges = {
all: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: ['DATE_DESC', 'CREATED_AT_DESC'],
orderBy: ['DATE_DESC'],
}),
};

View File

@@ -109,6 +109,10 @@ async function init() {
document.title = 'traxxx';
},
},
beforeCreate() {
this.uid = uid;
uid += 1;
},
methods: {
formatDate,
formatDuration,
@@ -117,14 +121,14 @@ async function init() {
getPath,
getBgPath: (media, type) => `url(${getPath(media, type)})`,
},
beforeCreate() {
this.uid = uid;
uid += 1;
},
});
app.directive('tooltip', {
beforeMount(el, binding) {
if (!binding.value) {
return;
}
// don't include HTML in native title attribute
const textEl = document.createElement('div');
textEl.innerHTML = binding.value;

View File

@@ -1,5 +1,10 @@
import { graphql } from '../api';
import { releasesFragment, releaseFragment, releaseFields } from '../fragments';
import {
releasesFragment,
releaseFragment,
releaseFields,
movieFields,
} from '../fragments';
import { curateRelease } from '../curate';
import getDateRange from '../get-date-range';
@@ -32,7 +37,7 @@ function initReleasesActions(store, router) {
});
return {
releases: releases.map(release => curateRelease(release)),
releases: releases.map((release) => curateRelease(release)),
totalCount,
};
}
@@ -67,6 +72,8 @@ function initReleasesActions(store, router) {
query Movies(
$limit:Int = 1000,
$offset:Int = 0,
$hasAuth: Boolean!
$userId: Int
) {
connection: moviesConnection(
first: $limit
@@ -79,65 +86,57 @@ function initReleasesActions(store, router) {
}
) {
movies: nodes {
id
title
url
slug
date
datePrecision
actors {
id
name
slug
}
tags {
id
name
slug
}
entity {
id
name
slug
type
parent {
id
name
slug
type
}
}
covers: moviesCovers {
media {
id
path
thumbnail
lazy
width
height
thumbnailWidth
thumbnailHeight
isS3
sfw: sfwMedia {
id
path
thumbnail
lazy
comment
}
}
}
${movieFields}
}
totalCount
}
}
`, {
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
limit,
offset: Math.max(0, (pageNumber - 1)) * limit,
});
return {
movies: movies.map(release => curateRelease(release)),
movies: movies.map((release) => curateRelease(release)),
totalCount,
};
}
async function searchMovies({ _commit }, { query, limit = 20, pageNumber = 1 }) {
const { connection: { movies, totalCount } } = await graphql(`
query SearchMovies(
$query: String!
$limit:Int = 20
$offset:Int = 0
$hasAuth: Boolean!
$userId: Int
) {
connection: searchMoviesConnection(
query: $query
first: $limit
offset: $offset
orderBy: RANK_DESC
) {
movies: nodes {
movie {
${movieFields}
}
}
totalCount
}
}
`, {
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
query,
limit,
offset: Math.max(0, (pageNumber - 1)) * limit,
});
return {
movies: movies.map(({ movie }) => curateRelease(movie)),
totalCount,
};
}
@@ -183,7 +182,7 @@ function initReleasesActions(store, router) {
isS3
}
}
posters: moviesPosterByMovieId {
poster: moviesPoster {
media {
id
path
@@ -196,7 +195,7 @@ function initReleasesActions(store, router) {
isS3
}
}
covers: moviesCovers {
covers: moviesCovers(orderBy: MEDIA_BY_MEDIA_ID__INDEX_ASC) {
media {
id
path
@@ -209,7 +208,7 @@ function initReleasesActions(store, router) {
isS3
}
}
trailer: moviesTrailerByMovieId {
trailer: moviesTrailer {
media {
id
path
@@ -297,6 +296,7 @@ function initReleasesActions(store, router) {
fetchReleaseById,
fetchMovies,
fetchMovieById,
searchMovies,
};
}

View File

@@ -74,7 +74,7 @@ const routes = [
{
path: '/actor/:actorId/:actorSlug',
name: 'actor',
redirect: from => ({
redirect: (from) => ({
name: 'actorRange',
params: {
...from.params,
@@ -91,7 +91,7 @@ const routes = [
{
path: '/director/:actorId/:actorSlug',
name: 'director',
redirect: from => ({
redirect: (from) => ({
name: 'directorRange',
params: {
...from.params,
@@ -107,7 +107,7 @@ const routes = [
},
{
path: '/channel/:entitySlug',
redirect: from => ({
redirect: (from) => ({
name: 'channel',
params: {
...from.params,
@@ -123,7 +123,7 @@ const routes = [
},
{
path: '/network/:entitySlug',
redirect: from => ({
redirect: (from) => ({
name: 'network',
params: {
...from.params,
@@ -139,7 +139,7 @@ const routes = [
},
{
path: '/studio/:entitySlug',
redirect: from => ({
redirect: (from) => ({
name: 'studio',
params: {
...from.params,
@@ -155,7 +155,7 @@ const routes = [
},
{
path: '/tag/:tagSlug',
redirect: from => ({
redirect: (from) => ({
name: 'tag',
params: {
...from.params,
@@ -171,12 +171,11 @@ const routes = [
},
{
path: '/actors',
redirect: from => ({
redirect: (from) => ({
name: 'actors',
params: {
...from.params,
gender: 'all',
letter: 'all',
tags: 'all',
range: 'latest',
pageNumber: 1,
@@ -184,7 +183,7 @@ const routes = [
}),
},
{
path: '/actors/:gender?/:letter?/:pageNumber',
path: '/actors/:gender?/:pageNumber',
component: Actors,
name: 'actors',
},
@@ -196,6 +195,17 @@ const routes = [
{
path: '/movies',
component: Movies,
redirect: {
name: 'movies',
params: {
range: 'latest',
pageNumber: 1,
},
},
},
{
path: '/movies/:range/:pageNumber',
component: Movies,
name: 'movies',
},
{
@@ -218,7 +228,18 @@ const routes = [
name: 'notifications',
},
{
path: '/stash/:stashId/:stashSlug?',
path: '/stash/:stashId/:stashSlug',
redirect: (from) => ({
name: 'stash',
params: {
...from.params,
range: 'scenes',
pageNumber: 1,
},
}),
},
{
path: '/stash/:stashId/:stashSlug?/:range/:pageNumber',
component: Stash,
name: 'stash',
},

View File

@@ -5,14 +5,24 @@ import {
patch,
} from '../api';
import { releaseFields, actorStashesFields } from '../fragments';
import { releaseFields, actorStashesFields, movieFields } from '../fragments';
import { curateStash } from '../curate';
function initStashesActions(store, _router) {
async function fetchStash(context, stashId) {
async function fetchStash(context, {
stashId,
section = 'scenes',
pageNumber = 1,
limit = 20,
}) {
const { stash } = await graphql(`
query Stash(
$stashId: Int!
$includeScenes: Boolean!
$includeActors: Boolean!
$includeMovies: Boolean!
$limit:Int = 10,
$offset:Int = 0,
$hasAuth: Boolean!
$userId: Int
) {
@@ -26,45 +36,77 @@ function initStashesActions(store, _router) {
id
username
}
actors: stashesActors {
comment
actor {
id
name
slug
gender
age
ageFromBirth
dateOfBirth
birthCity
birthState
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: avatarMedia {
actorsConnection: stashesActorsConnection(
orderBy: CREATED_AT_DESC
first: $limit
offset: $offset
) @include(if: $includeActors) {
totalCount
actors: nodes {
comment
actor {
id
path
thumbnail
lazy
isS3
width
height
name
slug
gender
age
ageFromBirth
dateOfBirth
birthCity
birthState
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: avatarMedia {
id
path
thumbnail
lazy
isS3
width
height
}
${actorStashesFields}
}
${actorStashesFields}
}
}
scenes: stashesScenes {
comment
scene {
${releaseFields}
scenesConnection: stashesScenesConnection(
orderBy: [CREATED_AT_DESC, RELEASE_BY_SCENE_ID__DATE_DESC]
first: $limit
offset: $offset
) @include(if: $includeScenes) {
totalCount
scenes: nodes {
comment
scene {
${releaseFields}
}
}
}
moviesConnection: stashesMoviesConnection(
orderBy: [CREATED_AT_DESC, MOVIE_BY_MOVIE_ID__DATE_DESC]
first: $limit
offset: $offset
) @include(if: $includeMovies) {
totalCount
movies: nodes {
comment
movie {
${movieFields}
}
}
}
}
}
`, {
stashId: Number(stashId),
includeScenes: section === 'scenes',
includeActors: section === 'actors',
includeMovies: section === 'movies',
offset: Math.max(0, (pageNumber - 1)) * limit,
limit: Number(limit),
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
});

View File

@@ -35,7 +35,7 @@ function initTagsActions(store, _router) {
name
slug
}
poster: tagsPosterByTagId {
poster: tagsPoster {
media {
id
thumbnail
@@ -103,6 +103,57 @@ function initTagsActions(store, _router) {
}
}
}
banners: bannersTags(filter: {
banner: {
bannersTagsConnection: {
none: {
tag: {
slug: {
in: $exclude
}
}
}
}
}
}) {
banner {
id
width
height
type
ratio
entity {
id
type
name
slug
independent
parent {
id
type
name
slug
independent
}
}
campaigns {
id
url
entity {
id
type
name
slug
parent {
id
type
name
slug
}
}
}
}
}
scenesConnection(
filter: {
date: {
@@ -137,14 +188,14 @@ function initTagsActions(store, _router) {
before,
orderBy,
offset: Math.max(0, (pageNumber - 1)) * limit,
exclude: store.state.ui.tagFilter.filter(tagFilter => tagFilter !== tagSlug),
exclude: store.state.ui.tagFilter.filter((tagFilter) => tagFilter !== tagSlug),
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
});
return {
tag: curateTag(tagBySlug, null, curateRelease),
releases: tagBySlug.scenesConnection.releases.map(release => curateRelease(release)),
releases: tagBySlug.scenesConnection.releases.map((release) => curateRelease(release)),
totalCount: tagBySlug.scenesConnection.totalCount,
};
}
@@ -159,15 +210,21 @@ function initTagsActions(store, _router) {
query Tags(
$slugs: [String!] = [],
$limit: Int = 100
$exclude: [String!]
) {
tags(
filter: { slug: { in: $slugs } },
filter: {
slug: {
in: $slugs
notIn: $exclude
}
},
first: $limit
) {
id
name
slug
poster: tagsPosterByTagId {
poster: tagsPoster {
media {
thumbnail
comment
@@ -206,9 +263,10 @@ function initTagsActions(store, _router) {
`, {
slugs,
limit,
exclude: store.state.ui.tagFilter,
});
return tags.map(tag => curateTag(tag, store.state.ui.sfw));
return tags.map((tag) => curateTag(tag, store.state.ui.sfw));
}
async function searchTags({ _commit }, {
@@ -225,14 +283,14 @@ function initTagsActions(store, _router) {
$minLength: Int = 2
) {
tags: searchTags(
search: $query,
query: $query,
first: $limit
minLength: $minLength
) {
id
name
slug
poster: tagsPosterByTagId {
poster: tagsPoster {
media {
thumbnail
comment
@@ -274,7 +332,7 @@ function initTagsActions(store, _router) {
minLength,
});
return tags.map(tag => curateTag(tag, store.state.ui.sfw));
return tags.map((tag) => curateTag(tag, store.state.ui.sfw));
}
async function fetchTagReleases({ _commit }, tagId) {

View File

@@ -70,7 +70,7 @@ function initUiActions(store, _router) {
slug
}
}
entity: alertsEntityByAlertId {
entity: alertsEntity {
entity {
id
name
@@ -103,7 +103,7 @@ function initUiActions(store, _router) {
};
}
const curatedNotifications = notifications.nodes.map(notification => curateNotification(notification));
const curatedNotifications = notifications.nodes.map((notification) => curateNotification(notification));
return {
notifications: curatedNotifications,
@@ -148,7 +148,7 @@ function initUiActions(store, _router) {
rank
}
actors: searchActors(
search: $query,
query: $query,
first: $limit
) {
id
@@ -221,11 +221,9 @@ function initUiActions(store, _router) {
userId: store.state.auth.user?.id,
});
console.log(res.results);
return {
releases: res?.results.map(result => curateRelease(result.release)) || [],
actors: res?.actors.map(actor => curateActor(actor)) || [],
releases: res?.results.map((result) => curateRelease(result.release)) || [],
actors: res?.actors.map((actor) => curateActor(actor)) || [],
};
}

View File

@@ -3,7 +3,7 @@ async function initUiObservers(store, _router) {
body.classList.add(store.state.ui.theme);
store.watch(state => state.ui.theme, (newTheme, oldTheme) => {
store.watch((state) => state.ui.theme, (newTheme, oldTheme) => {
body.classList.add(newTheme);
body.classList.remove(oldTheme);
});

View File

@@ -57,7 +57,10 @@ function initUsersActions(store, _router) {
}
}
}
scenes: stashesScenes(first: 20) {
scenes: stashesScenes(
first: 20
orderBy: CREATED_AT_DESC
) {
comment
scene {
${releaseFields}
@@ -87,7 +90,7 @@ function initUsersActions(store, _router) {
${actorFields}
}
}
entity: alertsEntityByAlertId {
entity: alertsEntity {
entity {
id
name

View File

@@ -24,6 +24,7 @@ module.exports = {
saveUninitialized: false,
cookie: {
secure: true,
maxAge: 2629800000, // 1 month
},
},
},
@@ -34,7 +35,8 @@ module.exports = {
secretKey: 'abcdefghijklmnopqrstuvwxyz1234567890ABCD',
},
auth: {
enabled: true,
login: true,
signup: true,
},
exclude: {
channels: [
@@ -64,10 +66,7 @@ module.exports = {
// pornpros
'milfhumiliation',
'humiliated',
'flexiblepositions',
'publicviolations',
'amateurviolations',
'squirtdisgrace',
'cumdisgrace',
'webcamhackers',
'collegeteens',
@@ -77,6 +76,11 @@ module.exports = {
'pornmegaload',
'scorelandtv',
'scoretv',
// spizoo
'intimatelesbians',
'porngoespro',
'pornstartease',
'thestripperexperience',
// teenscoreclub
'maiko',
'ncuksinners',
@@ -89,6 +93,9 @@ module.exports = {
networks: [
// dummy network for testing
'traxxx',
// porn doe
'forbondage',
'score',
],
},
profiles: [
@@ -140,6 +147,11 @@ module.exports = {
'hotcrazymess',
'thatsitcomshow',
],
[
// Adult DVD Empire
'elegantangel',
'westcoastproductions',
],
'21sextury',
'julesjordan',
'dorcelclub',
@@ -237,6 +249,7 @@ module.exports = {
'www.tushy.com',
'www.tushyraw.com',
'www.deeper.com',
'www.slayed.com',
'sthw-trailer-vixen.ssl-cdn.com',
'sthw-trailer-tushy.ssl-cdn.com',
'sthw-trailer-tushyraw.ssl-cdn.com',
@@ -249,6 +262,30 @@ module.exports = {
'streamhw-trailer-blacked.ssl-cdn.com',
'streamhw-trailer-blackedraw.ssl-cdn.com',
'streamhw-trailer-deeper.ssl-cdn.com',
'cdn.vixen.com',
'cdn.tushy.com',
'cdn.blacked.com',
'cdn.deeper.com',
'cdn.blackedraw.com',
'cdn.tushyraw.com',
'cdn.slayed.com',
'www.vogov.com',
'www.vogov.com',
'www.nubiles.net',
'www.brattysis.com',
'www.deeplush.com',
'www.detentiongirls.com',
'www.daddyslilangel.com',
'www.myfamilypies.com',
'www.bountyhunterporn.com',
'www.anilos.com',
'www.thatsitcomshow.com',
'www.nubileset.com',
'www.nubilesunscripted.com',
'www.nubilefilms.com',
'www.badteenspunished.com',
'www.cumlouder.com',
'im0.imgcm.com',
],
},
limits: {
@@ -261,15 +298,26 @@ module.exports = {
interval: 1000,
concurrency: 1,
},
'www.realitykings.com': {
interval: 1000,
'westcoastproductions.com': {
interval: 100,
concurrency: 1,
},
'images.mylfcdn.net': {
interval: 50,
concurrency: 1,
},
},
fetchAfter: [1, 'week'],
missingDateLimit: 3,
memorySampling: {
enabled: false,
sampleDuration: 300000, // 5 minutes
snapshotIntervals: [],
},
media: {
path: './media',
maxSize: 1000,
quality: 80,
thumbnailSize: 320, // width for 16:9 will be exactly 576px
thumbnailQuality: 100,
lazySize: 90,

36768
docs/names.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
const config = require('config');
exports.up = knex => Promise.resolve()
exports.up = (knex) => Promise.resolve()
.then(() => knex.schema.createTable('countries', (table) => {
table.text('alpha2', 2)
.unique()
@@ -108,7 +108,7 @@ exports.up = knex => Promise.resolve()
.onDelete('cascade');
table.date('date');
table.enum('date_precision', ['year', 'month', 'day', 'hour', 'minute', 'second'])
table.enum('date_precision', ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'])
.defaultTo('year');
table.text('comment');
@@ -643,7 +643,7 @@ exports.up = knex => Promise.resolve()
.references('alpha2')
.inTable('countries');
table.enum('date_precision', ['year', 'month', 'day', 'hour', 'minute', 'second'])
table.enum('date_precision', ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'])
.defaultTo('day');
table.text('description');
@@ -667,6 +667,11 @@ exports.up = knex => Promise.resolve()
.onDelete('cascade');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
table.datetime('updated_at')
.notNullable()
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('releases_actors', (table) => {
@@ -787,7 +792,6 @@ exports.up = knex => Promise.resolve()
}))
.then(() => knex.schema.createTable('releases_tags', (table) => {
table.integer('tag_id', 12)
.notNullable()
.references('id')
.inTable('tags');
@@ -797,6 +801,8 @@ exports.up = knex => Promise.resolve()
.inTable('releases')
.onDelete('cascade');
table.text('original_tag');
table.unique(['tag_id', 'release_id']);
}))
.then(() => knex.schema.createTable('releases_search', (table) => {
@@ -827,7 +833,7 @@ exports.up = knex => Promise.resolve()
table.timestamp('date');
table.index('date');
table.enum('date_precision', ['year', 'month', 'day', 'hour', 'minute', 'second'])
table.enum('date_precision', ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'])
.defaultTo('day');
table.text('description');
@@ -905,10 +911,17 @@ exports.up = knex => Promise.resolve()
table.text('media_id', 21)
.notNullable()
.references('id')
.inTable('media');
.inTable('media')
.onDelete('cascade');
table.unique('movie_id');
}))
.then(() => knex.schema.createTable('movies_search', (table) => {
table.integer('movie_id', 16)
.references('id')
.inTable('movies')
.onDelete('cascade');
}))
.then(() => knex.schema.createTable('chapters', (table) => {
table.increments('id', 16);
@@ -973,7 +986,6 @@ exports.up = knex => Promise.resolve()
}))
.then(() => knex.schema.createTable('chapters_tags', (table) => {
table.integer('tag_id', 12)
.notNullable()
.references('id')
.inTable('tags')
.onDelete('cascade');
@@ -984,6 +996,8 @@ exports.up = knex => Promise.resolve()
.inTable('chapters')
.onDelete('cascade');
table.text('original_tag');
table.unique(['tag_id', 'chapter_id']);
}))
.then(() => knex.schema.createTable('users_roles', (table) => {
@@ -1268,11 +1282,107 @@ exports.up = knex => Promise.resolve()
.notNullable()
.defaultTo(knex.fn.now());
}))
// SEARCH
.then(() => knex.schema.createTable('affiliates', (table) => {
table.string('id')
.primary()
.unique()
.notNullable();
table.integer('entity_id', 12)
.references('id')
.inTable('entities');
table.text('url');
table.text('parameters');
table.unique(['entity_id', 'url']);
table.unique(['entity_id', 'parameters']);
table.text('comment');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('banners', (table) => {
table.string('id')
.primary()
.unique()
.notNullable();
table.string('type')
.defaultTo('jpg');
table.integer('width')
.notNullable();
table.integer('height')
.notNullable();
table.integer('entity_id', 12)
.references('id')
.inTable('entities');
table.text('comment');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('banners_tags', (table) => {
table.increments('id');
table.string('banner_id')
.notNullable()
.references('id')
.inTable('banners');
table.integer('tag_id')
.notNullable()
.references('id')
.inTable('tags');
table.unique(['banner_id', 'tag_id']);
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('campaigns', (table) => {
table.increments('id');
table.integer('entity_id', 12)
.notNullable()
.references('id')
.inTable('entities');
table.text('affiliate_id')
.references('id')
.inTable('affiliates');
table.text('url');
table.string('banner_id')
.references('id')
.inTable('banners');
table.text('comment');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
}))
// SEARCH AND SORT
.then(() => { // eslint-disable-line arrow-body-style
// allow vim fold
return knex.raw(`
ALTER TABLE releases_search ADD COLUMN document tsvector;
ALTER TABLE movies_search ADD COLUMN document tsvector;
/* allow scenes without dates to be mixed inbetween scenes with dates */
ALTER TABLE releases
ADD COLUMN effective_date timestamptz
GENERATED ALWAYS AS (COALESCE(date, created_at)) STORED;
`);
})
// INDEXES
@@ -1282,14 +1392,23 @@ exports.up = knex => Promise.resolve()
CREATE UNIQUE INDEX unique_actor_slugs_network ON actors (slug, entity_id, entry_id);
CREATE UNIQUE INDEX unique_actor_slugs ON actors (slug) WHERE entity_id IS NULL;
CREATE UNIQUE INDEX unique_entity_campaigns_banner_url ON campaigns (entity_id, url, banner_id) WHERE affiliate_id IS NULL;
CREATE UNIQUE INDEX unique_entity_campaigns_url ON campaigns (entity_id, url) WHERE banner_id IS NULL AND affiliate_id IS NULL;
CREATE UNIQUE INDEX unique_entity_campaigns_banner_affiliate ON campaigns (entity_id, affiliate_id, banner_id) WHERE url IS NULL;
CREATE UNIQUE INDEX unique_entity_campaigns_affiliate ON campaigns (entity_id, affiliate_id) WHERE banner_id IS NULL AND url IS NULL;
CREATE UNIQUE INDEX releases_search_unique ON releases_search (release_id);
CREATE UNIQUE INDEX movies_search_unique ON movies_search (movie_id);
CREATE INDEX releases_search_index ON releases_search USING GIN (document);
CREATE INDEX movies_search_index ON movies_search USING GIN (document);
`);
})
// FUNCTIONS
.then(() => { // eslint-disable-line arrow-body-style
// allow vim fold
return knex.raw(`
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE FUNCTION current_user_id() RETURNS INTEGER AS $$
/* if the user ID is undefined, the adapter will pass it as a string, which cannot be cast as NULL by ::integer */
SELECT NULLIF(current_setting('user.id', true), '')::integer;
@@ -1299,6 +1418,7 @@ exports.up = knex => Promise.resolve()
* Using a table as a proxy for the search results allows us to get both a reference to the releases table, and the ranking.
* A composite type does not seem to be compatible with PostGraphile's @sortable, and a view does not allow for many native constraints */
CREATE TABLE releases_search_results (release_id integer, rank real, FOREIGN KEY (release_id) REFERENCES releases (id));
CREATE TABLE movies_search_results (movie_id integer, rank real, FOREIGN KEY (movie_id) REFERENCES movies (id));
CREATE FUNCTION search_releases(query text) RETURNS SETOF releases_search_results AS $$
SELECT releases.id, ranks.rank FROM (
@@ -1312,6 +1432,18 @@ exports.up = knex => Promise.resolve()
ORDER BY ranks.rank DESC;
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_movies(query text) RETURNS SETOF movies_search_results AS $$
SELECT movies.id, ranks.rank FROM (
SELECT
movies_search.movie_id,
ts_rank(movies_search.document, to_tsquery('english', array_to_string(array(SELECT * FROM regexp_matches(query, '[A-Za-zÀ-ÖØ-öø-ÿ0-9]+', 'g')), '|'))) AS rank
FROM movies_search
) ranks
LEFT JOIN movies ON movies.id = ranks.movie_id
WHERE ranks.rank > 0
ORDER BY ranks.rank DESC;
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_entities(search text) RETURNS SETOF entities AS $$
SELECT * FROM entities
WHERE
@@ -1320,25 +1452,29 @@ exports.up = knex => Promise.resolve()
array_to_string(alias, '') ILIKE ('%' || TRIM(search) || '%') OR
replace(array_to_string(alias, ''), ' ', '') ILIKE ('%' || TRIM(search) || '%') OR
url ILIKE ('%' || search || '%')
ORDER BY similarity(entities.name, search) DESC, name ASC
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_actors(search text, min_length smallint DEFAULT 2) RETURNS SETOF actors AS $$
CREATE FUNCTION search_actors(query text, min_length smallint DEFAULT 2) RETURNS SETOF actors AS $$
SELECT * FROM actors
WHERE length(search) >= min_length
AND name ILIKE ('%' || TRIM(search) || '%')
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_tags(search text, min_length smallint DEFAULT 2, is_primary boolean DEFAULT true) RETURNS SETOF tags AS $$
SELECT * FROM tags
WHERE length(search) >= min_length
AND name ILIKE ('%' || TRIM(search) || '%')
WHERE query IS NULL
OR length(query) >= min_length
AND CASE
WHEN is_primary
THEN tags.alias_for IS NULL
WHEN length(query) > 1 THEN name ILIKE ('%' || TRIM(query) || '%')
WHEN length(query) = 1 THEN name ILIKE (TRIM(query) || '%')
ELSE true
END
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_tags(query text, min_length smallint DEFAULT 2) RETURNS SETOF tags AS $$
SELECT aliases.* FROM tags
LEFT JOIN tags AS aliases ON aliases.id = tags.alias_for OR (tags.alias_for IS NULL AND aliases.id = tags.id)
WHERE length(query) >= min_length
AND tags.name ILIKE ('%' || TRIM(query) || '%')
GROUP BY aliases.id
ORDER BY similarity(aliases.slug, query) DESC, slug ASC
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION actors_tags(actor actors, selectable_tags text[]) RETURNS SETOF tags AS $$
SELECT tags.*
FROM releases_actors
@@ -1470,6 +1606,16 @@ exports.up = knex => Promise.resolve()
CREATE FUNCTION releases_is_new(release releases) RETURNS boolean AS $$
SELECT EXISTS(SELECT true WHERE (SELECT id FROM batches ORDER BY created_at DESC LIMIT 1) = release.created_batch_id);
$$ LANGUAGE sql STABLE;
CREATE FUNCTION banners_ratio(banner banners) RETURNS numeric AS $$
SELECT ROUND(banner.width::decimal / banner.height::decimal, 2);
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION get_random_campaign() RETURNS SETOF campaigns AS $$
SELECT * FROM campaigns
ORDER BY random()
LIMIT 1;
$$ LANGUAGE sql STABLE;
`);
})
// POLICIES
@@ -1603,8 +1749,11 @@ exports.up = knex => Promise.resolve()
COMMENT ON FUNCTION actors_actors IS E'@sortable';
COMMENT ON FUNCTION actors_scenes IS E'@sortable';
COMMENT ON FUNCTION tags_scenes IS E'@sortable';
COMMENT ON FUNCTION search_releases IS E'@sortable';
COMMENT ON FUNCTION search_entities IS E'@sortable';
COMMENT ON FUNCTION search_actors IS E'@sortable';
COMMENT ON FUNCTION search_movies IS E'@sortable';
COMMENT ON FUNCTION search_tags IS E'@sortable';
`);
});
@@ -1622,6 +1771,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS releases_teasers CASCADE;
DROP TABLE IF EXISTS releases_tags CASCADE;
DROP TABLE IF EXISTS releases_search CASCADE;
DROP TABLE IF EXISTS movies_search CASCADE;
DROP TABLE IF EXISTS movies_scenes CASCADE;
DROP TABLE IF EXISTS movies_covers CASCADE;
@@ -1636,6 +1786,11 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS chapters_posters CASCADE;
DROP TABLE IF EXISTS chapters_photos CASCADE;
DROP TABLE IF EXISTS banners_tags CASCADE;
DROP TABLE IF EXISTS banners CASCADE;
DROP TABLE IF EXISTS campaigns CASCADE;
DROP TABLE IF EXISTS affiliates CASCADE;
DROP TABLE IF EXISTS batches CASCADE;
DROP TABLE IF EXISTS actors_avatars CASCADE;
@@ -1648,6 +1803,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS entities_tags CASCADE;
DROP TABLE IF EXISTS entities_social CASCADE;
DROP TABLE IF EXISTS sites_tags CASCADE;
DROP TABLE IF EXISTS sites_social CASCADE;
DROP TABLE IF EXISTS networks_social CASCADE;
@@ -1692,6 +1848,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP FUNCTION IF EXISTS search_sites;
DROP FUNCTION IF EXISTS search_entities;
DROP FUNCTION IF EXISTS search_actors;
DROP FUNCTION IF EXISTS search_movies;
DROP FUNCTION IF EXISTS search_tags;
DROP FUNCTION IF EXISTS get_random_sfw_media_id;
@@ -1710,7 +1867,9 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP POLICY IF EXISTS stashes_policy ON stashes_actors;
DROP FUNCTION IF EXISTS current_user_id;
DROP FUNCTION IF EXISTS get_random_campaign;
DROP TABLE IF EXISTS releases_search_results;
DROP TABLE IF EXISTS movies_search_results;
`);
};

View File

@@ -0,0 +1,29 @@
exports.up = async (knex) => knex.raw(`
CREATE FUNCTION entities_scenes(entity entities) RETURNS SETOF releases AS $$
WITH RECURSIVE children AS (
SELECT entities.id
FROM entities
WHERE entities.id = entity.id
UNION ALL
SELECT entities.id
FROM entities
INNER JOIN children ON children.id = entities.parent_id
)
SELECT releases FROM releases
INNER JOIN children ON children.id = releases.entity_id
UNION
SELECT releases FROM releases
INNER JOIN children ON children.id = releases.studio_id;
$$ LANGUAGE SQL STABLE;
COMMENT ON FUNCTION entities_scenes IS E'@sortable';
`);
exports.down = async (knex) => knex.raw(`
DROP FUNCTION IF EXISTS entities_scenes;
`);

View File

@@ -0,0 +1,15 @@
exports.up = async (knex) => Promise.resolve()
.then(() => knex.schema.alterTable('releases_tags', (table) => {
table.index('release_id');
}))
.then(() => knex.schema.alterTable('movies_scenes', (table) => {
table.index('scene_id');
}));
exports.down = async (knex) => Promise.resolve()
.then(() => knex.schema.alterTable('releases_tags', (table) => {
table.dropIndex('release_id');
}))
.then(() => knex.schema.alterTable('movies_scenes', (table) => {
table.dropIndex('scene_id');
}));

View File

@@ -0,0 +1,11 @@
exports.up = async (knex) => knex.raw(`
CREATE OR REPLACE FUNCTION entities_scene_total(entity entities) RETURNS integer AS $$
SELECT COUNT(id)
FROM releases
WHERE releases.entity_id = entity.id;
$$ LANGUAGE SQL STABLE;
`);
exports.down = async (knex) => knex.raw(`
DROP FUNCTION IF EXISTS entities_scene_total;
`);

View File

@@ -0,0 +1,8 @@
exports.up = async (knex) => knex.raw(`
CREATE VIEW movies_tagged AS
SELECT * FROM movies;
`);
exports.down = async (knex) => knex.raw(`
DROP VIEW IF EXISTS movies_tagged;
`);

29684
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "traxxx",
"version": "1.193.3",
"version": "1.209.4",
"description": "All the latest porn releases in one place",
"main": "src/app.js",
"scripts": {
"start": "node -r source-map-support/register src/init.js",
"webpack": "webpack --env=production --mode=production",
"webpack-dev": "webpack --env=development --mode=development",
"webpack-watch": "webpack --progress --colors --watch --env=development --mode=development",
"webpack-watch": "webpack --progress --color --watch --env=development --mode=development",
"babel": "babel src --source-maps -d dist",
"babel-watch": "babel src -w --source-maps -d dist",
"build": "babel src --source-maps -d dist && webpack --env=production --mode=production",
@@ -40,38 +40,37 @@
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.8.4",
"@babel/eslint-parser": "^7.16.0",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/register": "^7.8.3",
"@vue/compiler-sfc": "^3.0.4",
"autoprefixer": "^9.7.4",
"babel-eslint": "^10.1.0",
"autoprefixer": "^10.4.0",
"babel-loader": "^8.0.6",
"babel-preset-airbnb": "^3.3.2",
"css-loader": "^5.0.1",
"eslint": "^7.20.0",
"eslint-config-airbnb": "^17.1.1",
"eslint-config-airbnb-base": "^13.2.0",
"babel-preset-airbnb": "^5.0.0",
"css-loader": "^6.5.0",
"eslint": "^8.1.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-vue": "^6.2.1",
"eslint-watch": "^4.0.2",
"eslint-webpack-plugin": "^2.5.2",
"mini-css-extract-plugin": "^1.3.3",
"node-sass": "^5.0.0",
"postcss-loader": "^3.0.0",
"eslint-plugin-vue": "^8.0.3",
"eslint-watch": "^7.0.0",
"eslint-webpack-plugin": "^3.1.0",
"mini-css-extract-plugin": "^2.4.3",
"node-sass": "^6.0.1",
"postcss-loader": "^6.2.0",
"raw-loader": "^4.0.2",
"sass-loader": "^11.0.1",
"style-loader": "^0.23.1",
"vue-loader": "^16.1.2",
"sass-loader": "^12.3.0",
"style-loader": "^3.3.1",
"vue-loader": "^16.8.2",
"webpack": "^5.11.0",
"webpack-cli": "^3.3.11"
"webpack-cli": "^4.9.1"
},
"dependencies": {
"@casl/ability": "^5.2.2",
"@graphile-contrib/pg-order-by-related": "^1.0.0-beta.6",
"@graphile-contrib/pg-simplify-inflector": "^5.0.0-beta.1",
"@graphile-contrib/pg-simplify-inflector": "^6.1.0",
"acorn": "^8.0.4",
"array-equal": "^1.0.0",
"aws-sdk": "^2.847.0",
@@ -88,61 +87,62 @@
"cloudscraper": "^4.6.0",
"config": "^3.2.5",
"connect-session-knex": "^2.0.0",
"convert": "^1.6.2",
"convert": "^4.2.4",
"cookie": "^0.4.0",
"csv-stringify": "^5.3.6",
"dayjs": "^1.8.21",
"dompurify": "^2.0.11",
"ejs": "^3.0.1",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"express-promise-router": "^4.1.0",
"express-react-views": "^0.11.0",
"express-session": "^1.17.1",
"face-api.js": "^0.22.2",
"faker": "^5.1.0",
"file-type": "^14.1.4",
"file-type": "^16.5.3",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.1",
"graphile-utils": "^4.5.6",
"graphql": "^14.6.0",
"iconv-lite": "^0.5.1",
"inquirer": "^7.3.3",
"jsdom": "^16.3.0",
"knex": "^0.21.13",
"fs-extra": "^10.0.0",
"graphile-utils": "^4.12.2",
"graphql": "^15.4.0",
"html-entities": "^2.3.2",
"iconv-lite": "^0.6.3",
"inquirer": "^8.2.0",
"inspector-api": "^1.4.2",
"jsdom": "^18.0.0",
"knex": "^0.95.12",
"knex-migrate": "^1.7.4",
"longjohn": "^0.2.12",
"mime": "^2.4.4",
"mitt": "^2.1.0",
"mitt": "^3.0.0",
"moment": "^2.24.0",
"nanoid": "^2.1.11",
"object-merge-advanced": "^10.12.1",
"nanoid": "^3.1.30",
"node-fetch": "^2.6.7",
"object-merge-advanced": "^12.1.0",
"object.omit": "^3.0.0",
"opn": "^5.5.0",
"opn": "^6.0.0",
"pg": "^8.5.1",
"postgraphile": "^4.10.0",
"postgraphile-plugin-connection-filter": "^1.1.3",
"postgraphile-plugin-connection-filter": "^2.2.2",
"promise-task-queue": "^1.2.0",
"prop-types": "^15.7.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"sharp": "^0.27.2",
"sharp": "^0.29.2",
"showdown": "^1.9.1",
"source-map-support": "^0.5.16",
"template-format": "^1.2.5",
"tippy.js": "^6.3.1",
"tough-cookie": "^3.0.1",
"tty-table": "^2.8.12",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"undici": "^4.13.0",
"url-pattern": "^1.0.3",
"v-tooltip": "^2.0.3",
"video.js": "^7.11.4",
"videojs-vr": "^1.7.1",
"vue": "^3.0.4",
"vue-router": "^4.0.1",
"vuex": "^4.0.0-rc.2",
"vue": "^3.2.20",
"vue-router": "^4.0.12",
"vuex": "^4.0.2",
"why-is-node-running": "^2.2.0",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^4.4.2",
"yargs": "^13.3.0"
"yargs": "^17.2.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Some files were not shown because too many files have changed in this diff Show More