Merge branch 'experimental'

This commit is contained in:
DebaucheryLibrarian 2020-08-15 19:04:47 +02:00
commit bd6396d7a8
730 changed files with 3958 additions and 1265 deletions

View File

@ -18,12 +18,44 @@ Do not modify `config/default.js`, but instead create a copy at `config/local.js
You can also use `npm run flush` to run both steps at once, and wipe the database completely later.
#### Networks and channels
To scrape the networks and channels available in the database, you can configure `include` and `exclude` lists. To include all available channels and only use the `exclude` list, leave the `include` parameter unconfigured. The `exclude` lists will exclude channels and child networks from networks on the `include` lists, but not vice versa. That is, if the `include` list includes a network and the `exclude` list excludes one of that network's channels, the channel will not be scraped. However, if the `include` list includes a channel, and the `exclude` list includes its parent network, the channel will be scraped.
This configuration will scrape Evil Angel and all XEmpire channels, except for LesbianX.
```
include: {
networks: [
'xempire',
],
channels: [
'evilangel',
],
},
exclude: {
channels: [
'lesbianx',
],
}
```
This configuration will scrape all channels, except for BAM Visions, and except all channels part of the Vixen network.
```
exclude: {
channels: [
'bamvisions',
],
networks: [
'vixen'
],
},
```
### Building
To build traxxx, run the following command:
`npm run build`
To generate the thumbnails for logos and tag photos, run:
To generate thumbnails for logos and tag photos, install ImageMagick and run:
`npm run logos-thumbs`
@ -33,13 +65,24 @@ To generate the thumbnails for logos and tag photos, run:
`./traxxx --option value` or `npm start -- --option value`
* `--server`: Run the web server
* `--all`: Fetch updates from the channels and networks in the configuration file.
* `--channel [slug] [slug]`: Fetch updates from specific channels. The slug is the channel's name in lowercase and without cases or special characters. For example, Teens Like It Big is teenslikeitbig.
* `--network [slug] [slug]`: Fetch updates from all sites of a specific network. The network slug is composed similarly to the channel slug.
#### Channels
* `--channels [slug] [slug]`: Fetch updates from specific channels. The slug is the channel's name in lowercase and without cases or special characters. For example, Teens Like It Big is teenslikeitbig. Overrides configured included networks and channels.
* `--networks [slug] [slug]`: Fetch updates from all sites of a specific network. The network slug is composed similarly to the channel slug. Overrides configured included networks and channels.
* `--exclude-channels [slug] [slug]`: Scrape every configured, specified or available channel, except for specified. Overrides configured excluded channels.
* `--exclude-networks [slug] [slug]`: Scrape every configured, specified or available network, except for specified. Overrides configured excluded networks.
* `--after "[time]"`: Do not fetch scenes older than this period or date. Example values are: `"1 month"`, `"3 years"`, `"2019-01-01"`.
* `--scene [URL]`: Try to retrieve scene details from its official channel or network URL.
* `--deep`: Follow each release link found running `--channel` or `--network` and scrape it for more details. Enabled by default ; use `--no-deep` to only save information found on the overview pages.
#### Actors
* `--actors "[name]" "[name]"`: Fetch actor profiles. When no names are specified, actors without existing profiles are scraped
* `--actors-file [filepath]`: Fetch all scenes for the actors specified in a file using a newline delimiter.
* `--actors-sources [slug] [slug]`: Scrapers to use for actor profiles. Defaults to config.
* `--actors-update [time]`: Update actors that don't have any profiles newer than period ("1 month") or date (2020-08-01). Using this argument without a value will default to 1900-01-01, practically updating all actors.
* `--actors-scenes`: Fetch all scenes for scraped actors. Use with caution, as an actor may have many scenes.
* `--scene-actors`: Fetch profiles for actors associated with scraped scenes. Use with caution, as scenes may have many actors, each with many profiles.
#### Developers
* `--no-save`: Do not store retrieved information in local database, forcing re-fetch.
* `--level`: Change log level to `silly`, `verbose`, `info`, `warn` or `error`.

View File

@ -29,7 +29,7 @@
/>
</div>
<div class="actor-inner">
<div class="content-inner actor-inner">
<div
class="profile"
:class="{ expanded: bioExpanded, 'with-avatar': !!actor.avatar }"
@ -296,6 +296,7 @@
:items-total="totalCount"
:items-per-page="limit"
:available-tags="actor.tags"
:available-channels="actor.channels"
/>
<Releases :releases="releases" />
@ -306,13 +307,15 @@
class="pagination-top"
/>
</div>
<Footer />
</div>
</div>
</template>
<script>
import Pagination from '../pagination/pagination.vue';
import FilterBar from '../header/filter-bar.vue';
import FilterBar from '../filters/filter-bar.vue';
import Releases from '../releases/releases.vue';
import Photos from './photos.vue';
import Expand from '../expand/expand.vue';
@ -402,7 +405,7 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--highlight-extreme);
color: var(--lighten-extreme);
background: var(--profile);
padding: .75rem 1rem;
}
@ -429,7 +432,7 @@ export default {
.profile {
background: var(--profile);
color: var(--highlight-extreme);
color: var(--lighten-extreme);
width: 100%;
max-height: 18rem;
display: flex;
@ -486,7 +489,7 @@ export default {
overflow: hidden;
&:not(:last-of-type) {
border-bottom: solid 1px var(--highlight-hint);
border-bottom: solid 1px var(--lighten-hint);
}
}
@ -497,14 +500,14 @@ export default {
}
.bio-label {
color: var(--highlight);
color: var(--lighten);
margin: 0 1rem 0 0;
flex-shrink: 0;
font-style: normal;
font-weight: 400;
.icon {
fill: var(--highlight);
fill: var(--lighten);
margin: -.25rem .5rem 0 0;
}
}
@ -538,7 +541,7 @@ export default {
.age {
font-weight: bold;
padding: 0 0 0 .5rem;
border-left: solid 1px var(--highlight-weak);
border-left: solid 1px var(--lighten-weak);
margin: 0 0 0 .5rem;
}
@ -554,7 +557,7 @@ export default {
.height-imperial,
.weight-imperial {
padding: 0 0 0 .5rem;
border-left: solid 1px var(--highlight-weak);
border-left: solid 1px var(--lighten-weak);
margin: 0 0 0 .5rem;
}
@ -571,7 +574,7 @@ export default {
}
.scraped {
color: var(--highlight-weak);
color: var(--lighten-weak);
font-size: .8rem;
}
@ -713,8 +716,13 @@ export default {
margin: 1rem 0 0 0;
}
.actor-header {
padding: .5rem 1rem;
}
.header-name {
flex-grow: 1;
font-size: 1.3rem;
}
}
</style>

View File

@ -2,6 +2,13 @@
<div class="actors">
<nav class="filter">
<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 } }"
@ -33,13 +40,6 @@
replace
><Icon icon="question5" /></router-link>
</li>
<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>
</ul>
<ul class="letters nolist">
@ -80,6 +80,8 @@
:items-per-page="limit"
class="pagination-top"
/>
<Footer />
</div>
</template>
@ -107,7 +109,7 @@ function letter() {
}
function gender() {
return this.$route.params.gender || 'female';
return this.$route.params.gender || 'all';
}
async function route() {

View File

@ -3,10 +3,12 @@
class="container"
:class="theme"
>
<Sidebar
v-if="showSidebar"
:toggle-sidebar="toggleSidebar"
/>
<transition name="slide">
<Sidebar
v-if="showSidebar"
@toggle="(state) => showSidebar = state"
/>
</transition>
<Header :toggle-sidebar="toggleSidebar" />
@ -36,7 +38,6 @@ function toggleSidebar(state) {
function mounted() {
document.addEventListener('click', () => {
EventBus.$emit('blur');
this.showSidebar = false;
});
}
@ -88,4 +89,34 @@ export default {
overflow-y: auto;
overflow-x: hidden;
}
.slide-enter-active,
.slide-leave-active {
&.sidebar-container {
transition: background .2s ease-in-out;
}
.sidebar {
transition: transform .2s ease-in-out;
}
}
.slide-enter,
.slide-leave-to {
&.sidebar-container {
background: transparent;
}
.sidebar {
transform: translate(-100%, 0);
}
}
.column {
width: 1200px;
max-width: 100%;
padding: 0 1rem;
margin: 0 auto;
box-sizing: border-box;
}
</style>

View File

@ -35,7 +35,10 @@
class="name"
>{{ entity.name }}</h2>
<Icon icon="share2" />
<Icon
v-if="entity.url"
icon="share2"
/>
</a>
<ul
@ -103,6 +106,8 @@
class="pagination-top"
/>
</div>
<Footer />
</div>
</div>
</template>
@ -110,7 +115,7 @@
<script>
import Vue from 'vue';
import FilterBar from '../header/filter-bar.vue';
import FilterBar from '../filters/filter-bar.vue';
import Pagination from '../pagination/pagination.vue';
import Releases from '../releases/releases.vue';
import Children from './children.vue';
@ -156,7 +161,7 @@ export default {
return {
entity: null,
totalCount: null,
limit: 20,
limit: Number(this.$route.query.limit) || 20,
expanded: false,
};
},

View File

@ -0,0 +1,144 @@
<template>
<v-popover class="filter-container">
<div class="filter">
<Icon icon="antenna" />
<div
v-if="selectedChannels.length > 0"
class="filter-applied"
>{{ selectedChannels.length }} {{ selectedChannels.length > 1 ? 'channels' : 'channel' }}</div>
<div
v-else
class="filter-applied empty"
>Channels</div>
</div>
<div slot="popover">
<router-link
class="filter-clear"
:to="{ query: { ...$route.query, channels: undefined } }"
:class="{ active: selectedChannels.length > 0 }"
>clear all<Icon icon="cross2" /></router-link>
<ul class="filter-items nolist">
<li
v-for="channel in channelsPerNetwork"
:key="`channel-${channel.id}`"
class="filter-item"
:class="{ [channel.type]: true, independent: channel.independent }"
>
<router-link
:to="{ query: { ...$route.query, channels: channel.slug, mode }, params: { pageNumber: 1 } }"
class="filter-name"
>
<img
v-if="channel.independent || !channel.parent"
:src="`/img/logos/${channel.slug}/favicon.png`"
class="favicon"
>
{{ channel.name }}
</router-link>
<router-link
:to="{ query: { ...$route.query, ...getNewRange(channel.slug), mode }, params: { pageNumber: 1 } }"
class="filter-include"
:class="{ selected: selectedChannels.includes(channel.slug) }"
>
<Icon
icon="checkmark"
class="filter-add"
/>
<Icon
icon="cross2"
class="filter-remove"
/>
</router-link>
</li>
</ul>
</div>
</v-popover>
</template>
<script>
function getNewRange(channel) {
if (this.selectedChannels.includes(channel)) {
return { channels: this.selectedChannels.filter(selectedTag => selectedTag !== channel).join(',') || undefined };
}
return { channels: this.selectedChannels.concat(channel).join(',') };
}
function selectedChannels() {
return this.$route.query.channels ? this.$route.query.channels.split(',') : [];
}
function channelsPerNetwork() {
const networks = this.availableChannels.reduce((acc, channel) => {
if (channel.independent || !channel.parent) {
acc[channel.slug] = { ...channel, children: [] };
return acc;
}
if (!acc[channel.parent.slug]) {
acc[channel.parent.slug] = { ...channel.parent, children: [] };
}
acc[channel.parent.slug].children.push(channel);
return acc;
}, {});
return Object.values(networks).reduce((acc, network) => [...acc, network, ...(network.children || [])], []);
}
export default {
props: {
filter: {
type: Array,
default: () => [],
},
compact: {
type: Boolean,
default: false,
},
availableChannels: {
type: Array,
default: () => [],
},
},
data() {
return {
mode: this.$route.query.mode || 'all',
};
},
computed: {
channelsPerNetwork,
selectedChannels,
},
methods: {
getNewRange,
},
};
</script>
<style lang="scss" scoped>
.favicon {
width: 1rem;
height: 1rem;
padding: 0 .75rem 0 0;
filter: drop-shadow(0 0 1px var(--darken));
}
.network .filter-name,
.independent .filter-name {
font-weight: bold;
padding: .5rem;
}
.channel:not(.independent) .filter-name {
padding: .5rem .5rem .5rem 2.25rem;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="filter-bar noselect">
<span class="sort">
<div class="sort">
<router-link
:to="{ params: { range: 'latest', pageNumber: 1 } }"
:class="{ active: $route.name === 'latest' || range === 'latest' }"
@ -18,20 +18,28 @@
:class="{ active: $route.name === 'new' || range === 'new' }"
class="range-button"
>New</router-link>
</span>
</div>
<Filters
class="filters"
:filter="filter"
:available-tags="availableTags"
@set-filter="setFilter"
/>
<div class="filters">
<ChannelFilter
class="filters-filter"
:filter="filter"
:available-channels="availableChannels"
/>
<TagFilter
class="filters-filter"
:filter="filter"
:available-tags="availableTags"
/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import Filters from './filters.vue';
import ChannelFilter from './channel-filter.vue';
import TagFilter from './tag-filter.vue';
function filter(state) {
return state.ui.filter;
@ -45,12 +53,6 @@ function batch(state) {
return state.ui.batch;
}
async function setFilter(newFilter) {
this.$store.dispatch('setFilter', newFilter);
await this.fetchReleases();
}
async function setRange(newRange) {
this.$store.dispatch('setRange', newRange);
@ -65,7 +67,8 @@ async function setBatch(newBatch) {
export default {
components: {
Filters,
ChannelFilter,
TagFilter,
},
props: {
fetchReleases: {
@ -88,6 +91,10 @@ export default {
type: Array,
default: () => [],
},
availableChannels: {
type: Array,
default: () => [],
},
},
computed: {
...mapState({
@ -97,13 +104,164 @@ export default {
}),
},
methods: {
setFilter,
setRange,
setBatch,
},
};
</script>
<style lang="scss">
@import 'breakpoints';
.filter {
color: var(--shadow);
display: inline-flex;
align-items: center;
.icon {
fill: var(--shadow);
margin: -.1rem 0 0 0;
}
&:hover {
cursor: pointer;
.applied {
color: var(--shadow-strong);
}
.icon {
fill: var(--shadow-strong);
}
}
}
.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);
}
}
.filter-mode {
width: 100%;
background: none;
padding: .75rem;
margin: 0 0 .5rem 0;
font-size: 1rem;
border: none;
border-bottom: solid 1px var(--shadow-hint);
}
.filter-clear {
display: flex;
align-items: center;
justify-content: space-between;
padding: .5rem 1rem;
color: var(--darken-weak);
text-decoration: none;
cursor: default;
.icon {
fill: var(--darken-hint);
margin: 0 0 0 1rem;
}
&.active {
color: var(--darken);
.icon {
fill: var(--darken-weak);
}
&:hover {
color: var(--text);
background: var(--darken-hint);
cursor: pointer;
.icon {
fill: var(--alert);
}
}
}
}
.filter-items .filter-item {
display: flex;
align-items: center;
&:hover {
background: var(--darken-hint);
cursor: pointer;
}
&.selected .filter-include .filter-add {
fill: var(--success);
}
}
.filter-include {
.icon {
width: 1rem;
height: 1rem;
padding: .5rem 1rem;
fill: var(--darken-hint);
}
.filter-remove {
display: none;
fill: var(--alert);
}
&:hover {
cursor: pointer;
&.selected .filter-add {
display: none;
}
&.selected .filter-remove {
display: inline-block;
}
}
}
.filter-name {
min-width: 8rem;
display: flex;
flex-grow: 1;
padding: .5rem .75rem .5rem 1rem;
color: var(--text);
text-decoration: none;
}
.filter-include:hover,
.filter-name:hover {
background: var(--darken-hint);
}
@media(max-width: $breakpoint-micro) {
.filter-applied {
display: none;
}
.filters-filter:not(:last-child) .filter {
padding: .5rem;
}
.filters-filter:last-child .filter {
padding: .5rem 0 .5rem .5rem;
}
}
</style>
<style lang="scss" scoped>
@import 'theme';
@ -162,8 +320,12 @@ export default {
}
}
.filters {
.filters-filter {
display: inline-block;
flex-shrink: 0;
&:not(:last-child) {
margin: 0 1rem 0 0;
}
}
</style>

View File

@ -0,0 +1,111 @@
<template>
<v-popover class="filter-container">
<div class="filter">
<Icon icon="price-tag4" />
<div
v-if="selectedTags.length > 0"
class="filter-applied"
>{{ selectedTags.length }} {{ selectedTags.length > 1 ? 'tags' : 'tag' }}</div>
<div
v-else
class="filter-applied empty"
>Tags</div>
</div>
<div slot="popover">
<select
v-model="mode"
class="filter-mode"
@change="$router.push({ query: { ...$route.query, mode }, params: { pageNumber: 1 } })"
>
<option value="all">match all selected</option>
<option value="any">match any selected</option>
</select>
<router-link
class="filter-clear"
:to="{ query: { ...$route.query, tags: undefined, mode: undefined } }"
:class="{ active: selectedTags.length > 0 }"
>clear all<Icon icon="cross2" /></router-link>
<ul class="filter-items nolist">
<li
v-for="tag in availableTags"
:key="`tag-${tag.id}`"
class="filter-item"
:class="{ selected: selectedTags.includes(tag.slug) }"
>
<router-link
:to="{ query: { ...$route.query, tags: tag.slug, mode }, params: { pageNumber: 1 } }"
class="filter-name"
>{{ tag.name }}</router-link>
<router-link
:to="{ query: { ...$route.query, ...getNewRange(tag.slug), mode }, params: { pageNumber: 1 } }"
class="filter-include"
:class="{ selected: selectedTags.includes(tag.slug) }"
>
<Icon
icon="checkmark"
class="filter-add"
/>
<Icon
icon="cross2"
class="filter-remove"
/>
</router-link>
</li>
</ul>
</div>
</v-popover>
</template>
<script>
function getNewRange(tag) {
if (this.selectedTags.includes(tag)) {
return { tags: this.selectedTags.filter(selectedTag => selectedTag !== tag).join(',') || undefined };
}
return { tags: this.selectedTags.concat(tag).join(',') };
}
function selectedTags() {
return this.$route.query.tags ? this.$route.query.tags.split(',') : [];
}
export default {
props: {
filter: {
type: Array,
default: () => [],
},
compact: {
type: Boolean,
default: false,
},
availableTags: {
type: Array,
default: () => [],
},
},
data() {
return {
mode: this.$route.query.mode || 'all',
};
},
computed: {
selectedTags,
},
methods: {
getNewRange,
},
};
</script>
<style lang="scss" scoped>
@import 'theme';
</style>

View File

@ -0,0 +1,34 @@
<template>
<footer class="footer">
<span class="segment">© traxxx</span>
<router-link
:to="{ name: 'stats' }"
class="segment footer-link nolink"
>stats</router-link>
</footer>
</template>
<style lang="scss" scoped>
.footer {
margin: 2rem 0 0 0;
background: var(--background);
color: var(--shadow);
box-shadow: inset 0 1px 3px var(--darken-hint);
font-size: .8rem;
font-weight: bold;
text-align: center;
}
.segment {
padding: .5rem;
&:not(:last-child) {
border-right: solid 1px var(--shadow-hint);
}
}
.footer-link {
text-decoration: underline;
}
</style>

View File

@ -1,200 +0,0 @@
<template>
<v-popover class="filters-container">
<div class="filters">
<div
v-if="selectedTags.length > 0"
class="applied"
>{{ selectedTags.length }} selected</div>
<div
v-else
class="applied empty"
>Filter by tags</div>
<Icon icon="filter" />
</div>
<div slot="popover">
<select
v-model="mode"
class="mode"
@change="$router.push({ query: { ...$route.query, mode }, params: { pageNumber: 1 } })"
>
<option
value="all"
class="option"
>match all selected</option>
<option
value="any"
class="option"
>match any selected</option>
</select>
<ul class="tags nolist">
<li
v-for="tag in availableTags"
:key="`tag-${tag.id}`"
class="tag"
:class="{ selected: selectedTags.includes(tag.slug) }"
>
<router-link :to="{ query: { ...getNewRange(tag.slug), mode }, params: { pageNumber: 1 } }">
<Icon
icon="checkmark"
class="include"
/>
</router-link>
<router-link
:to="{ query: { ...(selectedTags.length === 1 && selectedTags.includes(tag.slug) ? null : { tags: tag.slug }), mode }, params: { pageNumber: 1 } }"
class="name"
>{{ tag.name }}</router-link>
</li>
</ul>
</div>
</v-popover>
</template>
<script>
function getNewRange(tag) {
if (this.selectedTags.includes(tag)) {
if (this.selectedTags.length > 1) {
return { tags: this.selectedTags.filter(selectedTag => selectedTag !== tag).join(',') };
}
return {};
}
return { tags: this.selectedTags.concat(tag).join(',') };
}
function selectedTags() {
return this.$route.query.tags ? this.$route.query.tags.split(',') : [];
}
export default {
props: {
filter: {
type: Array,
default: () => [],
},
compact: {
type: Boolean,
default: false,
},
availableTags: {
type: Array,
default: () => [],
},
},
data() {
return {
mode: this.$route.query.mode || 'all',
};
},
computed: {
selectedTags,
},
methods: {
getNewRange,
},
};
</script>
<style lang="scss" scoped>
@import 'theme';
.filters {
display: inline-flex;
align-items: center;
.icon {
fill: var(--shadow);
margin: 0 .5rem 0 0;
}
&:hover {
cursor: pointer;
.applied {
color: var(--shadow-strong);
}
.icon {
fill: var(--shadow-strong);
}
}
}
.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);
}
}
.mode {
width: 100%;
background: none;
padding: .75rem;
font-size: 1rem;
border: none;
border-bottom: solid 1px var(--shadow-hint);
}
.tags {
padding: .5rem 0;
}
.tag {
display: flex;
align-items: center;
.include {
width: 1rem;
height: 1rem;
padding: .5rem .75rem .5rem 1rem;
fill: var(--darken-hint);
&:hover {
cursor: pointer;
}
}
.name {
min-width: 8rem;
display: flex;
justify-content: space-between;
flex-grow: 1;
padding: .5rem 1rem .5rem .75rem;
color: var(--text);
text-decoration: none;
}
&:hover {
background: var(--darken-hint);
cursor: pointer;
}
.include:hover,
.name:hover {
background: var(--darken-hint);
}
&.selected .include {
fill: var(--success);
}
}
@media(max-width: $breakpoint0) {
.applied {
display: none;
}
}
</style>

View File

@ -2,15 +2,9 @@
<header class="header">
<div class="header-nav">
<div
class="sidebar-toggle"
class="sidebar-toggle noselect"
@click.stop="toggleSidebar"
>
<Icon icon="menu" />
<div
class="logo"
v-html="logo"
/>
</div>
><Icon icon="menu" /></div>
<router-link
to="/"
@ -52,6 +46,20 @@
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/movies"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Movies</a>
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
@ -82,7 +90,7 @@
<Icon
v-show="sfw"
v-tooltip="'Hit N to disable safe mode'"
icon="evil2"
icon="fire"
class="toggle noselect"
@click.native="setSfw(false)"
/>
@ -215,11 +223,6 @@ export default {
align-items: center;
height: 100%;
.logo {
fill: var(--primary);
padding: .5rem;
}
.icon {
display: inline-block;
fill: var(--shadow-modest);
@ -353,15 +356,18 @@ export default {
}
}
@media(max-width: $breakpoint-micro) {
.nav,
.header-logo {
@media(max-width: $breakpoint) {
.nav {
display: none;
}
.sidebar-toggle {
display: flex;
}
.header-logo {
padding: 0 0 0 .5rem;
}
}
@media(max-width: $breakpoint-nano) {

View File

@ -20,12 +20,14 @@
:items-per-page="limit"
class="pagination-bottom"
/>
<Footer />
</div>
</div>
</template>
<script>
import FilterBar from '../header/filter-bar.vue';
import FilterBar from '../filters/filter-bar.vue';
import Releases from '../releases/releases.vue';
import Pagination from '../pagination/pagination.vue';

View File

@ -40,6 +40,8 @@
:entity="entity"
/>
</div>
<Footer />
</div>
</template>

View File

@ -9,12 +9,12 @@
>
<router-link
class="pagination-button cursor"
:to="{ params: { pageNumber: 1 } }"
:to="{ params: { pageNumber: 1 }, query: $route.query }"
><Icon icon="first2" /></router-link>
<router-link
class="pagination-button cursor"
:to="{ params: { pageNumber: pageNumber - 1 } }"
:to="{ params: { pageNumber: pageNumber - 1 }, query: $route.query }"
><Icon icon="arrow-left" /></router-link>
</span>
@ -30,14 +30,14 @@
<router-link
v-for="pageX in pageNumber - 1"
:key="`page-${pageX}`"
:to="{ params: { pageNumber: pageNumber - pageX } }"
:to="{ params: { pageNumber: pageNumber - pageX }, query: $route.query }"
class="pagination-button page"
> {{ pageNumber - pageX }} </router-link>
</span>
<router-link
:key="`page-${pageNumber}`"
:to="{ params: { pageNumber } }"
:to="{ params: { pageNumber }, query: $route.query }"
class="pagination-button page active"
> {{ pageNumber }} </router-link>
@ -45,7 +45,7 @@
<router-link
v-for="pageX in (pageCount - pageNumber)"
:key="`page-${pageX + pageNumber}`"
:to="{ params: { pageNumber: pageX + pageNumber } }"
:to="{ params: { pageNumber: pageX + pageNumber }, query: $route.query }"
class="pagination-button page"
> {{ pageX + pageNumber }} </router-link>
</span>
@ -56,12 +56,12 @@
>
<router-link
class="pagination-button cursor"
:to="{ params: { pageNumber: pageNumber + 1 } }"
:to="{ params: { pageNumber: pageNumber + 1 }, query: $route.query }"
><Icon icon="arrow-right" /></router-link>
<router-link
class="pagination-button cursor"
:to="{ params: { pageNumber: pageCount } }"
:to="{ params: { pageNumber: pageCount }, query: $route.query }"
><Icon icon="last2" /></router-link>
</span>

View File

@ -0,0 +1,223 @@
<template>
<div class="details">
<div class="column">
<div class="tidbits">
<a
v-if="release.date"
:title="release.url && `View scene on ${release.entity.name}`"
:href="release.url"
:class="{ link: release.url }"
target="_blank"
rel="noopener noreferrer"
class="tidbit date nolink"
>
<span class="date-compact">{{ formatDate(release.date, 'MMM D, YYYY', release.datePrecision) }}</span>
<span class="date-full">{{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }}</span>
<Icon
v-if="release.url"
icon="share2"
/>
</a>
</div>
<div class="site">
<template v-if="release.entity.parent && !release.entity.independent">
<a
v-if="release.entity.parent.hasLogo"
:href="`/network/${release.entity.parent.slug}`"
class="logo-link"
>
<img
:src="`/img/logos/${release.entity.parent.slug}/thumbs/network.png`"
:title="release.entity.parent.name"
:alt="release.entity.parent.name"
class="logo logo-parent"
>
</a>
<a
v-else
:href="`/network/${release.entity.parent.slug}`"
class="logo-link logo-name"
>{{ release.entity.parent.name }}</a>
<span class="chain">presents</span>
<a
v-if="release.entity.hasLogo"
:href="`/${release.entity.type}/${release.entity.slug}`"
class="logo-link"
>
<img
v-if="release.entity.type === 'network'"
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
:title="release.entity.name"
class="logo logo-site"
>
<img
v-else
:src="`/img/logos/${release.entity.parent.slug}/thumbs/${release.entity.slug}.png`"
:title="release.entity.name"
class="logo logo-site"
>
</a>
<a
v-else
:href="`/${release.entity.type}/${release.entity.slug}`"
class="logo-link logo-name"
>{{ release.entity.name }}</a>
</template>
<a
v-else
:href="`/${release.entity.type}/${release.entity.slug}`"
>
<img
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
:title="release.entity.name"
class="logo logo-site"
>
</a>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
release: {
type: Object,
default: null,
},
},
};
</script>
<style lang="scss" scoped>
@import 'breakpoints';
.details {
background: var(--profile);
color: var(--text-light);
box-shadow: 0 0 3px var(--shadow-weak);
cursor: default;
.column {
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
}
.link {
color: var(--text-light);
.icon {
fill: var(--lighten);
}
&:hover {
color: var(--text-light);
.icon {
fill: var(--text-light);
}
}
}
}
.tidbits {
flex-shrink: 0;
height: 100%;
}
.tidbit {
display: inline-flex;
align-items: center;
height: 100%;
&.date {
flex-shrink: 0;
font-weight: bold;
.icon {
fill: var(--lighten);
margin: -.2rem 0 0 .75rem;
}
}
}
.site {
display: inline-flex;
align-items: center;
padding: .25rem 0;
font-size: 0;
}
.logo {
display: inline-block;
}
.logo-link {
text-decoration: none;
}
.logo-site {
height: 2.5rem;
max-width: 15rem;
margin: .25rem 0;
object-fit: contain;
object-position: 100% 50%;
}
.logo-parent {
height: 1.5rem;
max-width: 10rem;
object-fit: contain;
object-position: 100% 50%;
}
.logo-name {
padding: .5rem 0;
color: var(--text-light);
font-size: 1.25rem;
font-weight: bold;
}
.chain {
color: var(--lighten);
padding: 0 .5rem;
font-weight: bold;
font-size: .8rem;
}
.date-compact {
display: none;
}
@media(max-width: $breakpoint-mega) {
.logo-parent,
.chain {
display: none;
}
.logo-site {
width: 100%;
}
}
@media(max-width: $breakpoint) {
.date-full {
display: none;
}
.date-compact {
display: inline-block;
}
}
</style>

View File

@ -71,7 +71,7 @@
<div
v-for="photo in photos"
:key="`media-${photo.index}`"
:key="`media-${photo.id}`"
class="item-container"
>
<a

View File

@ -0,0 +1,171 @@
<template>
<div class="tile">
<div class="movie">
<router-link
:to="{ name: 'movie', params: { movieId: movie.id, movieSlug: movie.slug } }"
class="cover"
>
<img
v-if="movie.covers[0]"
:src="`/media/${movie.covers[0].thumbnail}`"
>
</router-link>
<div class="info">
<router-link
:to="{ name: 'movie', params: { movieId: movie.id, movieSlug: movie.slug } }"
class="title-link"
>
<h3 class="title">{{ movie.title }}</h3>
</router-link>
<ul
class="actors nolist"
:title="movie.actors.map(actor => actor.name).join(', ')"
>
<li
v-for="actor in movie.actors"
:key="`tag-${movie.id}-${actor.id}`"
class="actor"
><router-link
:to="`/actor/${actor.id}/${actor.slug}`"
class="actor-link"
>{{ actor.name }}</router-link></li>
</ul>
<ul
class="tags nolist"
:title="movie.tags.map(tag => tag.name).join(', ')"
>
<li
v-for="tag in movie.tags"
:key="`tag-${movie.id}-${tag.id}`"
class="tag"
><router-link
:to="`/tag/${tag.slug}`"
class="tag-link"
>{{ tag.name }}</router-link></li>
</ul>
</div>
</div>
<Details :release="movie" />
</div>
</template>
<script>
import Details from './tile-details.vue';
export default {
components: {
Details,
},
props: {
movie: {
type: Object,
default: null,
},
},
};
</script>
<style lang="scss" scoped>
@import 'breakpoints';
.tile {
display: flex;
flex-direction: column;
background: var(--background);
box-shadow: 0 0 3px var(--darken-weak);
font-size: 0;
}
.movie {
display: flex;
}
.title-link {
color: var(--text);
text-decoration: none;
}
.cover {
height: 16rem;
box-shadow: 0 0 3px var(--darken-weak);
img {
height: 100%;
max-width: 12rem;
object-fit: cover;
object-position: center ;
}
}
.info {
flex-grow: 1;
overflow: hidden;
}
.title {
box-sizing: border-box;
padding: 1rem;
margin: 0;
font-size: 1rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.actors {
padding: 0 1rem;
margin: 0 0 1rem 0;
line-height: 1.5;
}
.actor:not(:last-child)::after {
content: ',';
margin: 0 .25rem 0 0;
font-size: 1rem;
}
.actor-link {
font-size: 1rem;
color: var(--link);
text-decoration: none;
&:hover {
color: var(--primary);
}
}
.tags {
padding: .2rem 1rem 0 1rem;
height: 1.75rem;
line-height: 2;
overflow: hidden;
}
.tag {
margin: 0 0 .5rem 0;
}
.tag-link {
background: var(--background);
font-size: .75rem;
padding: .25rem .5rem;
color: var(--shadow);
font-weight: bold;
text-decoration: none;
box-shadow: 0 0 3px var(--shadow-weak);
&:hover {
color: var(--primary);
}
}
@media(max-width: $breakpoint) {
.cover {
height: 12rem;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div
v-if="movie"
class="movie"
>
<Media
:release="movie"
/>
<Details :release="movie" />
<div class="column">
<h2 class="title">{{ movie.title }}</h2>
<p>{{ movie.description }}</p>
<div
v-lazy-container="{ selector: '.lazy' }"
class="actors"
>
<ActorTile
v-for="actor in movie.actors"
:key="`actor-${actor.id}`"
:actor="actor"
/>
</div>
<Tags
v-if="movie.tags && movie.tags.length > 0"
:tags="movie.tags"
/>
<Releases
:releases="movie.scenes"
/>
</div>
</div>
</template>
<script>
import Media from './media.vue';
import Details from './details.vue';
import Tags from './tags.vue';
import Releases from './releases.vue';
import ActorTile from '../actors/tile.vue';
async function mounted() {
this.movie = await this.$store.dispatch('fetchMovieById', this.$route.params.movieId);
}
export default {
components: {
Details,
Tags,
Media,
ActorTile,
Releases,
},
data() {
return {
movie: null,
};
},
mounted,
};
</script>
<style lang="scss" scoped>
.title {
padding: .5rem 0;
margin: 0 0 1rem 0;
color: var(--shadow-strong);
}
.covers {
display: inline-block;
margin: 0 0 1rem 0;
}
.cover {
height: 20rem;
margin: 0 1rem 0 0;
box-shadow: 0 0 3px var(--shadow-weak);
}
.trailer {
height: 20rem;
}
.date {
display: inline-block;
padding: 1rem;
}
.content-inner {
padding: 1rem;
}
.actors {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: .5rem;
flex-grow: 1;
flex-wrap: wrap;
margin: 0 0 1rem 0;
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<div class="movies">
<div class="content-inner">
<div class="tiles">
<MovieTile
v-for="movie in movies"
:key="`movie-${movie.id}`"
:movie="movie"
/>
</div>
</div>
<Footer />
</div>
</template>
<script>
import MovieTile from './movie-tile.vue';
async function mounted() {
const { movies, totalCount } = await this.$store.dispatch('fetchMovies', {
limit: 30,
});
this.movies = movies;
this.totalCount = totalCount;
}
export default {
components: {
MovieTile,
},
data() {
return {
movies: [],
totalCount: 0,
};
},
mounted,
};
</script>
<style lang="scss" scoped>
@import 'breakpoints';
.movies {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
grid-gap: 1rem;
}
@media(max-width: $breakpoint) {
.tiles {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
}
</style>

View File

@ -15,7 +15,7 @@
v-for="(release, index) in releases"
:key="`release-${release.id}`"
>
<ReleaseTile
<SceneTile
:release="release"
:referer="referer"
:index="index"
@ -36,7 +36,7 @@
</template>
<script>
import ReleaseTile from './tile.vue';
import SceneTile from './scene-tile.vue';
function range() {
return this.$route.params.range;
@ -48,7 +48,7 @@ function sfw() {
export default {
components: {
ReleaseTile,
SceneTile,
},
props: {
releases: {

View File

@ -145,7 +145,6 @@ export default {
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 0 0 .5rem 0;
overflow: hidden;
box-shadow: 0 0 3px var(--darken-weak);
height: 100%;
@ -212,7 +211,7 @@ export default {
}
.title {
margin: 0 .25rem .25rem 0;
margin: 0;
color: var(--text);
font-size: 1rem;
line-height: 1.5;
@ -226,12 +225,6 @@ export default {
color: var(--shadow);
}
.network {
color: #555;
margin: 0 .25rem 0 0;
font-size: .8rem;
}
.actors {
word-wrap: break-word;
overflow: hidden;
@ -255,18 +248,21 @@ export default {
}
.labels {
padding: 0 .5rem 1rem .25rem;
padding: .1rem .5rem 1.5rem .5rem;
max-height: .5rem;
overflow-y: hidden;
font-size: 0;
line-height: 2;
}
.shoot {
display: inline;
padding: .25rem .5rem .25rem .25rem;
border-right: solid 1px var(--shadow-hint);
color: var(--shadow-strong);
font-size: 0.8rem;
padding: .25rem .5rem;
background: var(--primary);
color: var(--text-light);
font-size: 0.75rem;
font-weight: bold;
box-shadow: inset 0 0 3px var(--shadow-weak);
}
.tags {
@ -275,17 +271,19 @@ export default {
}
.tag {
margin: 0 0 .25rem 0;
margin: 0 0 1rem 0;
}
.tag-link {
background: var(--background);
color: var(--shadow);
display: inline-block;
padding: .25rem;
padding: .25rem .5rem;
font-size: .75rem;
font-weight: bold;
text-decoration: none;
line-height: 1;
box-shadow: 0 0 3px var(--shadow-weak);
&:hover {
color: var(--primary);

View File

@ -20,91 +20,7 @@
/>
</Scroll>
<div class="details">
<div class="column">
<div class="tidbits">
<a
v-if="release.date"
:title="release.url && `View scene on ${release.entity.name}`"
:href="release.url"
:class="{ link: release.url }"
target="_blank"
rel="noopener noreferrer"
class="tidbit date"
>
<span class="showable">{{ formatDate(release.date, 'MMM D, YYYY', release.datePrecision) }}</span>
<span class="hideable">{{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }}</span>
<Icon
v-if="release.url"
icon="share2"
/>
</a>
</div>
<div class="site">
<template v-if="release.entity.parent && !release.entity.independent">
<a
v-if="release.entity.parent.hasLogo"
:href="`/network/${release.entity.parent.slug}`"
class="logo-link"
>
<img
:src="`/img/logos/${release.entity.parent.slug}/thumbs/network.png`"
:title="release.entity.parent.name"
:alt="release.entity.parent.name"
class="logo logo-parent"
>
</a>
<a
v-else
:href="`/network/${release.entity.parent.slug}`"
class="logo-link logo-name"
>{{ release.entity.parent.name }}</a>
<span class="chain">presents</span>
<a
v-if="release.entity.hasLogo"
:href="`/${release.entity.type}/${release.entity.slug}`"
class="logo-link"
>
<img
v-if="release.entity.type === 'network'"
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
:title="release.entity.name"
class="logo logo-site"
>
<img
v-else
:src="`/img/logos/${release.entity.parent.slug}/thumbs/${release.entity.slug}.png`"
:title="release.entity.name"
class="logo logo-site"
>
</a>
<a
v-else
:href="`/${release.entity.type}/${release.entity.slug}`"
class="logo-link logo-name"
>{{ release.entity.name }}</a>
</template>
<a
v-else
:href="`/${release.entity.type}/${release.entity.slug}`"
>
<img
:src="`/img/logos/${release.entity.slug}/thumbs/network.png`"
:title="release.entity.name"
class="logo logo-site"
>
</a>
</div>
</div>
</div>
<Details :release="release" />
<Expand
v-if="release.photos.length > 0"
@ -136,18 +52,7 @@
v-if="release.tags.length > 0"
class="row"
>
<ul class="tags nolist">
<li
v-for="tag in release.tags"
:key="`tag-${tag.slug}`"
class="tag"
>
<a
:href="`/tag/${tag.slug}`"
class="link"
>{{ tag.name }}</a>
</li>
</ul>
<Tags :tags="release.tags" />
</div>
<div class="row associations">
@ -162,24 +67,28 @@
<Actor :actor="actor" />
</li>
</ul>
<div
v-if="release.movies && release.movies.length > 0"
class="movies"
>
<Release :release="release.movies[0]" />
</div>
</div>
<div
v-if="release.scenes && release.scenes.length > 0"
class="scenes"
v-if="release.movies && release.movies.length > 0"
class="row"
>
<h3>Scenes</h3>
<Releases
:releases="release.scenes"
class="row"
/>
<span class="row-label">Part of</span>
<div class="movies">
<router-link
v-for="movie in release.movies"
:key="`movie-${movie.id}`"
:to="{ name: 'movie', params: { movieId: movie.id, movieSlug: movie.slug } }"
class="movie"
>
<span class="movie-title">{{ movie.title }}</span>
<img
:src="`/media/${movie.covers[0].thumbnail}`"
class="movie-cover"
>
</router-link>
</div>
</div>
<div
@ -259,9 +168,9 @@
<script>
import Media from './media.vue';
import Details from './details.vue';
import Tags from './tags.vue';
import Actor from '../actors/tile.vue';
import Release from './tile.vue';
import Releases from './releases.vue';
import Scroll from '../scroll/scroll.vue';
import Expand from '../expand/expand.vue';
@ -278,11 +187,11 @@ async function mounted() {
export default {
components: {
Actor,
Details,
Media,
Release,
Releases,
Scroll,
Expand,
Tags,
},
data() {
return {
@ -298,112 +207,7 @@ export default {
</script>
<style lang="scss" scoped>
@import 'theme';
.column {
width: 1200px;
max-width: 100%;
padding: 0 1rem;
margin: 0 auto;
box-sizing: border-box;
}
.details {
background: var(--profile);
color: var(--text-light);
box-shadow: 0 0 3px var(--shadow-weak);
cursor: default;
.column {
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
}
.link {
color: var(--text-light);
.icon {
fill: var(--lighten);
}
&:hover {
color: var(--text-light);
.icon {
fill: var(--text-light);
}
}
}
}
.tidbits {
flex-shrink: 0;
height: 100%;
}
.tidbit {
display: inline-flex;
align-items: center;
height: 100%;
&.date {
flex-shrink: 0;
padding: 0 2rem 0 0;
font-weight: bold;
.icon {
fill: var(--lighten);
margin: -.2rem 0 0 .75rem;
}
}
}
.site {
display: inline-flex;
align-items: center;
padding: .25rem 0;
font-size: 0;
}
.logo {
display: inline-block;
}
.logo-link {
text-decoration: none;
}
.logo-site {
height: 2.5rem;
max-width: 15rem;
margin: .25rem 0;
object-fit: contain;
object-position: 100% 50%;
}
.logo-parent {
height: 1.5rem;
max-width: 10rem;
object-fit: contain;
object-position: 100% 50%;
}
.logo-name {
padding: .5rem 0;
color: var(--text-light);
font-size: 1.25rem;
font-weight: bold;
}
.chain {
color: var(--lighten);
padding: 0 .5rem;
font-weight: bold;
font-size: .8rem;
}
@import 'breakpoints';
.expand-bottom {
border-bottom: solid 1px var(--shadow-hint);
}
@ -487,6 +291,39 @@ export default {
flex-wrap: wrap;
}
.movies {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: .5rem;
flex-grow: 1;
flex-wrap: wrap;
}
.movie {
display: flex;
flex-direction: column;
background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak);
color: var(--text);
text-decoration: none;
&:hover .movie-title {
color: var(--primary);
}
}
.movie-cover {
width: 100%;
}
.movie-title {
padding: .5rem;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link {
display: inline-flex;
color: var(--link);
@ -501,35 +338,10 @@ export default {
}
}
.tag .link {
background: var(--background);
display: inline-block;
padding: .5rem;
margin: 0 .25rem .25rem 0;
box-shadow: 0 0 2px var(--shadow-weak);
text-decoration: none;
text-transform: capitalize;
&:hover {
color: var(--primary);
}
}
.showable {
display: none;
}
@media(max-width: $breakpoint3) {
.logo-parent,
.chain {
display: none;
}
.logo-site {
width: 100%;
}
}
@media(max-width: $breakpoint) {
.hideable {
display: none;

View File

@ -0,0 +1,42 @@
<template>
<ul class="tags nolist">
<li
v-for="tag in tags"
:key="`tag-${tag.slug}`"
class="tag"
>
<a
:href="`/tag/${tag.slug}`"
class="link"
>{{ tag.name }}</a>
</li>
</ul>
</template>
<script>
export default {
props: {
tags: {
type: Array,
default: () => [],
},
},
};
</script>
<style lang="scss" scoped>
.tag .link {
color: var(--link);
background: var(--background);
display: inline-block;
padding: .5rem;
margin: 0 .25rem .25rem 0;
box-shadow: 0 0 2px var(--shadow-weak);
text-decoration: none;
text-transform: capitalize;
&:hover {
color: var(--primary);
}
}
</style>

View File

@ -36,8 +36,8 @@
<a
v-if="release.date"
v-tooltip.bottom="release.url && `View scene on ${release.entity.name}`"
:title="release.url && `View scene on ${release.entity.name}`"
v-tooltip.bottom="release.url && `View release on ${release.entity.name}`"
:title="release.url && `View release on ${release.entity.name}`"
:href="release.url"
:class="{ upcoming: isAfter(release.date, new Date()) }"
target="_blank"
@ -73,6 +73,7 @@ export default {
width: 100%;
display: flex;
justify-content: space-between;
white-space: nowrap;
background: var(--profile);
font-size: 0;
font-weight: bold;

View File

@ -1,5 +1,8 @@
<template>
<div class="sidebar-container">
<div
class="sidebar-container"
@click="$emit('toggle', false)"
>
<div
class="sidebar"
@click.stop
@ -8,14 +11,14 @@
<div class="sidebar-header">
<Icon
icon="cross2"
class="sidebar-close"
@click.native="toggleSidebar(false)"
class="sidebar-close noselect"
@click.native="$emit('toggle', false)"
/>
<router-link
to="/updates"
class="logo-link"
@click.native="toggleSidebar(false)"
@click.native="$emit('toggle', false)"
>
<h1 class="sidebar-logo">
<div
@ -32,7 +35,7 @@
<router-link
v-slot="{ href, isActive, navigate }"
to="/updates"
@click.native="toggleSidebar(false)"
@click.native="$emit('toggle', false)"
>
<a
class="nav-link"
@ -47,7 +50,7 @@
<router-link
v-slot="{ href, isActive, navigate }"
to="/actors"
@click.native="toggleSidebar(false)"
@click.native="$emit('toggle', false)"
>
<a
class="nav-link"
@ -62,7 +65,7 @@
<router-link
v-slot="{ href, isActive, navigate }"
to="/networks"
@click.native="toggleSidebar(false)"
@click.native="$emit('toggle', false)"
>
<a
class="nav-link"
@ -73,11 +76,26 @@
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/movies"
@click.native="$emit('toggle', false)"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Movies</a>
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/tags"
@click.native="toggleSidebar(false)"
@click.native="$emit('toggle', false)"
>
<a
class="nav-link"
@ -96,7 +114,7 @@
v-show="sfw"
class="toggle"
@click="setSfw(false)"
><Icon icon="evil2" />Disable safe mode</label>
><Icon icon="fire" />Disable safe mode</label>
<label
v-show="!sfw"
@ -142,12 +160,6 @@ function setSfw(enabled) {
}
export default {
props: {
toggleSidebar: {
type: Function,
default: null,
},
},
data() {
return {
logo,
@ -172,7 +184,7 @@ export default {
width: 100%;
position: absolute;
z-index: 10;
background: var(--darken-hint);
background: var(--darken-weak);
}
.sidebar {

View File

@ -0,0 +1,95 @@
<template>
<div class="stats">
<div class="content-inner">
<h1 class="heading">Stats</h1>
<dl class="stat-table">
<div class="stat-row">
<dt class="stat-label">Networks</dt>
<dd class="stat-value">{{ totalNetworks }}</dd>
</div>
<div class="stat-row">
<dt class="stat-label">Channels</dt>
<dd class="stat-value">{{ totalChannels }}</dd>
</div>
<div class="stat-row">
<dt class="stat-label">Scenes</dt>
<dd class="stat-value">{{ totalScenes }}</dd>
</div>
<div class="stat-row">
<dt class="stat-label">Movies</dt>
<dd class="stat-value">{{ totalMovies }}</dd>
</div>
<div class="stat-row">
<dt class="stat-label">Actors</dt>
<dd class="stat-value">{{ totalActors }}</dd>
</div>
</dl>
</div>
<Footer />
</div>
</template>
<script>
async function mounted() {
const stats = await this.$store.dispatch('fetchStats');
this.totalScenes = stats.totalScenes;
this.totalMovies = stats.totalMovies;
this.totalActors = stats.totalActors;
this.totalNetworks = stats.totalNetworks;
this.totalChannels = stats.totalChannels;
}
export default {
data() {
return {
totalScenes: 0,
totalMovies: 0,
totalActors: 0,
totalNetworks: 0,
totalChannels: 0,
};
},
mounted,
};
</script>
<style lang="scss" scoped>
.stats {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.content-inner {
padding: 1rem;
}
.stat-row {
width: 20rem;
max-width: 100%;
display: flex;
padding: .5rem 0;
justify-content: space-between;
&:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint);
}
}
.stat-label {
display: inline-block;
font-weight: bold;
color: var(--shadow-strong);
}
.stat-value {
display: inline-block;
}
</style>

View File

@ -12,6 +12,7 @@
:src="`/img/${poster.thumbnail}`"
:alt="tag.poster.comment"
class="poster"
@load="$parent.$emit('load')"
>
</a>
@ -28,6 +29,7 @@
:src="`/img/${photo.thumbnail}`"
:alt="photo.comment"
class="photo"
@load="$parent.$emit('load')"
>
</a>
</div>
@ -67,7 +69,7 @@ export default {
<style lang="scss" scoped>
.photos {
width: 100%;
padding: 1rem 1rem 0 1rem;
padding: .5rem 1rem 0 .5rem;
box-sizing: border-box;
overflow-x: auto;
white-space: nowrap;

View File

@ -31,6 +31,8 @@
<FilterBar :fetch-releases="fetchReleases" />
<Releases :releases="tag.releases" />
<Footer />
</div>
</div>
</template>
@ -41,7 +43,7 @@ import { Converter } from 'showdown';
import escapeHtml from '../../../src/utils/escape-html';
import FilterBar from '../header/filter-bar.vue';
import FilterBar from '../filters/filter-bar.vue';
import Photos from './photos.vue';
import Releases from '../releases/releases.vue';
import Scroll from '../scroll/scroll.vue';
@ -123,7 +125,7 @@ export default {
}
.title {
padding: .75rem 1rem;
padding: .5rem 1rem;
margin: 0;
flex-shrink: 0;
text-transform: capitalize;

View File

@ -20,6 +20,8 @@
/>
</div>
</div>
<Footer />
</div>
</template>
@ -48,13 +50,11 @@ async function mounted() {
'creampie',
'squirting',
],
ethnicity: [
appearance: [
'asian',
'ebony',
'latina',
'caucasian',
],
appearance: [
'natural-boobs',
'fake-boobs',
'blonde',
@ -73,6 +73,15 @@ async function mounted() {
'ass-eating',
'atm',
],
cumshot: [
'facial',
'bukkake',
'creampie',
'anal-creampie',
'cum-in-mouth',
'oral-creampie',
'cum-on-butt',
],
extreme: [
'airtight',
'dap',
@ -81,14 +90,6 @@ async function mounted() {
'dv-tp',
'tap',
],
cumshot: [
'facial',
'bukkake',
'creampie',
'anal-creampie',
'cum-in-mouth',
'cum-on-butt',
],
roleplay: [
'family',
'parody',
@ -98,6 +99,7 @@ async function mounted() {
],
fetish: [
'bdsm',
'bondage',
'femdom',
],
toys: [
@ -146,7 +148,7 @@ export default {
@import 'theme';
.tags {
padding: 1rem;
padding: 1rem 1rem 0 1rem;
}
.tiles {
@ -157,11 +159,6 @@ export default {
}
.heading {
display: inline-block;
background: var(--primary);
color: var(--text-light);
padding: .5rem;
font-size: 1rem;
text-transform: capitalize;
}

View File

@ -48,6 +48,7 @@
:to="{ name: 'tag', params: { tagSlug: tag.slug, range: 'latest' } }"
:title="tag.name"
>{{ tag.name }}</router-link>
</div>
<span
@ -85,6 +86,11 @@ export default {
box-sizing: border-box;
position: relative;
text-decoration: none;
font-size: 0;
&:hover .poster {
box-shadow: 0 0 3px var(--darken);
}
}
.poster {
@ -93,24 +99,17 @@ export default {
}
.title {
display: inline-block;
display: block;
box-sizing: border-box;
padding: .25rem .5rem .25rem 1rem;
padding: .5rem;
overflow: hidden;
white-space: nowrap;
color: var(--shadow-strong);
color: var(--text-light);
background: var(--profile);
text-decoration: none;
font-size: .9rem;
font-weight: bold;
text-transform: capitalize;
text-overflow: ellipsis;
}
.poster-link:hover + .title,
.title:hover {
color: var(--primary)
}
.poster-link:hover .poster {
box-shadow: 0 0 3px var(--darken);
}
</style>

View File

@ -18,6 +18,12 @@
}
}
.nolink {
display: inline-block;
color: inherit;
text-decoration: none;
}
:focus {
outline: none;
}

View File

@ -0,0 +1,7 @@
<!-- 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>antenna</title>
<path d="M11.182 11.932c-0.192 0-0.384-0.073-0.53-0.22-0.293-0.293-0.293-0.768 0-1.061 1.462-1.462 1.462-3.841 0-5.303s-3.841-1.462-5.303 0c-0.708 0.708-1.098 1.65-1.098 2.652s0.39 1.943 1.098 2.652c0.293 0.293 0.293 0.768 0 1.061s-0.768 0.293-1.061 0c-0.992-0.992-1.538-2.31-1.538-3.712s0.546-2.721 1.538-3.712c2.047-2.047 5.378-2.047 7.425 0s2.047 5.378 0 7.425c-0.146 0.146-0.338 0.22-0.53 0.22v0z"></path>
<path d="M13.127 13.877c-0.192 0-0.384-0.073-0.53-0.22-0.293-0.293-0.293-0.768 0-1.061 2.534-2.534 2.534-6.658 0-9.192s-6.658-2.534-9.192 0c-2.534 2.534-2.534 6.658 0 9.192 0.293 0.293 0.293 0.768 0 1.061s-0.768 0.293-1.061 0c-1.511-1.511-2.343-3.52-2.343-5.657s0.832-4.146 2.343-5.657c1.511-1.511 3.52-2.343 5.657-2.343s4.146 0.832 5.657 2.343c1.511 1.511 2.343 3.52 2.343 5.657s-0.832 4.146-2.343 5.657c-0.146 0.146-0.338 0.22-0.53 0.22v0z"></path>
<path d="M9.5 15h-0.501l0.001-5.268c0.598-0.346 1-0.992 1-1.732 0-1.105-0.895-2-2-2s-2 0.895-2 2c0 0.74 0.402 1.386 1 1.732l-0.001 5.268h-0.499c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h3c0.276 0 0.5-0.224 0.5-0.5s-0.224-0.5-0.5-0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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>antenna2</title>
<path d="M5 16h6l-2-2v-7.268c0.598-0.346 1-0.992 1-1.732 0-1.105-0.895-2-2-2s-2 0.895-2 2c0 0.74 0.402 1.386 1 1.732v7.268l-2 2zM11.122 2.5c0.549 0.685 0.878 1.554 0.878 2.5s-0.329 1.815-0.878 2.5l0.781 0.625c0.686-0.856 1.097-1.942 1.097-3.125s-0.411-2.269-1.097-3.125l-0.781 0.625zM4.097 1.875c-0.686 0.856-1.097 1.942-1.097 3.125s0.411 2.269 1.097 3.125l0.781-0.625c-0.549-0.685-0.878-1.554-0.878-2.5s0.329-1.815 0.878-2.5l-0.781-0.625zM1.755 0c-1.098 1.37-1.755 3.108-1.755 5s0.657 3.63 1.755 5l0.781-0.625c-0.961-1.198-1.536-2.719-1.536-4.375s0.575-3.176 1.536-4.375l-0.781-0.625zM14.245 0l-0.781 0.625c0.961 1.198 1.536 2.719 1.536 4.375s-0.575 3.176-1.536 4.375l0.781 0.625c1.098-1.37 1.755-3.108 1.755-5s-0.657-3.63-1.755-5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 904 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>camera</title>
<path d="M4.75 9.5c0 1.795 1.455 3.25 3.25 3.25s3.25-1.455 3.25-3.25-1.455-3.25-3.25-3.25-3.25 1.455-3.25 3.25zM15 4h-3.5c-0.25-1-0.5-2-1.5-2h-4c-1 0-1.25 1-1.5 2h-3.5c-0.55 0-1 0.45-1 1v9c0 0.55 0.45 1 1 1h14c0.55 0 1-0.45 1-1v-9c0-0.55-0.45-1-1-1zM8 13.938c-2.451 0-4.438-1.987-4.438-4.438s1.987-4.438 4.438-4.438c2.451 0 4.438 1.987 4.438 4.438s-1.987 4.438-4.438 4.438zM15 7h-2v-1h2v1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 559 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>display</title>
<path d="M0 1v10h16v-10h-16zM15 10h-14v-8h14v8zM10.5 12h-5l-0.5 2-1 1h8l-1-1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 248 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>display4</title>
<path d="M16 13v-12h-16v12h7v1h-3v1h8v-1h-3v-1h7zM2 3h12v8h-12v-8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 238 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>station</title>
<path d="M10 5c0-1.105-0.895-2-2-2s-2 0.895-2 2c0 0.709 0.369 1.331 0.925 1.686l-2.54 9.314h1.231l0.273-1h4.224l0.273 1h1.231l-2.54-9.314c0.556-0.355 0.925-0.977 0.925-1.686zM9.294 12h-2.587l0.273-1h2.042l0.273 1zM7.252 10l0.748-2.743 0.748 2.743h-1.496zM6.161 14l0.273-1h3.133l0.273 1h-3.678zM10.38 0.602c1.56 0.846 2.62 2.498 2.62 4.398s-1.059 3.552-2.62 4.398c0.689-1.096 1.12-2.66 1.12-4.398s-0.431-3.302-1.12-4.398zM13.38 0.602c1.56 0.846 2.62 2.498 2.62 4.398s-1.059 3.552-2.62 4.398c0.689-1.096 1.12-2.66 1.12-4.398s-0.431-3.302-1.12-4.398zM5.62 9.398c-1.56-0.846-2.62-2.498-2.62-4.398s1.059-3.552 2.62-4.398c-0.689 1.096-1.12 2.66-1.12 4.398s0.431 3.302 1.12 4.398zM0 5c0-1.9 1.059-3.552 2.62-4.398-0.689 1.096-1.12 2.66-1.12 4.398s0.431 3.302 1.12 4.398c-1.56-0.846-2.62-2.498-2.62-4.398z"></path>
</svg>

After

Width:  |  Height:  |  Size: 968 B

View File

@ -0,0 +1,9 @@
<!-- 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>station2</title>
<path d="M3.128 11.347l0.643-1.415c-1.389-1.193-2.271-2.961-2.271-4.932 0-1.535 0.535-2.947 1.428-4.060l-1.174-0.94c-1.136 1.414-1.754 3.159-1.754 5 0 2.137 0.832 4.146 2.343 5.657 0.249 0.249 0.511 0.478 0.785 0.69z"></path>
<path d="M12.229 9.932l0.643 1.415c0.274-0.211 0.536-0.441 0.785-0.69 1.511-1.511 2.343-3.52 2.343-5.657 0-1.84-0.618-3.585-1.754-5l-1.174 0.94c0.893 1.113 1.428 2.525 1.428 4.060 0 1.971-0.882 3.739-2.271 4.932z"></path>
<path d="M11.006 7.242l0.68 1.496c0.009-0.009 0.018-0.017 0.026-0.025 1.909-1.909 2.037-4.934 0.386-6.993l-1.171 0.937c1.067 1.331 1.093 3.226 0.079 4.585z"></path>
<path d="M4.288 8.712c0.009 0.009 0.017 0.017 0.026 0.025l0.68-1.496c-1.015-1.359-0.988-3.254 0.079-4.585l-1.171-0.937c-1.651 2.060-1.523 5.084 0.386 6.993z"></path>
<path d="M10 5c0-1.105-0.895-2-2-2s-2 0.895-2 2c0 0.583 0.25 1.108 0.648 1.473l-4.33 9.527h1.648l0.909-2h6.252l0.909 2h1.648l-4.33-9.527c0.398-0.366 0.648-0.89 0.648-1.473zM8 7.123l1.308 2.877h-2.616l1.308-2.877zM10.671 13h-5.343l0.909-2h3.525l0.909 2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

5
assets/img/icons/tv.svg Normal file
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>tv</title>
<path d="M15.331 4.502c-1.388-0.199-2.865-0.344-4.407-0.425l2.576-2.576-1-1-3.509 3.509c-0.328-0.006-0.659-0.009-0.991-0.009v0l-4-4-1 1 3.034 3.034c-1.889 0.066-3.693 0.227-5.365 0.467-0.43 1.683-0.669 3.543-0.669 5.498s0.239 3.815 0.669 5.498c2.244 0.323 4.724 0.502 7.331 0.502s5.087-0.179 7.331-0.502c0.43-1.683 0.669-3.543 0.669-5.498s-0.239-3.815-0.669-5.498zM13.498 13.666c-1.683 0.215-3.543 0.334-5.498 0.334s-3.815-0.119-5.498-0.334c-0.323-1.122-0.502-2.362-0.502-3.666s0.179-2.543 0.502-3.666c1.683-0.215 3.543-0.334 5.498-0.334s3.815 0.119 5.498 0.334c0.323 1.122 0.502 2.362 0.502 3.666s-0.179 2.543-0.502 3.666z"></path>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -7,8 +7,6 @@
<title>traxxx</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
@ -18,6 +16,8 @@
<meta name="msapplication-TileColor" content="#aa2c66">
<meta name="msapplication-config" content="/img/favicon/browserconfig.xml">
<link rel="stylesheet" href="/css/style.css">
<script src="/js/bundle.js" defer></script>
</head>
<body>

View File

@ -12,6 +12,7 @@ function initActorActions(store, router) {
range = 'latest',
}) {
const { before, after, orderBy } = getDateRange(range);
const includeChannels = router.currentRoute.query.channels ? router.currentRoute.query.channels.split(',') : [];
const includeTags = router.currentRoute.query.tags ? router.currentRoute.query.tags.split(',') : [];
const mode = router.currentRoute.query.mode || 'all';
@ -26,6 +27,7 @@ function initActorActions(store, router) {
$selectableTags: [String],
$includeTags: [String!],
$mode: String!,
${includeChannels.length > 0 ? '$includeChannels: [String!]' : ''}
) {
actor(id: $actorId) {
id
@ -146,12 +148,31 @@ function initActorActions(store, router) {
slug
priority
}
channels {
id
name
slug
type
independent
parent {
id
name
slug
type
}
}
scenesConnection(
filter: {
date: {
lessThan: $before,
greaterThan: $after,
}
${includeChannels.length > 0 ? `
entity: {
slug: {
in: $includeChannels
}
}` : ''}
}
selectedTags: $includeTags
mode: $mode
@ -176,6 +197,7 @@ function initActorActions(store, router) {
orderBy,
excludeTags: store.state.ui.filter,
includeTags,
includeChannels,
mode,
});

View File

@ -64,7 +64,7 @@ function curateRelease(release) {
...release,
actors: [],
poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map(({ tag }) => tag) : [],
tags: release.tags ? release.tags.map(tag => tag.tag || tag) : [],
};
if (release.scenes) curatedRelease.scenes = release.scenes.map(({ scene }) => curateRelease(scene));
@ -73,7 +73,7 @@ function curateRelease(release) {
if (release.covers) curatedRelease.covers = release.covers.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.map(({ actor }) => curateActor(actor, curatedRelease));
if (release.actors) curatedRelease.actors = release.actors.map(actor => curateActor(actor.actor || actor, curatedRelease));
if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.map(({ tag }) => tag);
if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.map(({ actor }) => curateActor(actor, curatedRelease));

View File

@ -86,7 +86,6 @@ function initEntitiesActions(store, _router) {
or: [
{
slug: { equalTo: $entitySlug }
type: { equalTo: "channel" }
},
{
parent: {
@ -181,6 +180,13 @@ function initEntitiesActions(store, _router) {
type: {
equalTo: "network"
}
childEntitiesConnection: {
some: {
type: {
equalTo: "channel"
}
}
}
}
{
independent: {
@ -198,7 +204,7 @@ function initEntitiesActions(store, _router) {
in: $entitySlugs
}
}
]
],
}
) {
id

View File

@ -93,6 +93,7 @@ const releaseTagsFragment = `
const releasePosterFragment = `
poster: releasesPosterByReleaseId {
media {
id
index
path
thumbnail
@ -112,6 +113,7 @@ const releasePosterFragment = `
const releaseCoversFragment = `
covers: releasesCovers {
media {
id
index
path
thumbnail
@ -131,6 +133,7 @@ const releaseCoversFragment = `
const releasePhotosFragment = `
photos: releasesPhotos {
media {
id
index
path
thumbnail
@ -150,6 +153,7 @@ const releasePhotosFragment = `
const releaseTrailerFragment = `
trailer: releasesTrailerByReleaseId {
media {
id
index
path
thumbnail
@ -175,7 +179,6 @@ const releaseFields = `
date
datePrecision
slug
type
shootId
productionDate
comment
@ -218,18 +221,6 @@ const releasesFragment = `
) {
releases: nodes {
${releaseFields}
movieActors: movieActorsByMovieId(orderBy: ACTOR_BY_ACTOR_ID__GENDER_ASC) {
actor {
${actorFields}
}
}
movieTags: movieTagsByMovieId(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
id
name
slug
}
}
}
totalCount
}
@ -256,48 +247,35 @@ const releaseFragment = `
${releaseTrailerFragment}
${releaseTeaserFragment}
${siteFragment}
movieActors: movieActorsByMovieId(orderBy: ACTOR_BY_ACTOR_ID__GENDER_ASC) {
actor {
${actorFields}
}
}
movieTags: movieTagsByMovieId(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
id
name
slug
}
}
movies: releasesMoviesBySceneId {
movie {
id
title
date
slug
createdAt
url
${releaseCoversFragment}
${siteFragment}
actors: movieActorsByMovieId {
actor {
id
name
slug
}
}
}
}
scenes: releasesMoviesByMovieId {
scene {
${releaseFields}
}
}
studio {
id
name
slug
url
}
movies: moviesScenesBySceneId {
movie {
id
title
slug
covers: moviesCoversByReleaseId {
media {
index
path
thumbnail
lazy
comment
sfw: sfwMedia {
id
thumbnail
lazy
path
comment
}
}
}
}
}
}
`;

View File

@ -12,6 +12,7 @@ import '../css/style.scss';
import Container from '../components/container/container.vue';
import Icon from '../components/icon/icon.vue';
import Footer from '../components/footer/footer.vue';
function formatDate(date, format = 'MMMM D, YYYY', precision = 'day') {
if (precision === 'year') {
@ -39,6 +40,7 @@ function init() {
Vue.mixin({
components: {
Icon,
Footer,
},
watch: {
pageTitle(title) {

View File

@ -1,5 +1,5 @@
import { graphql } from '../api';
import { releasesFragment, releaseFragment } from '../fragments';
import { releasesFragment, releaseFragment, releaseFields } from '../fragments';
import { curateRelease } from '../curate';
import getDateRange from '../get-date-range';
@ -47,9 +47,145 @@ function initReleasesActions(store, _router) {
return curateRelease(release);
}
async function fetchMovies({ _commit }, { limit = 10, pageNumber = 1 }) {
const { connection: { movies, totalCount } } = await graphql(`
query Movies(
$limit:Int = 1000,
$offset:Int = 0,
) {
connection: moviesConnection(
first: $limit
offset: $offset
orderBy: DATE_DESC
) {
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: moviesCoversByReleaseId {
media {
id
path
thumbnail
}
}
}
totalCount
}
}
`, {
limit,
offset: Math.max(0, (pageNumber - 1)) * limit,
});
return {
movies: movies.map(release => curateRelease(release)),
totalCount,
};
}
async function fetchMovieById({ _commit }, movieId) {
// const release = await get(`/releases/${releaseId}`);
const { movie } = await graphql(`
query Movie($movieId: Int!) {
movie(id: $movieId) {
id
title
slug
url
date
actors {
id
name
age
dateOfBirth
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: avatarMedia {
id
path
thumbnail
lazy
}
}
covers: moviesCoversByReleaseId {
media {
id
path
thumbnail
}
}
trailer: moviesTrailerByReleaseId {
media {
id
path
}
}
scenes: moviesScenes {
scene {
${releaseFields}
}
}
tags {
id
slug
name
}
entity {
id
name
slug
type
parent {
id
name
slug
type
}
}
}
}
`, {
movieId: Number(movieId),
});
return curateRelease(movie);
}
return {
fetchReleases,
fetchReleaseById,
fetchMovies,
fetchMovieById,
};
}

View File

@ -2,14 +2,17 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../components/home/home.vue';
import Release from '../components/releases/release.vue';
import Scene from '../components/releases/scene.vue';
import Movie from '../components/releases/movie.vue';
import Entity from '../components/entities/entity.vue';
import Networks from '../components/networks/networks.vue';
import Actor from '../components/actors/actor.vue';
import Actors from '../components/actors/actors.vue';
import Movies from '../components/releases/movies.vue';
import Tag from '../components/tags/tag.vue';
import Tags from '../components/tags/tags.vue';
import Search from '../components/search/search.vue';
import Stats from '../components/stats/stats.vue';
import NotFound from '../components/errors/404.vue';
Vue.use(VueRouter);
@ -43,12 +46,12 @@ const routes = [
},
{
path: '/scene/:releaseId/:releaseSlug?',
component: Release,
component: Scene,
name: 'scene',
},
{
path: '/movie/:releaseId/:releaseSlug?',
component: Release,
path: '/movie/:movieId/:movieSlug?',
component: Movie,
name: 'movie',
},
{
@ -137,7 +140,7 @@ const routes = [
name: 'actors',
params: {
...from.params,
gender: 'female',
gender: 'all',
letter: 'all',
tags: 'all',
range: 'latest',
@ -155,6 +158,11 @@ const routes = [
component: Networks,
name: 'networks',
},
{
path: '/movies',
component: Movies,
name: 'movies',
},
{
path: '/tags',
component: Tags,
@ -165,6 +173,11 @@ const routes = [
component: Search,
name: 'search',
},
{
path: '/stats',
component: Stats,
name: 'stats',
},
{
path: '*',
component: NotFound,

View File

@ -41,7 +41,6 @@ function initUiActions(_store, _router) {
slug
date
url
type
isNew
entity {
id
@ -155,6 +154,32 @@ function initUiActions(_store, _router) {
};
}
async function fetchStats() {
const {
scenes,
movies,
actors,
networks,
channels,
} = await graphql(`
query Stats {
scenes: releasesConnection { totalCount }
movies: moviesConnection { totalCount }
actors: actorsConnection { totalCount }
networks: entitiesConnection(filter: { type: { equalTo: "network" } }) { totalCount }
channels: entitiesConnection(filter: { type: { equalTo: "channel" } }) { totalCount }
}
`);
return {
totalScenes: scenes.totalCount,
totalMovies: movies.totalCount,
totalActors: actors.totalCount,
totalNetworks: networks.totalCount,
totalChannels: channels.totalCount,
};
}
return {
search,
setFilter,
@ -162,6 +187,7 @@ function initUiActions(_store, _router) {
setBatch,
setSfw,
setTheme,
fetchStats,
};
}

View File

@ -11,13 +11,7 @@ module.exports = {
sfwHost: '0.0.0.0',
sfwPort: 5001,
},
// include: [],
exclude: {
networks: [
'gamma',
'mindgeek',
'julesjordan',
],
channels: [
// 21sextreme, no longer updated
'mightymistress',
@ -161,14 +155,16 @@ module.exports = {
'pervertgallery',
'povperverts',
],
'kellymadison',
'private',
'ddfnetwork',
'bangbros',
'hitzefrei',
[
'silverstonedvd',
'silviasaint',
],
'kellymadison',
'porncz',
'gangbangcreampie',
'gloryholesecrets',
'aziani',

View File

@ -612,12 +612,9 @@ exports.up = knex => Promise.resolve()
.references('id')
.inTable('entities');
table.text('type', 10)
.defaultTo('scene');
table.text('shoot_id');
table.text('entry_id');
table.unique(['entity_id', 'entry_id', 'type']);
table.unique(['entity_id', 'entry_id']);
table.text('url', 1000);
table.text('title');
@ -668,22 +665,6 @@ exports.up = knex => Promise.resolve()
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('releases_movies', (table) => {
table.integer('movie_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.integer('scene_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.unique(['movie_id', 'scene_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('releases_directors', (table) => {
table.integer('release_id', 16)
.notNullable()
@ -780,6 +761,90 @@ exports.up = knex => Promise.resolve()
.references('id')
.inTable('releases');
}))
.then(() => knex.schema.createTable('movies', (table) => {
table.increments('id', 16);
table.integer('entity_id', 12)
.references('id')
.inTable('entities')
.notNullable();
table.integer('studio_id', 12)
.references('id')
.inTable('entities');
table.text('entry_id');
table.unique(['entity_id', 'entry_id']);
table.text('url', 1000);
table.text('title');
table.text('slug');
table.timestamp('date');
table.index('date');
table.enum('date_precision', ['year', 'month', 'day', 'hour', 'minute', 'second'])
.defaultTo('day');
table.text('description');
table.boolean('deep');
table.text('deep_url', 1000);
table.text('comment');
table.integer('created_batch_id', 12)
.references('id')
.inTable('batches');
table.integer('updated_batch_id', 12)
.references('id')
.inTable('batches');
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('movies_scenes', (table) => {
table.integer('movie_id', 16)
.notNullable()
.references('id')
.inTable('movies');
table.integer('scene_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.unique(['movie_id', 'scene_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('movies_covers', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
.inTable('movies');
table.text('media_id', 21)
.notNullable()
.references('id')
.inTable('media');
table.unique(['release_id', 'media_id']);
}))
.then(() => knex.schema.createTable('movies_trailers', (table) => {
table.integer('release_id', 16)
.unique()
.notNullable()
.references('id')
.inTable('movies');
table.text('media_id', 21)
.notNullable()
.references('id')
.inTable('media');
}))
// SEARCH
.then(() => { // eslint-disable-line arrow-body-style
// allow vim fold
@ -857,18 +922,25 @@ exports.up = knex => Promise.resolve()
ORDER BY tags.name;
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION actors_channels(actor actors) RETURNS SETOF entities AS $$
SELECT entities.*
FROM releases_actors
LEFT JOIN releases ON releases.id = releases_actors.release_id
LEFT JOIN entities ON entities.id = releases.entity_id
WHERE releases_actors.actor_id = actor.id
GROUP BY entities.id;
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION actors_scenes(actor actors, selected_tags text[], mode text DEFAULT 'all') RETURNS SETOF releases AS $$
SELECT releases.*
FROM releases
LEFT JOIN
releases_actors ON releases_actors.release_id = releases.id
LEFT JOIN
actors ON actors.id = releases_actors.actor_id
LEFT JOIN
releases_tags ON releases_tags.release_id = releases.id
LEFT JOIN
tags ON tags.id = releases_tags.tag_id
WHERE actors.id = actor.id
WHERE releases_actors.actor_id = actor.id
AND CASE
WHEN mode = 'any' AND array_length(selected_tags, 1) > 0
THEN tags.slug = ANY(selected_tags)
@ -886,6 +958,36 @@ exports.up = knex => Promise.resolve()
END;
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION movies_actors(movie movies) RETURNS SETOF actors AS $$
SELECT actors.*
FROM movies_scenes
LEFT JOIN
releases ON releases.id = movies_scenes.scene_id
LEFT JOIN
releases_actors ON releases_actors.release_id = releases.id
LEFT JOIN
actors ON actors.id = releases_actors.actor_id
WHERE movies_scenes.movie_id = movie.id
AND actors.id IS NOT NULL
GROUP BY actors.id
ORDER BY actors.name, actors.gender
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION movies_tags(movie movies) RETURNS SETOF tags AS $$
SELECT tags.*
FROM movies_scenes
LEFT JOIN
releases ON releases.id = movies_scenes.scene_id
LEFT JOIN
releases_tags ON releases_tags.release_id = releases.id
LEFT JOIN
tags ON tags.id = releases_tags.tag_id
WHERE movies_scenes.movie_id = movie.id
AND tags.id IS NOT NULL
GROUP BY tags.id
ORDER BY tags.priority DESC
$$ LANGUAGE SQL STABLE;
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;
@ -895,24 +997,11 @@ exports.up = knex => Promise.resolve()
.then(() => { // eslint-disable-line arrow-body-style
// allow vim fold
return knex.raw(`
CREATE VIEW movie_actors AS
SELECT releases_movies.movie_id, releases_actors.actor_id FROM releases_movies
LEFT JOIN releases ON releases.id = releases_movies.scene_id
LEFT JOIN releases_actors ON releases_actors.release_id = releases.id
GROUP BY movie_id, actor_id;
CREATE VIEW movie_tags AS
SELECT releases_movies.movie_id, releases_tags.tag_id FROM releases_movies
LEFT JOIN releases ON releases.id = releases_movies.scene_id
LEFT JOIN releases_tags ON releases_tags.release_id = releases.id
GROUP BY movie_id, tag_id;
COMMENT ON VIEW movie_actors IS E'@foreignKey (movie_id) references releases (id)\n@foreignKey (actor_id) references actors (id)';
COMMENT ON VIEW movie_tags IS E'@foreignKey (movie_id) references releases (id)\n@foreignKey (tag_id) references tags (id)';
COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many';
COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many';
COMMENT ON FUNCTION actors_tags IS E'@sortable';
COMMENT ON FUNCTION actors_channels IS E'@sortable';
COMMENT ON FUNCTION actors_scenes IS E'@sortable';
`);
});
@ -920,9 +1009,6 @@ exports.up = knex => Promise.resolve()
exports.down = (knex) => { // eslint-disable-line arrow-body-style
// allow vim fold
return knex.raw(`
DROP VIEW IF EXISTS movie_actors;
DROP VIEW IF EXISTS movie_tags;
DROP TABLE IF EXISTS releases_actors CASCADE;
DROP TABLE IF EXISTS releases_movies CASCADE;
DROP TABLE IF EXISTS releases_directors CASCADE;
@ -934,6 +1020,10 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS releases_tags CASCADE;
DROP TABLE IF EXISTS releases_search CASCADE;
DROP TABLE IF EXISTS movies_covers CASCADE;
DROP TABLE IF EXISTS movies_scenes CASCADE;
DROP TABLE IF EXISTS movies_trailers CASCADE;
DROP TABLE IF EXISTS batches CASCADE;
DROP TABLE IF EXISTS actors_avatars CASCADE;
@ -951,6 +1041,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS networks_social CASCADE;
DROP TABLE IF EXISTS tags_posters CASCADE;
DROP TABLE IF EXISTS tags_photos CASCADE;
DROP TABLE IF EXISTS movies CASCADE;
DROP TABLE IF EXISTS releases CASCADE;
DROP TABLE IF EXISTS actors CASCADE;
DROP TABLE IF EXISTS directors CASCADE;
@ -974,8 +1065,12 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP FUNCTION IF EXISTS releases_is_new;
DROP FUNCTION IF EXISTS actors_tags;
DROP FUNCTION IF EXISTS actors_channels;
DROP FUNCTION IF EXISTS actors_scenes;
DROP FUNCTION IF EXISTS movies_actors;
DROP FUNCTION IF EXISTS movies_tags;
DROP TEXT SEARCH CONFIGURATION IF EXISTS traxxx;
DROP TEXT SEARCH DICTIONARY IF EXISTS traxxx_dict;
`);

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

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