Merge pull request #132 from friendlycrab/cleanup

Remove unused and generated code
This commit is contained in:
Leopere
2019-10-16 16:59:18 -04:00
committed by GitHub
176 changed files with 20 additions and 35695 deletions

7
.gitignore vendored
View File

@@ -18,6 +18,13 @@
# Packr2 artifacts
**/*-packr.go
# GraphQL generated output
pkg/models/generated_*.go
ui/v2/src/core/generated-*.tsx
# packr generated files
*-packr.go
####
# Jetbrains
####

View File

@@ -9,10 +9,9 @@ env:
- GO111MODULE=on
before_install:
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
- cd ui/v2
- yarn install
- CI=false yarn build # TODO: Fix warnings
- cd ../..
- yarn --cwd ui/v2 install
- make generate
- CI=false yarn --cwd ui/v2 build # TODO: Fix warnings
#- go get -v github.com/mgechev/revive
script:
#- make lint

View File

@@ -13,9 +13,9 @@ clean:
packr2 clean
# Regenerates GraphQL files
.PHONY: gqlgen
gqlgen:
go run scripts/gqlgen.go
.PHONY: generate
generate:
go generate
cd ui/v2 && yarn run gqlgen
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.

View File

@@ -88,15 +88,17 @@ TODO
## Commands
* `make generate` - Generate Go GraphQL and packr2 files
* `make build` - Builds the binary (make sure to build the UI as well... see below)
* `make gqlgen` - Regenerate Go GraphQL files
* `make ui` - Builds the frontend
* `make vet` - Run `go vet`
* `make lint` - Run the linter
## Building a release
1. cd into the `ui/v2` directory and run `yarn build` to compile the frontend
2. cd back to the root directory and run `make build` to build the executable for your current platform
1. Run `make generate` to create generated files
2. Run `make ui` to compile the frontend
3. Run `make build` to build the executable for your current platform
## Cross compiling

View File

@@ -1,3 +1,5 @@
//go:generate go run github.com/99designs/gqlgen
//go:generate go run github.com/gobuffalo/packr/v2/packr2
package main
import (

File diff suppressed because it is too large Load Diff

View File

@@ -1,453 +0,0 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package models
import (
"fmt"
"io"
"strconv"
)
type ConfigGeneralInput struct {
// Array of file paths to content
Stashes []string `json:"stashes"`
// Path to the SQLite database
DatabasePath *string `json:"databasePath"`
// Path to generated files
GeneratedPath *string `json:"generatedPath"`
}
type ConfigGeneralResult struct {
// Array of file paths to content
Stashes []string `json:"stashes"`
// Path to the SQLite database
DatabasePath string `json:"databasePath"`
// Path to generated files
GeneratedPath string `json:"generatedPath"`
}
type ConfigInterfaceInput struct {
// Custom CSS
CSS *string `json:"css"`
CSSEnabled *bool `json:"cssEnabled"`
}
type ConfigInterfaceResult struct {
// Custom CSS
CSS *string `json:"css"`
CSSEnabled *bool `json:"cssEnabled"`
}
// All configuration settings
type ConfigResult struct {
General *ConfigGeneralResult `json:"general"`
Interface *ConfigInterfaceResult `json:"interface"`
}
type FindFilterType struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"per_page"`
Sort *string `json:"sort"`
Direction *SortDirectionEnum `json:"direction"`
}
type FindGalleriesResultType struct {
Count int `json:"count"`
Galleries []*Gallery `json:"galleries"`
}
type FindPerformersResultType struct {
Count int `json:"count"`
Performers []*Performer `json:"performers"`
}
type FindSceneMarkersResultType struct {
Count int `json:"count"`
SceneMarkers []*SceneMarker `json:"scene_markers"`
}
type FindScenesResultType struct {
Count int `json:"count"`
Scenes []*Scene `json:"scenes"`
}
type FindStudiosResultType struct {
Count int `json:"count"`
Studios []*Studio `json:"studios"`
}
type GalleryFilesType struct {
Index int `json:"index"`
Name *string `json:"name"`
Path *string `json:"path"`
}
type GenerateMetadataInput struct {
Sprites bool `json:"sprites"`
Previews bool `json:"previews"`
Markers bool `json:"markers"`
Transcodes bool `json:"transcodes"`
}
type IntCriterionInput struct {
Value int `json:"value"`
Modifier CriterionModifier `json:"modifier"`
}
type MarkerStringsResultType struct {
Count int `json:"count"`
ID string `json:"id"`
Title string `json:"title"`
}
type PerformerCreateInput struct {
Name *string `json:"name"`
URL *string `json:"url"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Favorite *bool `json:"favorite"`
// This should be base64 encoded
Image string `json:"image"`
}
type PerformerDestroyInput struct {
ID string `json:"id"`
}
type PerformerFilterType struct {
// Filter by favorite
FilterFavorites *bool `json:"filter_favorites"`
}
type PerformerUpdateInput struct {
ID string `json:"id"`
Name *string `json:"name"`
URL *string `json:"url"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Favorite *bool `json:"favorite"`
// This should be base64 encoded
Image *string `json:"image"`
}
type ScanMetadataInput struct {
NameFromMetadata bool `json:"nameFromMetadata"`
}
type SceneFileType struct {
Size *string `json:"size"`
Duration *float64 `json:"duration"`
VideoCodec *string `json:"video_codec"`
AudioCodec *string `json:"audio_codec"`
Width *int `json:"width"`
Height *int `json:"height"`
Framerate *float64 `json:"framerate"`
Bitrate *int `json:"bitrate"`
}
type SceneFilterType struct {
// Filter by rating
Rating *IntCriterionInput `json:"rating"`
// Filter by resolution
Resolution *ResolutionEnum `json:"resolution"`
// Filter to only include scenes which have markers. `true` or `false`
HasMarkers *string `json:"has_markers"`
// Filter to only include scenes missing this property
IsMissing *string `json:"is_missing"`
// Filter to only include scenes with this studio
StudioID *string `json:"studio_id"`
// Filter to only include scenes with these tags
Tags []string `json:"tags"`
// Filter to only include scenes with this performer
PerformerID *string `json:"performer_id"`
}
type SceneMarkerCreateInput struct {
Title string `json:"title"`
Seconds float64 `json:"seconds"`
SceneID string `json:"scene_id"`
PrimaryTagID string `json:"primary_tag_id"`
TagIds []string `json:"tag_ids"`
}
type SceneMarkerFilterType struct {
// Filter to only include scene markers with this tag
TagID *string `json:"tag_id"`
// Filter to only include scene markers with these tags
Tags []string `json:"tags"`
// Filter to only include scene markers attached to a scene with these tags
SceneTags []string `json:"scene_tags"`
// Filter to only include scene markers with these performers
Performers []string `json:"performers"`
}
type SceneMarkerTag struct {
Tag *Tag `json:"tag"`
SceneMarkers []*SceneMarker `json:"scene_markers"`
}
type SceneMarkerUpdateInput struct {
ID string `json:"id"`
Title string `json:"title"`
Seconds float64 `json:"seconds"`
SceneID string `json:"scene_id"`
PrimaryTagID string `json:"primary_tag_id"`
TagIds []string `json:"tag_ids"`
}
type ScenePathsType struct {
Screenshot *string `json:"screenshot"`
Preview *string `json:"preview"`
Stream *string `json:"stream"`
Webp *string `json:"webp"`
Vtt *string `json:"vtt"`
ChaptersVtt *string `json:"chapters_vtt"`
}
type SceneUpdateInput struct {
ClientMutationID *string `json:"clientMutationId"`
ID string `json:"id"`
Title *string `json:"title"`
Details *string `json:"details"`
URL *string `json:"url"`
Date *string `json:"date"`
Rating *int `json:"rating"`
StudioID *string `json:"studio_id"`
GalleryID *string `json:"gallery_id"`
PerformerIds []string `json:"performer_ids"`
TagIds []string `json:"tag_ids"`
}
// A performer from a scraping operation...
type ScrapedPerformer struct {
Name *string `json:"name"`
URL *string `json:"url"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
}
type StatsResultType struct {
SceneCount int `json:"scene_count"`
GalleryCount int `json:"gallery_count"`
PerformerCount int `json:"performer_count"`
StudioCount int `json:"studio_count"`
TagCount int `json:"tag_count"`
}
type StudioCreateInput struct {
Name string `json:"name"`
URL *string `json:"url"`
// This should be base64 encoded
Image string `json:"image"`
}
type StudioDestroyInput struct {
ID string `json:"id"`
}
type StudioUpdateInput struct {
ID string `json:"id"`
Name *string `json:"name"`
URL *string `json:"url"`
// This should be base64 encoded
Image *string `json:"image"`
}
type TagCreateInput struct {
Name string `json:"name"`
}
type TagDestroyInput struct {
ID string `json:"id"`
}
type TagUpdateInput struct {
ID string `json:"id"`
Name string `json:"name"`
}
type CriterionModifier string
const (
// =
CriterionModifierEquals CriterionModifier = "EQUALS"
// !=
CriterionModifierNotEquals CriterionModifier = "NOT_EQUALS"
// >
CriterionModifierGreaterThan CriterionModifier = "GREATER_THAN"
// <
CriterionModifierLessThan CriterionModifier = "LESS_THAN"
// IS NULL
CriterionModifierIsNull CriterionModifier = "IS_NULL"
// IS NOT NULL
CriterionModifierNotNull CriterionModifier = "NOT_NULL"
CriterionModifierIncludes CriterionModifier = "INCLUDES"
CriterionModifierExcludes CriterionModifier = "EXCLUDES"
)
var AllCriterionModifier = []CriterionModifier{
CriterionModifierEquals,
CriterionModifierNotEquals,
CriterionModifierGreaterThan,
CriterionModifierLessThan,
CriterionModifierIsNull,
CriterionModifierNotNull,
CriterionModifierIncludes,
CriterionModifierExcludes,
}
func (e CriterionModifier) IsValid() bool {
switch e {
case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierGreaterThan, CriterionModifierLessThan, CriterionModifierIsNull, CriterionModifierNotNull, CriterionModifierIncludes, CriterionModifierExcludes:
return true
}
return false
}
func (e CriterionModifier) String() string {
return string(e)
}
func (e *CriterionModifier) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = CriterionModifier(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid CriterionModifier", str)
}
return nil
}
func (e CriterionModifier) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ResolutionEnum string
const (
// 240p
ResolutionEnumLow ResolutionEnum = "LOW"
// 480p
ResolutionEnumStandard ResolutionEnum = "STANDARD"
// 720p
ResolutionEnumStandardHd ResolutionEnum = "STANDARD_HD"
// 1080p
ResolutionEnumFullHd ResolutionEnum = "FULL_HD"
// 4k
ResolutionEnumFourK ResolutionEnum = "FOUR_K"
)
var AllResolutionEnum = []ResolutionEnum{
ResolutionEnumLow,
ResolutionEnumStandard,
ResolutionEnumStandardHd,
ResolutionEnumFullHd,
ResolutionEnumFourK,
}
func (e ResolutionEnum) IsValid() bool {
switch e {
case ResolutionEnumLow, ResolutionEnumStandard, ResolutionEnumStandardHd, ResolutionEnumFullHd, ResolutionEnumFourK:
return true
}
return false
}
func (e ResolutionEnum) String() string {
return string(e)
}
func (e *ResolutionEnum) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ResolutionEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ResolutionEnum", str)
}
return nil
}
func (e ResolutionEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type SortDirectionEnum string
const (
SortDirectionEnumAsc SortDirectionEnum = "ASC"
SortDirectionEnumDesc SortDirectionEnum = "DESC"
)
var AllSortDirectionEnum = []SortDirectionEnum{
SortDirectionEnumAsc,
SortDirectionEnumDesc,
}
func (e SortDirectionEnum) IsValid() bool {
switch e {
case SortDirectionEnumAsc, SortDirectionEnumDesc:
return true
}
return false
}
func (e SortDirectionEnum) String() string {
return string(e)
}
func (e *SortDirectionEnum) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = SortDirectionEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid SortDirectionEnum", str)
}
return nil
}
func (e SortDirectionEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View File

@@ -1,9 +0,0 @@
// +build ignore
package main
import "github.com/99designs/gqlgen/cmd"
func main() {
cmd.Execute()
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 StashApp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,8 +0,0 @@
# Stash Frontend V1
## Dev
* `yarn install` to install the modules
* `yarn start` to start the dev UI server on port 4200
* `yarn schema` to regenerate graphql code
* `ng build --prod` to build the dist directory

View File

@@ -1,105 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"stash-frontend": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:class": {
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:guard": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/stash-frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "stash-frontend:build"
},
"configurations": {
"production": {
"browserTarget": "stash-frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "stash-frontend:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "stash-frontend"
}

View File

@@ -1,13 +0,0 @@
schema: "../../graphql/schema/**/*.graphql"
overwrite: true
generates:
./../../schema/schema.json:
- introspection
./src/app/core/graphql-generated.ts:
documents: ./../../graphql/documents/**/*.graphql
plugins:
- add: "/* tslint:disable */"
- time
- typescript-common
- typescript-client
- typescript-apollo-angular

View File

@@ -1,66 +0,0 @@
{
"name": "stash-frontend",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"dev:server": "ng serve --disableHostCheck --host=0.0.0.0 --port 7001 --ssl true --ssl-cert '../../certs/server.crt' --ssl-key '../../certs/server.key'",
"schema": "gql-gen"
},
"private": true,
"dependencies": {
"@angular/animations": "7.2.1",
"@angular/common": "7.2.1",
"@angular/compiler": "7.2.1",
"@angular/core": "7.2.1",
"@angular/forms": "7.2.1",
"@angular/http": "7.2.1",
"@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1",
"apollo-angular": "1.5.0",
"apollo-angular-link-http": "1.4.0",
"apollo-cache-inmemory": "1.4.2",
"apollo-client": "2.4.12",
"apollo-link": "1.2.6",
"apollo-link-error": "1.1.5",
"apollo-link-ws": "1.0.14",
"core-js": "2.6.2",
"graphql": "14.1.1",
"graphql-code-generator": "0.15.2",
"graphql-codegen-add": "0.15.2",
"graphql-codegen-introspection": "0.15.2",
"graphql-codegen-time": "0.15.2",
"graphql-codegen-typescript-apollo-angular": "0.15.2",
"graphql-codegen-typescript-client": "0.15.2",
"graphql-codegen-typescript-common": "0.15.2",
"graphql-codegen-typescript-resolvers": "0.15.2",
"graphql-codegen-typescript-server": "0.15.2",
"graphql-tag": "2.10.1",
"ng-lazyload-image": "5.0.0",
"ng2-semantic-ui": "0.9.7",
"ngx-clipboard": "11.1.9",
"ngx-pagination": "3.2.1",
"rxjs": "6.3.3",
"rxjs-compat": "6.3.3",
"subscriptions-transport-ws": "0.9.15",
"zone.js": "0.8.28"
},
"devDependencies": {
"@angular/cli": "7.2.2",
"@angular/compiler-cli": "7.2.1",
"@angular/language-service": "7.2.1",
"@angular-devkit/build-angular": "0.12.2",
"@types/node": "10.12.18",
"@types/zen-observable": "0.8.0",
"codelyzer": "4.5.0",
"ts-node": "7.0.1",
"tslint": "5.12.1",
"typescript": "3.2.4"
}
}

View File

@@ -1,25 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './core/page-not-found/page-not-found.component';
import { DashboardComponent } from './core/dashboard/dashboard.component';
const appRoutes: Routes = [
{ path: '', component: DashboardComponent },
{ path: 'scenes', loadChildren: './scenes/scenes.module#ScenesModule' },
{ path: 'galleries', loadChildren: './galleries/galleries.module#GalleriesModule' },
{ path: 'performers', loadChildren: './performers/performers.module#PerformersModule' },
{ path: 'studios', loadChildren: './studios/studios.module#StudiosModule' },
{ path: 'tags', loadChildren: './tags/tags.module#TagsModule' },
{ path: 'settings', loadChildren: './settings/settings.module#SettingsModule' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(appRoutes)
],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule {}

View File

@@ -1,12 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-navigation-bar></app-navigation-bar>
<div class="ui main container">
<router-outlet></router-outlet>
</div>
`
})
export class AppComponent {}

View File

@@ -1,30 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
// App
import { AppComponent } from './app.component';
// Modules
import { CoreModule } from './core/core.module';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
// Only include non-lazy loaded modules here
CoreModule,
// Keep app routing last so that other module routes install first
AppRoutingModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {
// Diagnostic only: inspect router configuration
constructor(router: Router) {
console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
}
}

View File

@@ -1,45 +0,0 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { ApolloModule } from 'apollo-angular';
import { HttpLinkModule } from 'apollo-angular-link-http';
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { StashService } from './stash.service';
@NgModule({
imports: [
CommonModule,
RouterModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
ApolloModule,
HttpLinkModule
],
declarations: [
NavigationBarComponent,
PageNotFoundComponent,
DashboardComponent
],
exports: [
NavigationBarComponent,
PageNotFoundComponent,
DashboardComponent
],
providers: [
StashService
]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error('CoreModule is already loaded. Import it in the AppModule only');
}
}
}

View File

@@ -1,50 +0,0 @@
<div class="ui inverted center aligned stackable vertically divided grid">
<div class="one column row">
<div class="ui inverted statistics column">
<div class="statistic">
<div class="value">
{{stats?.scene_count}}
</div>
<div class="label">
Scenes
</div>
</div>
<div class="statistic">
<div class="value">
{{stats?.gallery_count}}
</div>
<div class="label">
Galleries
</div>
</div>
<div class="statistic">
<div class="value">
{{stats?.performer_count}}
</div>
<div class="label">
Performers
</div>
</div>
<div class="statistic">
<div class="value">
{{stats?.studio_count}}
</div>
<div class="label">
Studios
</div>
</div>
<div class="statistic">
<div class="value">
{{stats?.tag_count}}
</div>
<div class="label">
Tags
</div>
</div>
</div>
</div>
<div class="one column row">
<div class="column">
</div>
</div>
</div>

View File

@@ -1,23 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { StashService } from '../stash.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
stats: any;
constructor(private stashService: StashService) {}
ngOnInit() {
this.fetchStats();
}
async fetchStats() {
const result = await this.stashService.stats().result();
this.stats = result.data.stats;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
<div class="ui inverted top fixed menu">
<div class="ui container">
<div class="header item">
<a routerLink="/">Stash</a>
</div>
<a routerLink="/scenes" routerLinkActive #rla="routerLinkActive" [class.active]="isScenesActiveHack(rla)" class="item">
<i class="video play icon"></i>Scenes
</a>
<a routerLink="/scenes/markers" routerLinkActive="active" class="item">
<i class="marker icon"></i>
Markers
</a>
<a routerLink="/galleries" routerLinkActive="active" class="item">
<i class="image icon"></i>
Galleries
</a>
<a routerLink="/performers" routerLinkActive="active" class="item">
<i class="user icon"></i>
Performers
</a>
<a routerLink="/studios" routerLinkActive="active" class="item">
<i class="record icon"></i>
Studios
</a>
<a routerLink="/tags" routerLinkActive="active" class="item">
<i class="tags icon"></i>
Tags
</a>
<a routerLink="/scenes/wall" routerLinkActive="active" class="item">
<i class="film icon"></i>
Scene Wall
</a>
<a routerLink="/settings" routerLinkActive="active" class="item">
<i class="settings icon"></i>
Settings
</a>
</div>
</div>

View File

@@ -1,22 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-navigation-bar',
templateUrl: './navigation-bar.component.html',
styleUrls: ['./navigation-bar.component.css']
})
export class NavigationBarComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit() {
}
isScenesActiveHack(rla) {
return rla.isActive &&
this.router.url !== '/scenes/wall' &&
!this.router.url.includes('/scenes/markers');
}
}

View File

@@ -1 +0,0 @@
<h1>Page not found</h1>

View File

@@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-page-not-found',
templateUrl: './page-not-found.component.html',
styleUrls: ['./page-not-found.component.css']
})
export class PageNotFoundComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -1,508 +0,0 @@
import { Injectable } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { ListFilter } from '../shared/models/list-state.model';
import { Apollo, QueryRef } from 'apollo-angular';
import { HttpLink } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import * as GQL from './graphql-generated';
import {WebSocketLink} from "apollo-link-ws";
@Injectable()
export class StashService {
private findScenesGQL = new GQL.FindScenesGQL(this.apollo);
private findSceneGQL = new GQL.FindSceneGQL(this.apollo);
private findSceneForEditingGQL = new GQL.FindSceneForEditingGQL(this.apollo);
private findSceneMarkersGQL = new GQL.FindSceneMarkersGQL(this.apollo);
private sceneWallGQL = new GQL.SceneWallGQL(this.apollo);
private markerWallGQL = new GQL.MarkerWallGQL(this.apollo);
private findPerformersGQL = new GQL.FindPerformersGQL(this.apollo);
private findPerformerGQL = new GQL.FindPerformerGQL(this.apollo);
private findStudiosGQL = new GQL.FindStudiosGQL(this.apollo);
private findStudioGQL = new GQL.FindStudioGQL(this.apollo);
private findGalleriesGQL = new GQL.FindGalleriesGQL(this.apollo);
private findGalleryGQL = new GQL.FindGalleryGQL(this.apollo);
private findTagGQL = new GQL.FindTagGQL(this.apollo);
private markerStringsGQL = new GQL.MarkerStringsGQL(this.apollo);
private scrapeFreeonesGQL = new GQL.ScrapeFreeonesGQL(this.apollo);
private scrapeFreeonesPerformersGQL = new GQL.ScrapeFreeonesPerformersGQL(this.apollo);
private allTagsGQL = new GQL.AllTagsGQL(this.apollo);
private allTagsForFilterGQL = new GQL.AllTagsForFilterGQL(this.apollo);
private allPerformersForFilterGQL = new GQL.AllPerformersForFilterGQL(this.apollo);
private statsGQL = new GQL.StatsGQL(this.apollo);
private sceneUpdateGQL = new GQL.SceneUpdateGQL(this.apollo);
private performerCreateGQL = new GQL.PerformerCreateGQL(this.apollo);
private performerUpdateGQL = new GQL.PerformerUpdateGQL(this.apollo);
private studioCreateGQL = new GQL.StudioCreateGQL(this.apollo);
private studioUpdateGQL = new GQL.StudioUpdateGQL(this.apollo);
private tagCreateGQL = new GQL.TagCreateGQL(this.apollo);
private tagDestroyGQL = new GQL.TagDestroyGQL(this.apollo);
private tagUpdateGQL = new GQL.TagUpdateGQL(this.apollo);
private sceneMarkerCreateGQL = new GQL.SceneMarkerCreateGQL(this.apollo);
private sceneMarkerUpdateGQL = new GQL.SceneMarkerUpdateGQL(this.apollo);
private sceneMarkerDestroyGQL = new GQL.SceneMarkerDestroyGQL(this.apollo);
private metadataImportGQL = new GQL.MetadataImportGQL(this.apollo);
private metadataExportGQL = new GQL.MetadataExportGQL(this.apollo);
private metadataScanGQL = new GQL.MetadataScanGQL(this.apollo);
private metadataGenerateGQL = new GQL.MetadataGenerateGQL(this.apollo);
private metadataCleanGQL = new GQL.MetadataCleanGQL(this.apollo);
private metadataUpdateGQL = new GQL.MetadataUpdateGQL(this.apollo);
constructor(private apollo: Apollo, private platformLocation: PlatformLocation, private httpLink: HttpLink) {
const platform: any = platformLocation;
const platformUrl = new URL(platform.location.origin);
platformUrl.port = platformUrl.protocol === 'https:' ? '9999' : '9998';
const url = platformUrl.toString().slice(0, -1);
const webSocketScheme = platformUrl.protocol === 'https:' ? 'wss' : 'ws';
const wsLink = new WebSocketLink({
uri: `${webSocketScheme}://${platform.location.hostname}:${platformUrl.port}/graphql`,
options: {
reconnect: true
}
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
const httpLinkHandler = httpLink.create({uri: `${url}/graphql`});
const splitLink = ApolloLink.split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
wsLink,
httpLinkHandler
);
const link = ApolloLink.from([
errorLink,
splitLink
]);
apollo.create({
link: link,
defaultOptions: {
watchQuery: {
fetchPolicy: 'network-only',
errorPolicy: 'all'
},
},
cache: new InMemoryCache({
// dataIdFromObject: o => {
// if (o.__typename === "MarkerStringsResultType") {
// return `${o.__typename}:${o.title}`
// } else {
// return `${o.__typename}:${o.id}`
// }
// },
cacheRedirects: {
Query: {
findScene: (rootValue, args, context) => {
return context.getCacheKey({__typename: 'Scene', id: args.id});
}
}
},
})
});
}
findScenes(page?: number, filter?: ListFilter): QueryRef<GQL.FindScenes.Query, Record<string, any>> {
let scene_filter = {};
if (filter.criteriaFilterOpen) {
scene_filter = filter.makeSceneFilter();
}
if (filter.customCriteria) {
filter.customCriteria.forEach(criteria => {
scene_filter[criteria.key] = criteria.value;
});
}
return this.findScenesGQL.watch({
filter: {
q: filter.searchTerm,
page: page,
per_page: filter.itemsPerPage,
sort: filter.sortBy,
direction: filter.sortDirection === 'asc' ? GQL.SortDirectionEnum.Asc : GQL.SortDirectionEnum.Desc
},
scene_filter: scene_filter
});
}
findScene(id?: any, checksum?: string) {
return this.findSceneGQL.watch({
id: id,
checksum: checksum
});
}
findSceneForEditing(id?: any) {
return this.findSceneForEditingGQL.watch({
id: id
});
}
findSceneMarkers(page?: number, filter?: ListFilter) {
let scene_marker_filter = {};
if (filter.criteriaFilterOpen) {
scene_marker_filter = filter.makeSceneMarkerFilter();
}
if (filter.customCriteria) {
filter.customCriteria.forEach(criteria => {
scene_marker_filter[criteria.key] = criteria.value;
});
}
return this.findSceneMarkersGQL.watch({
filter: {
q: filter.searchTerm,
page: page,
per_page: filter.itemsPerPage,
sort: filter.sortBy,
direction: filter.sortDirection === 'asc' ? GQL.SortDirectionEnum.Asc : GQL.SortDirectionEnum.Desc
},
scene_marker_filter: scene_marker_filter
});
}
sceneWall(q?: string) {
return this.sceneWallGQL.watch({
q: q
},
{
fetchPolicy: 'network-only'
});
}
markerWall(q?: string) {
return this.markerWallGQL.watch({
q: q
},
{
fetchPolicy: 'network-only'
});
}
findPerformers(page?: number, filter?: ListFilter) {
let performer_filter = {};
if (filter.criteriaFilterOpen) {
performer_filter = filter.makePerformerFilter();
}
// if (filter.customCriteria) {
// filter.customCriteria.forEach(criteria => {
// scene_filter[criteria.key] = criteria.value;
// });
// }
return this.findPerformersGQL.watch({
filter: {
q: filter.searchTerm,
page: page,
per_page: filter.itemsPerPage,
sort: filter.sortBy,
direction: filter.sortDirection === 'asc' ? GQL.SortDirectionEnum.Asc : GQL.SortDirectionEnum.Desc
},
performer_filter: performer_filter
});
}
findPerformer(id: any) {
return this.findPerformerGQL.watch({
id: id
});
}
findStudios(page?: number, filter?: ListFilter) {
return this.findStudiosGQL.watch({
filter: {
q: filter.searchTerm,
page: page,
per_page: filter.itemsPerPage,
sort: filter.sortBy,
direction: filter.sortDirection === 'asc' ? GQL.SortDirectionEnum.Asc : GQL.SortDirectionEnum.Desc
}
});
}
findStudio(id: any) {
return this.findStudioGQL.watch({
id: id
});
}
findGalleries(page?: number, filter?: ListFilter) {
return this.findGalleriesGQL.watch({
filter: {
q: filter.searchTerm,
page: page,
per_page: filter.itemsPerPage,
sort: filter.sortBy,
direction: filter.sortDirection === 'asc' ? GQL.SortDirectionEnum.Asc : GQL.SortDirectionEnum.Desc
}
});
}
findGallery(id: any) {
return this.findGalleryGQL.watch({
id: id
});
}
findTag(id: any) {
return this.findTagGQL.watch({
id: id
});
}
markerStrings(q?: string, sort?: string) {
return this.markerStringsGQL.watch({
q: q,
sort: sort
});
}
scrapeFreeones(performer_name: string) {
return this.scrapeFreeonesGQL.watch({
performer_name: performer_name
});
}
scrapeFreeonesPerformers(query: string) {
return this.scrapeFreeonesPerformersGQL.watch({
q: query
});
}
allTags() {
return this.allTagsGQL.watch();
}
allTagsForFilter() {
return this.allTagsForFilterGQL.watch();
}
allPerformersForFilter() {
return this.allPerformersForFilterGQL.watch();
}
stats() {
return this.statsGQL.watch();
}
sceneUpdate(scene: GQL.SceneUpdate.Variables) {
return this.sceneUpdateGQL.mutate({
id: scene.id,
title: scene.title,
details: scene.details,
url: scene.url,
date: scene.date,
rating: scene.rating,
studio_id: scene.studio_id,
gallery_id: scene.gallery_id,
performer_ids: scene.performer_ids,
tag_ids: scene.tag_ids
},
{
refetchQueries: [
{
query: this.findSceneGQL.document,
variables: {
id: scene.id
}
}
]
});
}
performerCreate(performer: GQL.PerformerCreate.Variables) {
return this.performerCreateGQL.mutate({
name: performer.name,
url: performer.url,
birthdate: performer.birthdate,
ethnicity: performer.ethnicity,
country: performer.country,
eye_color: performer.eye_color,
height: performer.height,
measurements: performer.measurements,
fake_tits: performer.fake_tits,
career_length: performer.career_length,
tattoos: performer.tattoos,
piercings: performer.piercings,
aliases: performer.aliases,
twitter: performer.twitter,
instagram: performer.instagram,
favorite: performer.favorite,
image: performer.image
});
}
performerUpdate(performer: GQL.PerformerUpdate.Variables) {
return this.performerUpdateGQL.mutate({
id: performer.id,
name: performer.name,
url: performer.url,
birthdate: performer.birthdate,
ethnicity: performer.ethnicity,
country: performer.country,
eye_color: performer.eye_color,
height: performer.height,
measurements: performer.measurements,
fake_tits: performer.fake_tits,
career_length: performer.career_length,
tattoos: performer.tattoos,
piercings: performer.piercings,
aliases: performer.aliases,
twitter: performer.twitter,
instagram: performer.instagram,
favorite: performer.favorite,
image: performer.image
},
{
refetchQueries: [
{
query: this.findPerformerGQL.document,
variables: {
id: performer.id
}
}
],
});
}
studioCreate(studio: GQL.StudioCreate.Variables) {
return this.studioCreateGQL.mutate({
name: studio.name,
url: studio.url,
image: studio.image
});
}
studioUpdate(studio: GQL.StudioUpdate.Variables) {
return this.studioUpdateGQL.mutate({
id: studio.id,
name: studio.name,
url: studio.url,
image: studio.image
},
{
refetchQueries: [
{
query: this.findStudioGQL.document,
variables: {
id: studio.id
}
}
],
});
}
tagCreate(tag: GQL.TagCreate.Variables) {
return this.tagCreateGQL.mutate({
name: tag.name
});
}
tagDestroy(tag: GQL.TagDestroy.Variables) {
return this.tagDestroyGQL.mutate({
id: tag.id
});
}
tagUpdate(tag: GQL.TagUpdate.Variables) {
return this.tagUpdateGQL.mutate({
id: tag.id,
name: tag.name
},
{
refetchQueries: [
{
query: this.findTagGQL.document,
variables: {
id: tag.id
}
}
],
});
}
markerCreate(marker: GQL.SceneMarkerCreate.Variables) {
return this.sceneMarkerCreateGQL.mutate({
title: marker.title,
seconds: marker.seconds,
scene_id: marker.scene_id,
primary_tag_id: marker.primary_tag_id,
tag_ids: marker.tag_ids
},
{
refetchQueries: () => ['FindScene']
});
}
markerUpdate(marker: GQL.SceneMarkerUpdate.Variables) {
return this.sceneMarkerUpdateGQL.mutate({
id: marker.id,
title: marker.title,
seconds: marker.seconds,
scene_id: marker.scene_id,
primary_tag_id: marker.primary_tag_id,
tag_ids: marker.tag_ids
},
{
refetchQueries: () => ['FindScene']
});
}
markerDestroy(id: any, scene_id: any) {
return this.sceneMarkerDestroyGQL.mutate({
id: id
},
{
refetchQueries: () => ['FindScene']
});
}
metadataImport() {
return this.metadataImportGQL.watch();
}
metadataExport() {
return this.metadataExportGQL.watch();
}
metadataScan() {
return this.metadataScanGQL.watch();
}
metadataGenerate() {
return this.metadataGenerateGQL.watch();
}
metadataClean() {
return this.metadataCleanGQL.watch();
}
metadataUpdate() {
return this.metadataUpdateGQL.subscribe()
}
}

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { GalleriesComponent } from './galleries/galleries.component';
import { GalleryListComponent } from './gallery-list/gallery-list.component';
import { GalleryDetailComponent } from './gallery-detail/gallery-detail.component';
const routes: Routes = [
{ path: '',
component: GalleriesComponent,
children: [
{ path: '', component: GalleryListComponent },
{ path: ':id', component: GalleryDetailComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GalleriesRoutingModule { }

View File

@@ -1,25 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { GalleriesRoutingModule } from './galleries-routing.module';
import { GalleriesService } from './galleries.service';
import { GalleriesComponent } from './galleries/galleries.component';
import { GalleryDetailComponent } from './gallery-detail/gallery-detail.component';
import { GalleryListComponent } from './gallery-list/gallery-list.component';
@NgModule({
imports: [
SharedModule,
GalleriesRoutingModule
],
declarations: [
GalleriesComponent,
GalleryDetailComponent,
GalleryListComponent
],
providers: [
GalleriesService
]
})
export class GalleriesModule { }

View File

@@ -1,11 +0,0 @@
import { Injectable } from '@angular/core';
import { GalleryListState } from '../shared/models/list-state.model';
@Injectable()
export class GalleriesService {
listState: GalleryListState = new GalleryListState();
constructor() { }
}

View File

@@ -1,13 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-galleries',
template: '<router-outlet></router-outlet>'
})
export class GalleriesComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1,40 +0,0 @@
#gallery-modal-container {
width: 100%;
height: 100%;
left: 0;
top: 0;
overflow: hidden;
z-index: 1500;
position: fixed;
}
#gallery-modal-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background:rgba(0,0,0,0.5);
}
#gallery-modal-image-wrapper {
position: absolute;
transform-origin: left top;
transition: transform 333ms cubic-bezier(.4,0,.22,1);
left: 0;
right: 0;
top: 0;
bottom: 0;
display: block;
justify-content: center;
align-content: center;
}
#gallery-modal-image {
width: 100%;
height: 100%;
object-fit: contain;
padding: 20px;
background: rgba(17, 17, 17, 0.5);
z-index: 1;
}

View File

@@ -1,22 +0,0 @@
<div
*ngIf="!!displayedImage"
(window:keydown)="onKey($event)"
id="gallery-modal-container">
<div id="gallery-modal-background"></div>
<div *ngIf="!!displayedImage.path" id="gallery-modal-image-wrapper">
<img id="gallery-modal-image" [src]="displayedImage.path" />
</div>
</div>
<div class="ui text menu">
<h3 class="header item">{{gallery?.title || 'No Title'}} - {{gallery?.files.length}} files</h3>
<div class="right menu">
<button (click)="onClickEdit()" class="ui button">Edit</button>
</div>
</div>
<app-gallery-preview
[gallery]="gallery"
[type]="'full'"
(clicked)="onClickCard($event)">
</app-gallery-preview>

View File

@@ -1,92 +0,0 @@
import { Component, OnInit, HostListener } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { GalleryImage } from '../../shared/models/gallery.model';
import { GalleryData } from '../../core/graphql-generated';
@Component({
selector: 'app-gallery-detail',
templateUrl: './gallery-detail.component.html',
styleUrls: ['./gallery-detail.component.css']
})
export class GalleryDetailComponent implements OnInit {
gallery: GalleryData.Fragment;
displayedImage: GalleryImage = null;
constructor(
private route: ActivatedRoute,
private stashService: StashService
) {}
ngOnInit() {
this.getGallery();
window.scrollTo(0, 0);
}
async getGallery() {
const id = parseInt(this.route.snapshot.params['id'], 10);
const result = await this.stashService.findGallery(id).result();
this.gallery = result.data.findGallery;
}
@HostListener('mousewheel', ['$event'])
onMousewheel(event) {
this.displayedImage = null;
}
@HostListener('window:mouseup', ['$event'])
onMouseup(event: MouseEvent) {
if (event.button !== 0 || !(event.target instanceof HTMLDivElement)) { return; }
const target: HTMLDivElement = event.target;
if (target.id !== 'gallery-image') {
this.displayedImage = null;
} else {
window.open(this.displayedImage.path, '_blank');
}
}
onClickEdit() {
// TODO
console.log('edit');
}
onClickCard(galleryImage: GalleryImage) {
console.log(galleryImage);
this.displayedImage = galleryImage;
}
onKey(event) {
const i = this.displayedImage.index;
console.log(event);
switch (event.key) {
case 'ArrowLeft': {
this.displayedImage = this.gallery.files[i - 1];
break;
}
case 'ArrowRight': {
this.displayedImage = this.gallery.files[i + 1];
break;
}
case 'ArrowUp': {
window.open(this.displayedImage.path, '_blank');
break;
}
case 'ArrowDown': {
this.displayedImage = null;
break;
}
default:
break;
}
event.preventDefault();
}
}

View File

@@ -1,7 +0,0 @@
<div class="ui text menu">
<div class="right menu">
<button (click)="onClickNew()" class="ui button">New</button>
</div>
</div>
<app-list [state]="state"></app-list>

View File

@@ -1,23 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { GalleriesService } from '../galleries.service';
@Component({
selector: 'app-gallery-list',
templateUrl: './gallery-list.component.html'
})
export class GalleryListComponent implements OnInit {
state = this.galleriesService.listState;
constructor(private galleriesService: GalleriesService,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit() {}
onClickNew() {
this.router.navigate(['new'], { relativeTo: this.route });
}
}

View File

@@ -1,122 +0,0 @@
<div class="ui three column grid" style="position: fixed; filter: blur(4px); z-index: -1; transform: translateZ(0); left: calc(-50vw + 50.7%); width: 100vw;">
<div *ngFor="let scene of sceneListState.data | shuffle | slice:0:6" class="column" style="background-size: cover; background-position: center center; height: 100vh;" [style.background-image]="'url(' + scene.paths.screenshot + ')'">
</div>
</div>
<div style="background-color: rgba(255, 255, 255, 0.7); margin-top: -1.8em;">
<div class="ui basic segment">
<div class="ui two column divided middle aligned very relaxed stackable grid" style="position: relative;">
<div class="column">
<img *ngIf="!!performer" [src]="performer?.image_path" class="ui fluid bordered image" />
</div>
<div class="center aligned column">
<div class="ui huge very relaxed list items">
<div class="item">
<div class="content">
<div class="header">
{{performer?.name}}
<button (click)="onClickEdit()" class="ui right floated button">Edit</button>
</div>
<div *ngIf="!!performer?.aliases" class="meta">{{performer.aliases}}</div>
<div *ngIf="!!performer?.birthdate" class="extra">
{{performer?.birthdate | date:"MM/dd/yy"}} - Age {{performer?.birthdate | age}}
</div>
</div>
</div>
<div class="item">
<div class="content">
<div class="header">Details</div>
<div class="description">
<div class="ui mini list">
<div *ngIf="!!performer?.career_length" class="item">
<span class="bold">Career Length</span>
{{performer.career_length}}
</div>
<div *ngIf="!!performer?.country" class="item">
<span class="bold">Country</span>
{{performer.country}}
</div>
<div *ngIf="!!performer?.ethnicity" class="item">
<span class="bold">Ethnicity</span>
{{performer.ethnicity?.toUpperCase()}}
</div>
<div *ngIf="!!performer?.eye_color" class="item">
<span class="bold">Eye Color</span>
{{performer.eye_color}}
</div>
<div *ngIf="!!performer?.height" class="item">
<span class="bold">Height (cm)</span>
{{performer.height}}
</div>
<div *ngIf="!!performer?.measurements" class="item">
<span class="bold">Measurements</span>
{{performer.measurements}}
</div>
<div *ngIf="!!performer?.fake_tits" class="item">
<span class="bold">Fake Tits</span>
{{performer.fake_tits}}
</div>
<div *ngIf="!!performer?.tattoos" class="item">
<span class="bold">Tattoos</span>
{{performer.tattoos}}
</div>
<div *ngIf="!!performer?.piercings" class="item">
<span class="bold">Piercings</span>
{{performer.piercings}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="performer?.twitter?.length > 0 || performer?.instagram?.length > 0" class="ui basic segment">
<h3>Social</h3>
<div class="ui horizontal list">
<div *ngIf="!!performer?.twitter && performer?.twitter?.length > 0" class="item">
<i class="big twitter icon"></i>
<div class="content">
<a [href]="twitterLink()">{{performer.twitter}}</a>
</div>
</div>
<div *ngIf="!!performer?.instagram && performer?.instagram?.length > 0" class="item">
<i class="big instagram icon"></i>
<div class="content">
<a [href]="instagramLink()">{{performer.instagram}}</a>
</div>
</div>
</div>
</div>
<div class="ui basic segment">
<div class="ui list">
<a *ngIf="!!performer?.url" [href]="performer?.url" class="item" title="More information">
<i class="linkify icon"></i>
<div class="content">
<div class="header">URL</div>
<div class="description">{{performer?.url}}</div>
</div>
</a>
</div>
</div>
</div>
<div class="ui fluid container" style="
left: 0;
right: 0;
position: absolute;
background-color: #000;
padding: 2rem;
box-shadow: 0px 0px 5px 0px rgba(34, 36, 38, 0.40);
">
<div class="ui container">
<h1 class="header">
{{performer?.name || 'Performer'}}'s Scenes
</h1>
<app-list [state]="sceneListState"></app-list>
</div>
</div>

View File

@@ -1,56 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { PerformersService } from '../performers.service';
import { PerformerData } from '../../core/graphql-generated';
import { SceneListState, CustomCriteria } from '../../shared/models/list-state.model';
@Component({
selector: 'app-performer-detail',
templateUrl: './performer-detail.component.html',
styleUrls: ['./performer-detail.component.css']
})
export class PerformerDetailComponent implements OnInit {
performer: PerformerData.Fragment;
sceneListState: SceneListState;
constructor(
private route: ActivatedRoute,
private stashService: StashService,
private performerService: PerformersService,
private router: Router
) {}
ngOnInit() {
const id = parseInt(this.route.snapshot.params['id'], 10);
this.sceneListState = this.performerService.detailsSceneListState;
this.sceneListState.filter.customCriteria = [];
this.sceneListState.filter.customCriteria.push(new CustomCriteria('performer_id', id.toString()));
this.getPerformer();
window.scrollTo(0, 0);
}
getPerformer() {
const id = parseInt(this.route.snapshot.params['id'], 10);
this.stashService.findPerformer(id).valueChanges.subscribe(performer => {
this.performer = performer.data.findPerformer;
});
}
onClickEdit() {
this.router.navigate(['edit'], { relativeTo: this.route });
}
twitterLink(): string {
return 'http://www.twitter.com/' + this.performer.twitter;
}
instagramLink(): string {
return 'http://www.instagram.com/' + this.performer.instagram;
}
}

View File

@@ -1,118 +0,0 @@
<div [class.loading]="loading" class="ui inverted form">
<h4 class="ui inverted dividing header">Performer Information</h4>
<div class="field">
<div class="fields">
<div class="fourteen wide field">
<label>Name</label>
<div class="ui search focus">
<input [formControl]="searchFormControl" type="text" placeholder="Name" />
<div *ngIf="performerNameOptions.length > 0 && !selectedName" class="results transition visible">
<a *ngFor="let name of performerNameOptions" (click)="onClickedPerformerName(name)" class="result">
<div class="content">
<div class="title">{{name}}</div>
</div>
</a>
</div>
</div>
</div>
<div class="two wide field">
<label>Favorite</label>
<div (click)="onFavoriteChange()" class="ui massive heart rating">
<i class="icon" [class.active]="favorite"></i>
</div>
</div>
</div>
<div class="field">
<label>Aliases</label>
<input [(ngModel)]="aliases" type="text" placeholder="Aliases" />
</div>
<div class="equal width fields">
<div class="field">
<label>Origin Country</label>
<input [(ngModel)]="country" type="text" placeholder="Origin Country" />
</div>
<div class="field">
<label>Birth Date (YYYY-MM-DD)</label>
<input [(ngModel)]="birthdate" type="text" placeholder="YYYY-MM-DD" />
</div>
<div class="field">
<label>Ethnicity</label>
<sui-select
[(ngModel)]="ethnicity"
[options]="ethnicityOptions"
placeholder="Ethnicity"
#ethnicitySelect>
<sui-select-option *ngFor="let option of ethnicitySelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
</div>
<div class="equal width fields">
<div class="field">
<label>Eye Color</label>
<input [(ngModel)]="eye_color" type="text" placeholder="Eye Color" />
</div>
<div class="field">
<label>Height (cm)</label>
<input [(ngModel)]="height" type="text" placeholder="Height (cm)" />
</div>
<div class="field">
<label>Measurements</label>
<input [(ngModel)]="measurements" type="text" placeholder="Measurements" />
</div>
<div class="field">
<label>Fake Tits</label>
<input [(ngModel)]="fake_tits" type="text" placeholder="Fake Tits" />
</div>
<div class="field">
<label>Career Length</label>
<input [(ngModel)]="career_length" type="text" placeholder="Career Length" />
</div>
</div>
<div class="equal width fields">
<div class="field">
<label>Tattoos</label>
<input [(ngModel)]="tattoos" type="text" placeholder="Tattoos" />
</div>
<div class="field">
<label>Piercings</label>
<input [(ngModel)]="piercings" type="text" placeholder="Piercings" />
</div>
</div>
<div class="equal width fields">
<div class="field">
<label>URL</label>
<input [(ngModel)]="url" type="url" placeholder="URL" />
</div>
<div class="field">
<label>Twitter Handle</label>
<input [(ngModel)]="twitter" type="text" placeholder="Twitter Handle" />
</div>
<div class="field">
<label>Instagram Handle</label>
<input [(ngModel)]="instagram" type="text" placeholder="Instagram Handle" />
</div>
</div>
<div class="field">
<label>Image</label>
<div class="fields">
<div class="fourteen wide field">
<input #imageInput type="file" (change)="onImageChange($event)" placeholder="Upload file" accept=".jpg,.jpeg">
</div>
<div class="two wide field">
<div class="ui small image">
<img *ngIf="!!imagePreview" [src]="imagePreview" />
</div>
<button class="ui button" (click)="onResetImage(imageInput)">Reset</button>
</div>
</div>
</div>
</div>
<button (click)="onSubmit()" class="ui primary submit button">Submit</button>
<button (click)="onScrape()" alt="Type in an exact name and click" class="ui primary submit button">Scrape From Freeones</button>
</div>

View File

@@ -1,204 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-performer-form',
templateUrl: './performer-form.component.html',
styleUrls: ['./performer-form.component.css']
})
export class PerformerFormComponent implements OnInit, OnDestroy {
name: string;
favorite: boolean;
aliases: string;
country: string;
birthdate: string;
ethnicity: string;
eye_color: string;
height: string;
measurements: string;
fake_tits: string;
career_length: string;
tattoos: string;
piercings: string;
url: string;
twitter: string;
instagram: string;
image: string;
loading = true;
imagePreview: string;
image_path: string;
ethnicityOptions: string[] = ['white', 'black', 'asian', 'hispanic'];
performerNameOptions: string[] = [];
selectedName: string;
searchFormControl = new FormControl();
constructor(
private route: ActivatedRoute,
private stashService: StashService,
private router: Router
) {}
ngOnInit() {
this.getPerformer();
this.searchFormControl.valueChanges.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(term => {
this.name = term;
this.getPerformerNames(term);
});
}
ngOnDestroy() {}
async getPerformer() {
const id = parseInt(this.route.snapshot.params['id'], 10);
if (!!id === false) {
console.log('new performer');
this.loading = false;
return;
}
const result = await this.stashService.findPerformer(id).result();
this.loading = result.loading;
this.name = result.data.findPerformer.name;
this.selectedName = this.name;
this.searchFormControl.setValue(this.name);
this.favorite = result.data.findPerformer.favorite;
this.aliases = result.data.findPerformer.aliases;
this.country = result.data.findPerformer.country;
this.birthdate = result.data.findPerformer.birthdate;
this.ethnicity = result.data.findPerformer.ethnicity;
this.eye_color = result.data.findPerformer.eye_color;
this.height = result.data.findPerformer.height;
this.measurements = result.data.findPerformer.measurements;
this.fake_tits = result.data.findPerformer.fake_tits;
this.career_length = result.data.findPerformer.career_length;
this.tattoos = result.data.findPerformer.tattoos;
this.piercings = result.data.findPerformer.piercings;
this.url = result.data.findPerformer.url;
this.twitter = result.data.findPerformer.twitter;
this.instagram = result.data.findPerformer.instagram;
this.image_path = result.data.findPerformer.image_path;
this.imagePreview = this.image_path;
}
async getPerformerNames(query: string) {
if (query === undefined) { return; }
if (this.selectedName !== this.name) { this.selectedName = null; }
const result = await this.stashService.scrapeFreeonesPerformers(query).result();
this.performerNameOptions = result.data.scrapeFreeonesPerformerList;
}
onClickedPerformerName(name) {
this.name = name;
this.selectedName = name;
this.searchFormControl.setValue(this.name);
}
onImageChange(event) {
const file: File = event.target.files[0];
const reader: FileReader = new FileReader();
reader.onloadend = (e) => {
this.image = reader.result as string;
this.imagePreview = this.image;
};
reader.readAsDataURL(file);
}
onResetImage(imageInput) {
imageInput.value = '';
this.imagePreview = this.image_path;
this.image = null;
}
onFavoriteChange() {
this.favorite = !this.favorite;
}
onSubmit() {
const id = this.route.snapshot.params['id'];
if (!!id) {
this.stashService.performerUpdate({
id: id,
name: this.name,
url: this.url,
birthdate: this.birthdate,
ethnicity: this.ethnicity,
country: this.country,
eye_color: this.eye_color,
height: this.height,
measurements: this.measurements,
fake_tits: this.fake_tits,
career_length: this.career_length,
tattoos: this.tattoos,
piercings: this.piercings,
aliases: this.aliases,
twitter: this.twitter,
instagram: this.instagram,
favorite: this.favorite,
image: this.image
}).subscribe(result => {
this.router.navigate(['/performers', id]);
});
} else {
this.stashService.performerCreate({
name: this.name,
url: this.url,
birthdate: this.birthdate,
ethnicity: this.ethnicity,
country: this.country,
eye_color: this.eye_color,
height: this.height,
measurements: this.measurements,
fake_tits: this.fake_tits,
career_length: this.career_length,
tattoos: this.tattoos,
piercings: this.piercings,
aliases: this.aliases,
twitter: this.twitter,
instagram: this.instagram,
favorite: this.favorite,
image: this.image
}).subscribe(result => {
this.router.navigate(['/performers', result.data.performerCreate.id]);
});
}
}
async onScrape() {
this.loading = true;
const result = await this.stashService.scrapeFreeones(this.name).result();
this.loading = false;
this.url = result.data.scrapeFreeones.url;
this.name = result.data.scrapeFreeones.name;
this.searchFormControl.setValue(this.name);
this.aliases = result.data.scrapeFreeones.aliases;
this.country = result.data.scrapeFreeones.country;
this.birthdate = result.data.scrapeFreeones.birthdate ? result.data.scrapeFreeones.birthdate : this.birthdate;
this.ethnicity = result.data.scrapeFreeones.ethnicity;
this.eye_color = result.data.scrapeFreeones.eye_color;
this.height = result.data.scrapeFreeones.height;
this.measurements = result.data.scrapeFreeones.measurements;
this.fake_tits = result.data.scrapeFreeones.fake_tits;
this.career_length = result.data.scrapeFreeones.career_length;
this.tattoos = result.data.scrapeFreeones.tattoos;
this.piercings = result.data.scrapeFreeones.piercings;
this.twitter = result.data.scrapeFreeones.twitter;
this.instagram = result.data.scrapeFreeones.instagram;
}
}

View File

@@ -1,7 +0,0 @@
<div class="ui text menu">
<div class="right menu">
<button (click)="onClickNew()" class="ui button">New</button>
</div>
</div>
<app-list [state]="state"></app-list>

View File

@@ -1,23 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PerformersService } from '../performers.service';
@Component({
selector: 'app-performer-list',
templateUrl: './performer-list.component.html'
})
export class PerformerListComponent implements OnInit {
state = this.performersService.performerListState;
constructor(private performersService: PerformersService,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit() {}
onClickNew() {
this.router.navigate(['new'], { relativeTo: this.route });
}
}

View File

@@ -1,29 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PerformersComponent } from './performers/performers.component';
import { PerformerListComponent } from './performer-list/performer-list.component';
import { PerformerDetailComponent } from './performer-detail/performer-detail.component';
import { PerformerFormComponent } from './performer-form/performer-form.component';
const performersRoutes: Routes = [
{ path: '',
component: PerformersComponent,
children: [
{ path: '', component: PerformerListComponent },
{ path: 'new', component: PerformerFormComponent },
{ path: ':id', component: PerformerDetailComponent },
{ path: ':id/edit', component: PerformerFormComponent }
]
}
];
@NgModule({
imports: [
RouterModule.forChild(performersRoutes)
],
exports: [
RouterModule
]
})
export class PerformersRoutingModule {}

View File

@@ -1,29 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { ReactiveFormsModule } from '@angular/forms';
import { PerformersRoutingModule } from './performers-routing.module';
import { PerformersService } from './performers.service';
import { PerformersComponent } from './performers/performers.component';
import { PerformerListComponent } from './performer-list/performer-list.component';
import { PerformerDetailComponent } from './performer-detail/performer-detail.component';
import { PerformerFormComponent } from './performer-form/performer-form.component';
@NgModule({
imports: [
ReactiveFormsModule,
SharedModule,
PerformersRoutingModule
],
declarations: [
PerformersComponent,
PerformerListComponent,
PerformerDetailComponent,
PerformerFormComponent
],
providers: [
PerformersService
]
})
export class PerformersModule {}

View File

@@ -1,11 +0,0 @@
import { Injectable } from '@angular/core';
import { PerformerListState, SceneListState } from '../shared/models/list-state.model';
@Injectable()
export class PerformersService {
performerListState: PerformerListState = new PerformerListState();
detailsSceneListState: SceneListState = new SceneListState();
constructor() {}
}

View File

@@ -1,13 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-performers',
template: '<router-outlet></router-outlet>'
})
export class PerformersComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1,16 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ScenesService } from '../scenes.service';
@Component({
selector: 'app-marker-list',
template: '<app-list [state]="state"></app-list>'
})
export class MarkerListComponent implements OnInit {
state = this.scenesService.sceneMarkerListState;
constructor(private scenesService: ScenesService) {}
ngOnInit() {}
}

View File

@@ -1,87 +0,0 @@
<button style="margin: 5px;" (click)="onClickAddMarker()" class="ui button">Create Marker</button>
<div [suiCollapse]="!showingMarkerModal">
<div class="ui inverted segment">
<h4 class="ui header">{{!!editingMarker ? 'Edit' : 'Create'}} Marker</h4>
<div class="ui inverted form">
<div class="field">
<label>Title</label>
<div class="ui search focus">
<input [formControl]="searchFormControl" (blur)="setHasFocus(false)" (focus)="setHasFocus(true)" type="text" placeholder="Title" />
<div *ngIf="filteredMarkerOptions.length > 0 && hasFocus" class="results transition visible">
<a *ngFor="let title of filteredMarkerOptions" (click)="onClickMarkerTitle(title)" class="result">
<div class="content">
<div class="title">{{title}}</div>
</div>
</a>
</div>
</div>
</div>
<div class="equal width fields">
<div class="field">
<label>Seconds</label>
<input [(ngModel)]="seconds" type="number" placeholder="Title" />
</div>
<div class="field">
<label>Primary Tag</label>
<sui-select
class="selection"
[(ngModel)]="primary_tag_id"
[options]="tags"
labelField="name"
valueField="id"
[isSearchable]="true"
placeholder="Primary Tag"
#primaryTagSelect>
<sui-select-option *ngFor="let option of primaryTagSelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
</div>
<div class="field">
<label>Additional Tags</label>
<sui-multi-select
class="selection"
[(ngModel)]="tag_ids"
[options]="tags"
labelField="name"
valueField="id"
[isSearchable]="true"
placeholder="Tags"
#tagSelect>
<sui-select-option *ngFor="let option of tagSelect.availableOptions" [value]="option"></sui-select-option>
</sui-multi-select>
</div>
<button (click)="onSubmit()" class="ui primary submit button">Submit</button>
<button (click)="onCancel()" class="ui button">Cancel</button>
<button *ngIf="!!editingMarker" (click)="onClickDelete()" class="ui right floated negative button">Delete (Click {{3 - deleteClickCount}} times)</button>
</div>
</div>
</div>
<div *ngIf="!!scene" class="ui container" style="overflow-y: hidden; overflow-x: auto; white-space: nowrap;">
<div *ngFor="let primary_tag of scene.scene_marker_tags" class="ui dark card" style="max-height: 300px; height: 100vh; overflow-y: auto; overflow-x: hidden; display: inline-block; margin: 5px;">
<div class="content" style="white-space: normal;">
<div class="header">{{primary_tag.tag.name}}</div>
<div class="ui divided items">
<div *ngFor="let marker of primary_tag.scene_markers" class="item" style="padding: 0.5em 0;">
<div class="content">
<div class="header" style="font-size: 1em;">
<a (click)="onClickMarker(marker)">{{marker.title}}</a>
</div>
<i (click)="onClickEditMarker(marker)" class="ui right floated link icon edit"></i>
<div class="meta">
<span>{{marker.seconds | seconds}}</span>
</div>
<div class="extra">
<div class="ui tiny labels">
<div *ngFor="let tag of marker.tags" class="ui label">
{{tag.name}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,162 +0,0 @@
import { Component, OnInit, OnChanges, SimpleChanges, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import { StashService } from '../../core/stash.service';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { MarkerStrings, SceneMarkerData, SceneData, AllTagsForFilter } from '../../core/graphql-generated';
@Component({
selector: 'app-scene-detail-marker-manager',
templateUrl: './scene-detail-marker-manager.component.html',
styleUrls: ['./scene-detail-marker-manager.component.css']
})
export class SceneDetailMarkerManagerComponent implements OnInit, OnChanges {
@Input() scene: SceneData.Fragment;
@Input() player: any;
showingMarkerModal = false;
markerOptions: MarkerStrings.Query['markerStrings'];
filteredMarkerOptions: string[] = [];
hasFocus = false;
editingMarker: SceneMarkerData.Fragment;
deleteClickCount = 0;
searchFormControl = new FormControl();
// Form input
title: string;
seconds: number;
primary_tag_id: string;
tag_ids: string[] = [];
// From the network
tags: AllTagsForFilter.AllTags[];
constructor(private stashService: StashService) {}
ngOnInit() {
this.stashService.allTagsForFilter().valueChanges.subscribe(result => {
this.tags = result.data.allTags;
});
this.stashService.markerStrings().valueChanges.subscribe(result => {
this.markerOptions = result.data.markerStrings;
});
this.searchFormControl.valueChanges.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(term => {
this.filteredMarkerOptions = this.markerOptions.filter(value => {
return value.title.toLowerCase().includes(term.toLowerCase());
}).map(value => {
return value.title;
}).slice(0, 15);
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['scene']) {
}
}
onSubmit() {
this.title = this.searchFormControl.value;
const input = {
id: null,
title: this.title,
seconds: this.seconds,
scene_id: this.scene.id,
primary_tag_id: this.primary_tag_id,
tag_ids: this.tag_ids
};
if (this.editingMarker == null) {
this.stashService.markerCreate(input).subscribe(response => {
console.log(response);
this.hideModal();
}, error => {
console.log(error);
});
} else {
input.id = this.editingMarker.id;
this.stashService.markerUpdate(input).subscribe(response => {
console.log(response);
this.hideModal();
}, error => {
console.log(error);
});
}
}
onCancel() {
this.hideModal();
}
onClickDelete() {
this.deleteClickCount += 1;
if (this.deleteClickCount > 2) {
this.stashService.markerDestroy(this.editingMarker.id, this.scene.id).subscribe(response => {
console.log('Delete successfull:', response);
this.hideModal();
});
}
}
onClickAddMarker() {
this.player.pause();
this.showModal();
}
onClickMarker(marker: SceneMarkerData.Fragment) {
this.player.seek(marker.seconds);
}
onClickEditMarker(marker: SceneMarkerData.Fragment) {
this.showModal(marker);
}
onClickMarkerTitle(title: string) {
this.setTitle(title);
}
setHasFocus(hasFocus: boolean) {
if (hasFocus === false) {
setTimeout(() => { this.hasFocus = false; }, 400);
} else {
this.hasFocus = hasFocus;
}
}
private hideModal() {
this.showingMarkerModal = false;
this.editingMarker = null;
}
private showModal(marker: SceneMarkerData.Fragment = null) {
this.deleteClickCount = 0;
this.showingMarkerModal = true;
this.setTitle('');
this.primary_tag_id = null;
this.tag_ids = [];
this.seconds = Math.round(this.player.getPosition());
if (marker == null) { return; }
this.editingMarker = marker;
this.setTitle(marker.title);
this.seconds = marker.seconds;
this.primary_tag_id = marker.primary_tag.id;
this.tag_ids = marker.tags.map(value => value.id);
}
private setTitle(title: string) {
this.title = title;
this.searchFormControl.setValue(title);
}
}

View File

@@ -1,128 +0,0 @@
.scrubber-wrapper {
position: relative;
overflow: hidden;
margin: 5px 0;
}
#scrubber-back {
float: left;
}
#scrubber-forward {
float: right;
}
.scrubber-button {
width: 1.5%;
height: 100%;
line-height: 120px;
padding: 0;
text-align: center;
border: 1px solid #555;
font-weight: 800;
font-size: 20px;
color: #FFF;
cursor: pointer;
}
.scrubber-content {
-webkit-user-select: none;
-webkit-overflow-scrolling: touch;
cursor: -webkit-grab;
height: 120px;
width: 96%;
margin: 0 0.5%;
display: inline-block;
position: relative;
overflow: hidden;
}
.scrubber-content.dragging {
cursor: -webkit-grabbing;
}
.scrubber-tags-background {
background-color: #555;
position: absolute;
left: 0;
right: 0;
height: 20px;
}
#scrubber-position-indicator {
background-color: #CCC;
width: 100%;
left: -100%;
height: 20px;
z-index: 0;
position: absolute;
}
#scrubber-current-position {
background-color: #FFF;
width: 2px;
height: 30px;
left: 50%;
z-index: 100;
position: absolute;
}
.scrubber-viewport {
position: static;
height: 100%;
overflow: hidden;
}
.scrubber-slider {
position: absolute;
width: 100%;
height: 100%;
left: 0;
transition: 333ms ease-out;
}
.scrubber-tags {
height: 20px;
position: relative;
margin-bottom: 10px;
}
.scrubber-tag {
position: absolute;
background-color: #000;
font-size: 10px;
white-space: nowrap;
padding: 0 10px;
cursor: pointer;
}
.scrubber-tag:hover {
z-index: 1;
background-color: #444;
}
.scrubber-tag:after {
content: "";
position: absolute;
bottom: -5px;
left: 50%;
margin-left: -5px;
border-top: solid 5px #000;
border-left: solid 5px transparent;
border-right: solid 5px transparent;
}
.scrubber-item {
position: absolute;
display: flex;
margin-right: 10px;
cursor: pointer;
color: white;
text-shadow: 1px 1px black;
text-align: center;
font-size: 10px;
}
.scrubber-item span {
display: inline-block;
align-self: flex-end;
width: 100%;
}

View File

@@ -1,30 +0,0 @@
<div class="scrubber-wrapper">
<a class="scrubber-button" id="scrubber-back" (click)="goBack()"><</a>
<div class="scrubber-content">
<div class="scrubber-tags-background"></div>
<div #positionIndicator id="scrubber-position-indicator"></div>
<div id="scrubber-current-position"></div>
<div class="scrubber-viewport">
<div #scrubberSlider class="scrubber-slider">
<div class="scrubber-tags">
<div
*ngFor="let marker of scene?.scene_markers; let i = index"
#tag
class="scrubber-tag"
[attr.data-marker-id]="i"
[ngStyle]="getTagStyle(tag, i)">
{{marker.title}}
</div>
</div>
<div
*ngFor="let spriteItem of spriteItems; let i = index"
class="scrubber-item"
[attr.data-sprite-item-id]="i"
[ngStyle]="getStyleForSprite(i)">
<span>{{spriteItem.start | seconds}} - {{spriteItem.end | seconds}}</span>
</div>
</div>
</div>
</div>
<a class="scrubber-button" id="scrubber-forward" (click)="goForward()">></a>
</div>

View File

@@ -1,232 +0,0 @@
import {
Component,
OnInit,
OnChanges,
SimpleChanges,
Input,
Output,
HostListener,
ViewChild,
EventEmitter
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SceneData } from '../../core/graphql-generated';
class SceneSpriteItem {
start: number;
end: number;
x: number;
y: number;
w: number;
h: number;
}
@Component({
selector: 'app-scene-detail-scrubber',
templateUrl: './scene-detail-scrubber.component.html',
styleUrls: ['./scene-detail-scrubber.component.css']
})
export class SceneDetailScrubberComponent implements OnInit, OnChanges {
@Input() scene: SceneData.Fragment;
@Output() seek: EventEmitter<number> = new EventEmitter();
@Output() scrolled: EventEmitter<any> = new EventEmitter();
slider: HTMLElement;
@ViewChild('scrubberSlider') sliderTag: any;
indicator: HTMLElement;
@ViewChild('positionIndicator') indicatorTag: any;
spriteItems: SceneSpriteItem[] = [];
private mouseDown = false;
private last: MouseEvent;
private start: MouseEvent;
private velocity = 0;
private _position = 0;
getPostion(): number { return this._position; }
setPosition(newPostion: number, shouldEmit: boolean = true) {
if (shouldEmit) { this.scrolled.emit(); }
const midpointOffset = this.slider.clientWidth / 2;
const bounds = this.getBounds() * -1;
if (newPostion > midpointOffset) {
this._position = midpointOffset;
} else if (newPostion < bounds - midpointOffset) {
this._position = bounds - midpointOffset;
} else {
this._position = newPostion;
}
this.slider.style.transform = `translateX(${this._position}px)`;
const indicatorPosition = ((newPostion - midpointOffset) / (bounds - (midpointOffset * 2)) * this.slider.clientWidth);
this.indicator.style.transform = `translateX(${indicatorPosition}px)`;
}
@HostListener('window:mouseup', ['$event'])
onMouseup(event: MouseEvent) {
if (!this.start) { return; }
this.mouseDown = false;
const delta = Math.abs(event.clientX - this.start.clientX);
if (delta < 1 && event.target instanceof HTMLDivElement) {
const target: HTMLDivElement = event.target;
let seekSeconds: number = null;
const spriteIdString = target.getAttribute('data-sprite-item-id');
if (spriteIdString != null) {
const spritePercentage = event.offsetX / target.clientWidth;
const offset = target.offsetLeft + (target.clientWidth * spritePercentage);
const percentage = offset / this.slider.scrollWidth;
seekSeconds = percentage * this.scene.file.duration;
}
const markerIdString = target.getAttribute('data-marker-id');
if (markerIdString != null) {
const marker = this.scene.scene_markers[Number(markerIdString)];
seekSeconds = marker.seconds;
}
if (!!seekSeconds) { this.seek.emit(seekSeconds); }
} else if (Math.abs(this.velocity) > 25) {
const newPosition = this.getPostion() + (this.velocity * 10);
this.setPosition(newPosition);
this.velocity = 0;
}
}
@HostListener('mousedown', ['$event'])
onMousedown(event) {
event.preventDefault();
this.mouseDown = true;
this.last = event;
this.start = event;
this.velocity = 0;
}
@HostListener('mousemove', ['$event'])
onMousemove(event: MouseEvent) {
if (!this.mouseDown) { return; }
// negative dragging right (past), positive left (future)
const delta = event.clientX - this.last.clientX;
const movement = event.movementX;
this.velocity = movement;
const newPostion = this.getPostion() + delta;
this.setPosition(newPostion);
this.last = event;
}
constructor(private http: HttpClient) {}
ngOnInit() {
this.slider = this.sliderTag.nativeElement;
this.indicator = this.indicatorTag.nativeElement;
this.slider.style.transform = `translateX(${this.slider.clientWidth / 2}px)`;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['scene']) {
this.fetchSpriteInfo();
}
}
fetchSpriteInfo() {
if (!this.scene) { return; }
this.http.get(this.scene.paths.vtt, {responseType: 'text'}).subscribe(res => {
// TODO: This is gnarly
const lines = res.split('\n');
if (lines.shift() !== 'WEBVTT') { return; }
if (lines.shift() !== '') { return; }
let item = new SceneSpriteItem();
this.spriteItems = [];
while (lines.length) {
const line = lines.shift();
if (line.includes('#') && line.includes('=') && line.includes(',')) {
const size = line.split('#')[1].split('=')[1].split(',');
item.x = Number(size[0]);
item.y = Number(size[1]);
item.w = Number(size[2]);
item.h = Number(size[3]);
this.spriteItems.push(item);
item = new SceneSpriteItem();
} else if (line.includes(' --> ')) {
const times = line.split(' --> ');
const start = times[0].split(':');
item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);
const end = times[1].split(':');
item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
}
}
}, error => {
console.log(error);
});
}
getBounds(): number {
return this.slider.scrollWidth - this.slider.clientWidth;
}
getStyleForSprite(i) {
const sprite = this.spriteItems[i];
const left = sprite.w * i;
const path = this.scene.paths.vtt.replace('_thumbs.vtt', '_sprite.jpg'); // TODO: Gnarly
return {
'width.px': sprite.w,
'height.px': sprite.h,
'margin': '0px auto',
'background-position': -sprite.x + 'px ' + -sprite.y + 'px',
'background-image': `url(${path})`,
'left.px': left
};
}
getTagStyle(tag: HTMLDivElement, i: number) {
if (!this.slider || this.spriteItems.length === 0 || this.getBounds() === 0) { return {}; }
const marker = this.scene.scene_markers[i];
const duration = Number(this.scene.file.duration);
const percentage = marker.seconds / duration;
// TODO: this doesn't seem necessary anymore. Double check.
// Need to offset from the left margin or the tags are slightly off.
// const offset = Number(window.getComputedStyle(this.slider.offsetParent).marginLeft.replace('px', ''));
const offset = 0;
const left = (this.slider.scrollWidth * percentage) - (tag.clientWidth / 2) + offset;
return {
'left.px': left,
'height.px': 20
};
}
goBack() {
const newPosition = this.getPostion() + this.slider.clientWidth;
this.setPosition(newPosition);
}
goForward() {
const newPosition = this.getPostion() - this.slider.clientWidth;
this.setPosition(newPosition);
}
public scrollTo(seconds: number) {
const duration = Number(this.scene.file.duration);
const percentage = seconds / duration;
const position = ((this.slider.scrollWidth * percentage) - (this.slider.clientWidth / 2)) * -1;
this.setPosition(position, false);
}
}

View File

@@ -1,96 +0,0 @@
<app-jwplayer
(seeked)="onSeeked()"
(time)="onTime($event)"
#jwplayer>
</app-jwplayer>
<div class="ui container segments">
<div class="ui inverted top attached segment">
<app-scene-detail-scrubber
#scrubber
[scene]="scene"
(seek)="scrubberSeek($event)"
(scrolled)="scrubberScrolled()">
</app-scene-detail-scrubber>
<app-scene-detail-marker-manager
#markerManager
[scene]="scene"
[player]="jwplayer.player">
</app-scene-detail-marker-manager>
</div>
<div class="ui inverted attached clearing segment">
<h1 class="ui inverted left floated marginless header">
{{scene?.title || 'No Title'}}
<div *ngIf="!!scene?.date" class="sub header">{{scene?.date | date:"MM/dd/yy"}}</div>
<div class="sub header">{{scene?.file.size | fileSize}}</div>
</h1>
<button (click)="onClickEdit()" class="ui right floated button">Edit</button>
<div *ngIf="!!scene">
<a *ngIf="!!scene.studio"
[routerLink]="['/studios', scene.studio.id]"
[style.background-image]="'url(' + scene.studio.image_path + ')'"
style="width: 100%; height: 100px; display: inline-block; background-size: contain; background-position: center; background-repeat: no-repeat; filter: drop-shadow( 5px 5px 4px #aaa );">
</a>
<span *ngIf="!scene.studio">No Studio</span>
</div>
</div>
<div *ngIf="!!scene?.details && scene?.details.length != 0" class="ui inverted attached segment">
<h3>Details</h3>
<p class="pre">{{scene.details}}</p>
</div>
<div *ngIf="scene?.performers.length > 0" class="ui inverted attached segment">
<h3>Performers</h3>
<div class="ui four centered stackable link cards">
<app-performer-card *ngFor="let performer of scene?.performers"
[performer]="performer"
[ageFromDate]="scene.date">
</app-performer-card>
</div>
</div>
<div *ngIf="scene?.tags.length > 0" class="ui inverted attached segment">
<h3>Tags</h3>
<div class="ui labels">
<a *ngFor="let tag of scene?.tags" class="ui label">{{tag.name}}</a>
</div>
</div>
<div *ngIf="!!scene?.gallery" class="ui inverted attached segment">
<h3 class="ui header">
Gallery
</h3>
<app-gallery-preview *ngIf="!!scene?.gallery" [gallery]="scene?.gallery"></app-gallery-preview>
</div>
<div class="ui inverted bottom attached segment">
<div class="ui inverted list">
<a class="clippable item" title="Click to copy" ngxClipboard [cbContent]="scene?.checksum">
<i class="privacy icon"></i>
<div class="content">
<div class="header">Checksum</div>
<div class="description">{{scene?.checksum}}</div>
</div>
</a>
<a class="clippable item" title="Click to copy" ngxClipboard [cbContent]="scene?.path">
<i class="folder open outline icon"></i>
<div class="content">
<div class="header">Path</div>
<div class="description">{{scene?.path}}</div>
</div>
</a>
<a *ngIf="!!scene?.url" class="clippable item" title="Click to copy" ngxClipboard [cbContent]="scene?.url">
<i class="server icon"></i>
<div class="content">
<div class="header">URL</div>
<div class="description">{{scene?.url}}</div>
</div>
</a>
</div>
</div>
</div>

View File

@@ -1,82 +0,0 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { SceneData } from '../../core/graphql-generated';
@Component({
selector: 'app-scene-detail',
templateUrl: './scene-detail.component.html',
styleUrls: ['./scene-detail.component.css']
})
export class SceneDetailComponent implements OnInit {
scene: SceneData.Fragment;
private lastTime = 0;
private isPlayerSetup = false;
@ViewChild('jwplayer') jwplayer: any;
@ViewChild('scrubber') scrubber: any;
constructor(private route: ActivatedRoute, private stashService: StashService, private router: Router) { }
ngOnInit() {
this.getScene();
window.scrollTo(0, 0);
}
getScene() {
const id = parseInt(this.route.snapshot.params['id'], 10);
this.stashService.findScene(id).valueChanges.subscribe(result => {
this.scene = Object.assign({scene_marker_tags: result.data.sceneMarkerTags}, result.data.findScene);
// TODO: Check this, this didn't matter before...
if (!this.isPlayerSetup) {
const streamPath = this.scene.paths.stream;
const screenshotPath = this.scene.paths.screenshot;
const vttPath = this.scene.paths.vtt;
const chaptersVttPath = this.scene.paths.chapters_vtt;
this.jwplayer.setupPlayer(streamPath, screenshotPath, vttPath, chaptersVttPath);
this.isPlayerSetup = true;
this.route.queryParams.subscribe(params => {
if (params['t'] != null) {
this.jwplayer.player.seek(params['t']);
}
});
}
}, error => {
console.log(error);
});
}
onClickEdit() {
this.router.navigate(['edit'], { relativeTo: this.route });
}
onSeeked() {
const position = this.jwplayer.player.getPosition();
this.scrubber.scrollTo(position);
this.jwplayer.player.play();
}
onTime(data) {
const position = this.jwplayer.player.getPosition();
const difference = Math.abs(position - this.lastTime);
if (difference > 1) {
this.lastTime = position;
this.scrubber.scrollTo(position);
}
}
scrubberSeek(seconds) {
this.jwplayer.player.seek(seconds);
}
scrubberScrolled() {
this.jwplayer.player.pause();
}
}

View File

@@ -1,98 +0,0 @@
<div [class.loading]="loading" class="ui inverted form">
<h4 class="ui inverted dividing header">Scene Information</h4>
<div class="field">
<div class="equal width fields">
<div class="field">
<label>Title</label>
<input [(ngModel)]="title" type="text" placeholder="Title" />
</div>
<div class="field">
<label>URL</label>
<input [(ngModel)]="url" type="url" placeholder="URL" />
</div>
<div class="field">
<label>Date (YYYY-MM-DD)</label>
<input [(ngModel)]="date" type="text" placeholder="Date (YYYY-MM-DD)" />
</div>
<div class="field">
<label>Rating</label>
<sui-rating class="ui massive rating" [(ngModel)]="rating" [maximum]="5"></sui-rating>
</div>
</div>
<div class="field">
<label>Gallery</label>
<sui-select
class="selection"
[(ngModel)]="gallery_id"
[options]="galleries"
labelField="path"
valueField="id"
[isSearchable]="true"
placeholder="Gallery"
#gallerySelect>
<sui-select-option [value]="{id: 0, path: 'None'}"></sui-select-option>
<sui-select-option *ngFor="let option of gallerySelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
<div class="field">
<label>Studio</label>
<sui-select
class="selection"
[(ngModel)]="studio_id"
[options]="studios"
labelField="name"
valueField="id"
[isSearchable]="true"
placeholder="Studio"
#studioSelect>
<sui-select-option *ngFor="let option of studioSelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
<ng-template let-option #performerOptionTemplate>
<img [lazyLoad]="option.image_path" height="80"/>&nbsp;{{ option.name }}
</ng-template>
<div class="field">
<label>Performers</label>
<sui-multi-select
class="selection"
[(ngModel)]="performer_ids"
[options]="performers"
labelField="name"
valueField="id"
[isSearchable]="true"
[optionTemplate]="performerOptionTemplate"
placeholder="Performers"
#performerSelect>
<sui-select-option *ngFor="let option of performerSelect.availableOptions" [value]="option"></sui-select-option>
</sui-multi-select>
</div>
<div class="field">
<label>Tags</label>
<div class="fields">
<div class="fourteen wide field">
<sui-multi-select
class="selection"
[(ngModel)]="tag_ids"
[options]="tags"
labelField="name"
valueField="id"
[isSearchable]="true"
placeholder="Tags"
#tagSelect>
<sui-select-option *ngFor="let option of tagSelect.availableOptions" [value]="option"></sui-select-option>
</sui-multi-select>
</div>
<div class="two wide field">
<%= link_to 'Add Tag', new_tag_path, class: 'ui fluid button' %>
</div>
</div>
</div>
<div class="field">
<label>Details</label>
<textarea [(ngModel)]="details" placeholder="Details"></textarea>
</div>
</div>
<button (click)="onSubmit()" class="ui primary submit button">Submit</button>
</div>

View File

@@ -1,87 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { FindSceneForEditing } from '../../core/graphql-generated';
@Component({
selector: 'app-scene-form',
templateUrl: './scene-form.component.html',
styleUrls: ['./scene-form.component.css']
})
export class SceneFormComponent implements OnInit {
loading = true;
title: string;
details: string;
url: string;
date: string;
rating: number;
gallery_id: string;
studio_id: string;
performer_ids: string[] = [];
tag_ids: string[] = [];
performers: FindSceneForEditing.Query['allPerformers'];
tags: FindSceneForEditing.Query['allTags'];
studios: FindSceneForEditing.Query['allStudios'];
galleries: FindSceneForEditing.Query['validGalleriesForScene'];
constructor(
private route: ActivatedRoute,
private stashService: StashService,
private router: Router
) {}
ngOnInit() {
this.getScene();
}
getScene() {
const id = parseInt(this.route.snapshot.params['id'], 10);
if (!!id === false) {
console.log('new scene');
return;
}
this.stashService.findSceneForEditing(id).valueChanges.subscribe(result => {
this.title = result.data.findScene.title;
this.details = result.data.findScene.details;
this.url = result.data.findScene.url;
this.date = result.data.findScene.date;
this.rating = result.data.findScene.rating;
this.gallery_id = !!result.data.findScene.gallery ? result.data.findScene.gallery.id : null;
this.studio_id = !!result.data.findScene.studio ? result.data.findScene.studio.id : null;
this.performer_ids = result.data.findScene.performers.map(performer => performer.id);
this.tag_ids = result.data.findScene.tags.map(tag => tag.id);
this.performers = result.data.allPerformers;
this.tags = result.data.allTags;
this.studios = result.data.allStudios;
this.galleries = result.data.validGalleriesForScene;
this.loading = result.loading;
});
}
onSubmit() {
const id = this.route.snapshot.params['id'];
this.stashService.sceneUpdate({
id: id,
title: this.title,
details: this.details,
url: this.url,
date: this.date,
rating: this.rating,
studio_id: this.studio_id,
gallery_id: this.gallery_id,
performer_ids: this.performer_ids,
tag_ids: this.tag_ids
}).subscribe(result => {
this.router.navigate(['/scenes', id]);
});
}
}

View File

@@ -1,16 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ScenesService } from '../scenes.service';
@Component({
selector: 'app-scene-list',
template: '<app-list [state]="state"></app-list>'
})
export class SceneListComponent implements OnInit {
state = this.scenesService.sceneListState;
constructor(private scenesService: ScenesService) {}
ngOnInit() {}
}

View File

@@ -1,37 +0,0 @@
<div class="simple-modal-container" [style.display]="showingMarkerList ? 'block' : 'none'">
<div class="simple-modal-content">
<table class="ui very basic celled table">
<thead>
<tr>
<th (click)="sortMarkers('title')">Title</th>
<th (click)="sortMarkers('count')">Scene Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let marker of markerOptions">
<td><a (click)="onClickMarker(marker)">{{marker.title}}</a></td>
<td>{{marker.count}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="wall grid">
<form id="scene-filter" class="ui inverted smassive form">
<div class="ui inverted massive fluid transparent icon input">
<input [formControl]="searchFormControl" name="q" placeholder="Search..." type="text">
<i class="search icon"></i>
<button (click)="refresh()" class="ui button" style="margin: 5px 5px;">Refresh</button>
<button *ngIf="mode == WallMode.Markers" (click)="toggleMarkerList()" class="ui button" style="margin: 5px 5px;">List Markers</button>
<button (click)="toggleMode()" class="ui button" style="margin: 5px 50px 5px 5px;">{{mode == WallMode.Scenes ? 'Scenes' : 'Markers'}}</button>
</div>
</form>
<div class="ui five column grid" style="margin: 0;">
<div *ngFor="let item of items" class="wall column">
<app-scene-wall-item *ngIf="mode == WallMode.Markers" [marker]="item"></app-scene-wall-item>
<app-scene-wall-item *ngIf="mode == WallMode.Scenes" [scene]="item"></app-scene-wall-item>
</div>
</div>
</div>

View File

@@ -1,84 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { StashService } from '../../core/stash.service';
export enum WallMode {
Scenes,
Markers
}
@Component({
selector: 'app-scene-wall',
templateUrl: './scene-wall.component.html',
styleUrls: ['./scene-wall.component.css']
})
export class SceneWallComponent implements OnInit {
WallMode = WallMode;
items: any[]; // scenes or scene markers
markerOptions: any[];
showingMarkerList = false;
searchTerm = '';
searchFormControl = new FormControl();
mode: WallMode = WallMode.Markers;
constructor(
private stashService: StashService
) {}
ngOnInit() {
this.searchFormControl.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged()
).subscribe(term => {
this.getScenes(term);
});
this.stashService.markerStrings().valueChanges.subscribe(result => {
this.markerOptions = result.data.markerStrings;
});
this.searchFormControl.setValue(this.searchTerm);
}
async getScenes(q: string) {
this.items = null;
this.searchTerm = q;
if (this.mode === WallMode.Scenes) {
const response = await this.stashService.sceneWall(q).result();
this.items = response.data.sceneWall;
} else {
const response = await this.stashService.markerWall(q).result();
this.items = response.data.markerWall;
}
}
toggleMode() {
if (this.mode === WallMode.Scenes) {
this.mode = WallMode.Markers;
} else {
this.mode = WallMode.Scenes;
}
this.getScenes(this.searchTerm);
}
toggleMarkerList() {
this.showingMarkerList = !this.showingMarkerList;
}
refresh() {
this.getScenes(this.searchTerm);
}
onClickMarker(marker) {
this.searchTerm = `${marker.title}`;
this.searchFormControl.setValue(this.searchTerm);
this.showingMarkerList = false;
}
async sortMarkers(by) {
const result = await this.stashService.markerStrings(null, by).result();
this.markerOptions = result.data.markerStrings;
}
}

View File

@@ -1,32 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ScenesComponent } from './scenes/scenes.component';
import { SceneListComponent } from './scene-list/scene-list.component';
import { SceneDetailComponent } from './scene-detail/scene-detail.component';
import { SceneFormComponent } from './scene-form/scene-form.component';
import { SceneWallComponent } from './scene-wall/scene-wall.component';
import { MarkerListComponent } from './marker-list/marker-list.component';
const scenesRoutes: Routes = [
{ path: 'wall', component: SceneWallComponent },
{ path: 'markers', component: MarkerListComponent },
{ path: '',
component: ScenesComponent,
children: [
{ path: '', component: SceneListComponent },
{ path: ':id', component: SceneDetailComponent },
{ path: ':id/edit', component: SceneFormComponent }
]
}
];
@NgModule({
imports: [
RouterModule.forChild(scenesRoutes)
],
exports: [
RouterModule
]
})
export class ScenesRoutingModule {}

View File

@@ -1,35 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { ScenesRoutingModule } from './scenes-routing.module';
import { ScenesService } from './scenes.service';
import { ScenesComponent } from './scenes/scenes.component';
import { SceneListComponent } from './scene-list/scene-list.component';
import { SceneDetailComponent } from './scene-detail/scene-detail.component';
import { SceneFormComponent } from './scene-form/scene-form.component';
import { SceneWallComponent } from './scene-wall/scene-wall.component';
import { SceneDetailScrubberComponent } from './scene-detail-scrubber/scene-detail-scrubber.component';
import { SceneDetailMarkerManagerComponent } from './scene-detail-marker-manager/scene-detail-marker-manager.component';
import { MarkerListComponent } from './marker-list/marker-list.component';
@NgModule({
imports: [
SharedModule,
ScenesRoutingModule
],
declarations: [
ScenesComponent,
SceneListComponent,
SceneDetailComponent,
SceneFormComponent,
SceneWallComponent,
SceneDetailScrubberComponent,
SceneDetailMarkerManagerComponent,
MarkerListComponent
],
providers: [
ScenesService
]
})
export class ScenesModule {}

View File

@@ -1,11 +0,0 @@
import { Injectable } from '@angular/core';
import { SceneListState, SceneMarkerListState } from '../shared/models/list-state.model';
@Injectable()
export class ScenesService {
sceneListState: SceneListState = new SceneListState();
sceneMarkerListState: SceneMarkerListState = new SceneMarkerListState();
constructor() {}
}

View File

@@ -1,13 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-scenes',
template: '<router-outlet></router-outlet>'
})
export class ScenesComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SettingsComponent } from './settings/settings.component';
const routes: Routes = [
{ path: '',
component: SettingsComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SettingsRoutingModule {}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { SettingsRoutingModule } from './settings-routing.module';
import { SettingsComponent } from './settings/settings.component';
@NgModule({
imports: [
SharedModule,
SettingsRoutingModule
],
declarations: [
SettingsComponent
]
})
export class SettingsModule {}

View File

@@ -1,20 +0,0 @@
<div class="ui text menu">
<div class="left menu">
<button (click)="onClickImport()" class="ui button">Import (Click {{3 - importClickCount}} times)</button>
<button (click)="onClickExport()" class="ui button">Export</button>
</div>
<div class="right menu">
<button (click)="onClickScan()" class="ui button">Scan</button>
<button (click)="onClickGenerate()" class="ui button">Generate</button>
<button (click)="onClickClean()" class="ui button">Clean</button>
</div>
</div>
<sui-progress class="indicating" [value]="progress">{{message}}</sui-progress>
<div class="ui divider"></div>
<div class="ui list" style="color: white;">
<div *ngFor="let log of logs" class="item">
<strong>{{log.type}}</strong> - {{log.message}}
</div>
</div>

View File

@@ -1,58 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { StashService } from '../../core/stash.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html'
})
export class SettingsComponent implements OnInit, OnDestroy {
progress: number;
message: string;
logs: string[];
statusObservable: Subscription;
importClickCount = 0;
constructor(private stashService: StashService) {}
ngOnInit() {
this.statusObservable = this.stashService.metadataUpdate().subscribe(response => {
const result = JSON.parse(response.data.metadataUpdate);
this.progress = result.progress;
this.message = result.message;
this.logs = result.logs;
});
}
ngOnDestroy() {
if (!this.statusObservable) { return; }
this.statusObservable.unsubscribe();
}
onClickImport() {
this.importClickCount += 1;
if (this.importClickCount > 2) {
this.stashService.metadataImport().refetch();
this.importClickCount = 0;
}
}
onClickExport() {
this.stashService.metadataExport().refetch();
}
onClickScan() {
this.stashService.metadataScan().refetch();
}
onClickGenerate() {
this.stashService.metadataGenerate().refetch();
}
onClickClean() {
this.stashService.metadataClean().refetch();
}
}

View File

@@ -1,23 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'age'
})
export class AgePipe implements PipeTransform {
transform(value: string, ageFromDate?: string): number {
if (!!value === false) { return 0; }
const birthdate = new Date(value);
const fromDate = !!ageFromDate ? new Date(ageFromDate) : new Date();
let age = fromDate.getFullYear() - birthdate.getFullYear();
if (birthdate.getMonth() > fromDate.getMonth() ||
(birthdate.getMonth() >= fromDate.getMonth() && birthdate.getDay() > fromDate.getDay())) {
age -= 1;
}
return age;
}
}

View File

@@ -1,85 +0,0 @@
import { Component, OnInit, ViewChild, ElementRef, HostListener } from '@angular/core';
@Component({
selector: 'app-base-wall-item',
template: ''
})
export class BaseWallItemComponent implements OnInit {
private video: any;
private hoverTimeout: any = null;
isHovering = false;
title = '';
imagePath = '';
videoPath = '';
@ViewChild('videoTag')
set videoTag(videoTag: ElementRef) {
if (videoTag === undefined) { return; }
this.video = videoTag.nativeElement;
this.video.volume = 0.05;
this.video.loop = true;
this.video.oncanplay = () => {
this.video.play();
};
}
constructor() {}
ngOnInit() {}
@HostListener('mouseenter', ['$event'])
onMouseEnter(e) {
if (!!this.hoverTimeout) { return; }
const that = this;
this.hoverTimeout = setTimeout(function() {
that.configureTimeout(e);
}, 1000);
}
@HostListener('mouseleave')
onMouseLeave() {
if (!!this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = null;
}
if (this.video !== undefined) {
this.video.pause();
this.video.src = '';
}
this.isHovering = false;
}
@HostListener('mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
if (!!this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = null;
}
this.configureTimeout(event);
}
transitionEnd(event) {
if (event.target.classList.contains('double-scale')) {
event.target.style.zIndex = 2;
} else {
event.target.style.zIndex = null;
}
}
private configureTimeout(event: MouseEvent) {
const that = this;
this.hoverTimeout = setTimeout(function() {
if (event.target instanceof HTMLElement) {
const target: HTMLElement = event.target;
if (target.className === 'scene-wall-item-text-container' ||
target.offsetParent.className === 'scene-wall-item-text-container') {
that.configureTimeout(event);
return;
}
}
that.isHovering = true;
}, 1000);
}
}

View File

@@ -1,15 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {
transform(value: any, args?: any): any {
if (value) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
return value;
}
}

View File

@@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filename'
})
export class FileNamePipe implements PipeTransform {
transform(value: string, args?: any): string {
if (!!value === false) { return 'No File Name'; }
return value.replace(/^.*[\\\/]/, '');
}
}

View File

@@ -1,29 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize'
})
export class FileSizePipe implements PipeTransform {
private units = [
'bytes',
'kB',
'MB',
'GB',
'TB',
'PB'
];
transform(bytes: number = 0, precision: number = 2): string {
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) { return '?'; }
let unit = 0;
while ( bytes >= 1024 ) {
bytes /= 1024;
unit++;
}
return bytes.toFixed(+precision) + ' ' + this.units[ unit ];
}
}

View File

@@ -1,9 +0,0 @@
<app-gallery-preview
[gallery]="gallery"
[numberOfRandomImages]="4"
[showTitles]="false"
[numberPerRow]="2">
</app-gallery-preview>
<div class="content">
<div class="header">{{gallery.title || gallery.path}}</div>
</div>

View File

@@ -1,28 +0,0 @@
import { Component, OnInit, Input, HostBinding } from '@angular/core';
import { Router } from '@angular/router';
import { GalleryData } from '../../core/graphql-generated';
@Component({
selector: 'app-gallery-card',
templateUrl: './gallery-card.component.html',
styleUrls: ['./gallery-card.component.css']
})
export class GalleryCardComponent implements OnInit {
@Input() gallery: GalleryData.Fragment;
// The host class needs to be card
@HostBinding('class') class = 'card';
constructor(
private router: Router
) {}
ngOnInit() {
}
onSelect(): void {
this.router.navigate(['/galleries', this.gallery.id]);
}
}

View File

@@ -1,11 +0,0 @@
<div (click)="onClickGallery()" class="ui grid" style="margin: 0;">
<div
*ngFor="let image of files"
[lazyLoad]="imagePath(image)"
(click)="onClickImage(image)"
[style.min-height]="(numberOfRandomImages <= 4) ? '200px' : '400px'"
style="height: 15vh; background-size: cover; background-position: 50% 25%;"
class="{{suiWidthForNumberPerRow()}} wide column">
<h3 *ngIf="showTitles">{{image.name}}</h3>
</div>
</div>

View File

@@ -1,98 +0,0 @@
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { StashService } from '../../core/stash.service';
import { GalleryImage } from '../../shared/models/gallery.model';
import { GalleryData } from '../../core/graphql-generated';
@Component({
selector: 'app-gallery-preview',
templateUrl: './gallery-preview.component.html',
styleUrls: ['./gallery-preview.component.css']
})
export class GalleryPreviewComponent implements OnInit, OnChanges {
@Input() gallery: GalleryData.Fragment;
@Input() galleryId: number;
@Input() type = 'random';
@Input() numberOfRandomImages = 12;
@Input() showTitles = true;
@Input() numberPerRow = 4;
@Output() clicked: EventEmitter<GalleryImage> = new EventEmitter();
files: GalleryImage[];
constructor(
private router: Router,
private stashService: StashService
) {}
ngOnInit() {
if (!!this.galleryId) {
this.fetchGallery();
}
}
async fetchGallery() {
const result = await this.stashService.findGallery(this.galleryId).result();
this.gallery = result.data.findGallery;
this.setupFiles();
}
imagePath(image) {
return `${image.path}?thumb=true`;
}
shuffle(a) {
for (let i = a.length; i; i--) {
const j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
}
onClickGallery() {
if (this.type === 'random') {
this.router.navigate(['galleries', this.gallery.id]);
}
}
onClickImage(image) {
if (this.type === 'full') {
this.clicked.emit(image);
}
}
suiWidthForNumberPerRow(): string {
switch (this.numberPerRow) {
case 1: {
return 'sixteen';
}
case 2: {
return 'eight';
}
case 4: {
return 'four';
}
default:
return 'four';
}
}
setupFiles() {
if (!this.gallery) { return; }
this.files = [...this.gallery.files];
if (this.type === 'random') {
this.shuffle(this.files);
this.files = this.files.slice(0, this.numberOfRandomImages);
} else if (this.type === 'gallery') {
}
}
ngOnChanges(changes: any) {
if (!!changes.gallery) {
this.setupFiles();
}
}
}

View File

@@ -1,4 +0,0 @@
<div
(window:keydown)="onKey($event)"
class="jwplayer">
</div>

View File

@@ -1,139 +0,0 @@
import { Component, EventEmitter, Input, Output, ElementRef } from '@angular/core';
declare var jwplayer: any;
@Component({
selector: 'app-jwplayer',
templateUrl: './jwplayer.component.html',
styleUrls: ['./jwplayer.component.css']
})
export class JwplayerComponent {
@Input() public title: string;
@Input() public file: string;
@Input() public image: string;
@Input() public height: string;
@Input() public width: string;
@Output() public bufferChange: EventEmitter<any> = new EventEmitter();
@Output() public complete: EventEmitter<any> = new EventEmitter();
@Output() public buffer: EventEmitter<any> = new EventEmitter();
@Output() public error: EventEmitter<any> = new EventEmitter<any>();
@Output() public play: EventEmitter<any> = new EventEmitter<any>();
@Output() public start: EventEmitter<any> = new EventEmitter<any>();
@Output() public fullscreen: EventEmitter<any> = new EventEmitter<any>();
@Output() public seeked: EventEmitter<any> = new EventEmitter();
@Output() public time: EventEmitter<any> = new EventEmitter();
private _player: any = null;
constructor(private _elementRef: ElementRef) { }
public get player(): any {
this._player = this._player || jwplayer(this._elementRef.nativeElement);
return this._player;
}
public setupPlayer(file: string, image?: string, vtt?: string, chaptersVtt?: string) {
this.player.remove();
this.player.setup({
file: file,
image: image,
tracks: [
{
file: vtt,
kind: 'thumbnails'
},
{
file: chaptersVtt,
kind: 'chapters'
}
],
primary: 'html5',
autostart: false,
playbackRateControls: [0.75, 1, 1.5, 2, 3, 4]
});
this.handleEventsFor(this.player);
}
public handleEventsFor = (player: any) => {
player.on('bufferChange', this.onBufferChange);
player.on('buffer', this.onBuffer);
player.on('complete', this.onComplete);
player.on('error', this.onError);
player.on('fullscreen', this.onFullScreen);
player.on('play', this.onPlay);
player.on('start', this.onStart);
player.on('seeked', this.onSeeked);
player.on('time', this.onTime);
}
public onComplete = (options: {}) => this.complete.emit(options);
public onError = () => this.error.emit();
public onBufferChange = (options: {
duration: number,
bufferPercent: number,
position: number,
metadata?: number
}) => this.bufferChange.emit(options)
public onBuffer = (options: {
oldState: string,
newState: string,
reason: string
}) => this.buffer.emit()
public onStart = (options: {
oldState: string,
newState: string,
reason: string
}) => this.buffer.emit()
public onFullScreen = (options: {
oldState: string,
newState: string,
reason: string
}) => this.buffer.emit()
public onPlay = (options: {
}) => this.play.emit()
public onSeeked = (options: {
}) => this.seeked.emit()
public onTime = (options: {
duration: number,
position: number,
viewable: boolean
}) => this.time.emit(options)
onKey(event) {
const currentPlaybackRate = this._player.getPlaybackRate();
switch (event.key) {
case '0': {
this._player.setPlaybackRate(1);
console.log(`Playback rate: 1`);
event.preventDefault();
break;
}
case '2': {
this._player.setPlaybackRate(currentPlaybackRate + 0.5);
console.log(`Playback rate: ${currentPlaybackRate + 0.5}`);
event.preventDefault();
break;
}
case '1': {
this._player.setPlaybackRate(currentPlaybackRate - 0.5);
console.log(`Playback rate: ${currentPlaybackRate - 0.5}`);
event.preventDefault();
break;
}
default:
break;
}
}
}

View File

@@ -1,121 +0,0 @@
<div class="ui text menu">
<div class="ui massive category search item" style="width: 33vw;">
<div class="ui inverted transparent icon input" style="width: 100%; border-bottom: 1px rgba(0,0,0,0.1) solid;">
<input [formControl]="searchFormControl" (ngModelChange)="onChange($event)" placeholder="Search..." type="text">
<i class="search link icon"></i>
</div>
</div>
<div class="right menu">
<div class="ui item">
<sui-select class="ui fluid selection"
[(ngModel)]="filter.itemsPerPage"
[options]="itemsPerPageOptions"
(selectedOptionChange)="onPerPageChange($event)"
placeholder="Items per page"
#itemsPerPageSelect>
<sui-select-option *ngFor="let option of itemsPerPageSelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
<div class="ui item">
<div class="ui buttons">
<button
*ngIf="filter.sortBy !== 'random'"
(click)="onSortChange()"
class="ui icon button">
<i
[class.up]="filter.sortDirection == 'asc'"
[class.down]="filter.sortDirection == 'desc'"
class="arrow icon"></i>
</button>
<div class="ui dropdown button" suiDropdown>
<span class="text">{{filter.sortBy | capitalize}}</span>
<i class="dropdown icon"></i>
<div class="menu" suiDropdownMenu>
<div *ngFor="let item of filter.sortByOptions" class="item" (click)="onSortByChange(item)">{{item | capitalize}}</div>
</div>
</div>
</div>
</div>
<div class="ui item">
<button (click)="filter.criteriaFilterOpen = !filter.criteriaFilterOpen" class="ui icon button" [class.active]="filter.criteriaFilterOpen"><i class="filter icon"></i></button>
</div>
<div *ngIf="filter.displayModeOptions.length > 1" class="ui item">
<div class="ui icon buttons">
<button
*ngIf="filter.displayModeOptions.includes(DisplayMode.Grid)"
(click)="onModeChange(DisplayMode.Grid)"
class="ui button"
[class.active]="filter.displayMode == DisplayMode.Grid">
<i class="grid layout icon"></i>
</button>
<button
*ngIf="filter.displayModeOptions.includes(DisplayMode.List)"
(click)="onModeChange(DisplayMode.List)"
class="ui button"
[class.active]="filter.displayMode == DisplayMode.List">
<i class="list layout icon"></i>
</button>
<button
*ngIf="filter.displayModeOptions.includes(DisplayMode.Wall)"
(click)="onModeChange(DisplayMode.Wall)"
class="ui button"
[class.active]="filter.displayMode == DisplayMode.Wall">
<i class="square full icon"></i>
</button>
</div>
</div>
</div>
</div>
<div [suiCollapse]="!filter.criteriaFilterOpen">
<div class="ui basic segment">
<div class="ui form">
<div class="field">
<div class="ui button" (click)="onAddCriteria()">Add Criteria</div>
</div>
<div *ngFor="let criteria of filter.criterions" class="fields">
<div class="one wide field">
<div class="ui button" (click)="onDeleteCriteria(criteria)">X</div>
</div>
<div class="three wide field">
<sui-select
class="selection"
[(ngModel)]="criteria.type"
[options]="filter.criteriaOptions"
(selectedOptionChange)="onCriteriaTypeChange($event, criteria)"
labelField="name"
valueField="value"
placeholder="Criteria"
#criteriaSelect>
<sui-select-option *ngFor="let option of criteriaSelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
</div>
<div class="twelve wide field">
<sui-select
*ngIf="criteria.valueType == CriteriaValueType.Single"
class="selection"
[(ngModel)]="criteria.value"
[options]="criteria.options"
(selectedOptionChange)="onCriteriaValueChange($event)"
[isDisabled]="criteria?.type == CriteriaType.None"
placeholder="Criteria"
#criteriaValueSelect>
<sui-select-option *ngFor="let option of criteriaValueSelect.availableOptions" [value]="option"></sui-select-option>
</sui-select>
<sui-multi-select
*ngIf="criteria.valueType == CriteriaValueType.Multiple"
class="selection"
[(ngModel)]="criteria.values"
[options]="criteria.options"
(selectedOptionsChange)="onCriteriaValueChange($event)"
labelField="name"
valueField="id"
[isSearchable]="true"
placeholder="Criteria"
#criteriaValuesSelect>
<sui-select-option *ngFor="let option of criteriaValuesSelect.availableOptions" [value]="option"></sui-select-option>
</sui-multi-select>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,105 +0,0 @@
import { Component, OnInit, OnDestroy, Input, Output, ViewChildren, EventEmitter, ElementRef, QueryList } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { StashService } from '../../core/stash.service';
import { ListFilter, DisplayMode, Criteria, CriteriaType, CriteriaValueType } from '../models/list-state.model';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-list-filter',
templateUrl: './list-filter.component.html'
})
export class ListFilterComponent implements OnInit, OnDestroy {
DisplayMode = DisplayMode;
CriteriaType = CriteriaType;
CriteriaValueType = CriteriaValueType;
@Input() filter: ListFilter;
@Output() filterUpdated = new EventEmitter<ListFilter>();
@ViewChildren('criteriaValueSelect') criteriaValueSelectInputs: QueryList<ElementRef>;
@ViewChildren('criteriaValuesSelect') criteriaValuesSelectInputs: QueryList<ElementRef>;
itemsPerPageOptions = [20, 40, 60, 120];
searchFormControl = new FormControl();
constructor(private stashService: StashService, private route: ActivatedRoute) {}
ngOnInit() {
this.route.queryParams.subscribe(params => {
this.filter.configureFromQueryParameters(params, this.stashService);
if (params['q'] != null) {
this.searchFormControl.setValue(this.filter.searchTerm);
}
});
this.filter.configureForFilterMode(this.filter.filterMode);
this.searchFormControl.valueChanges
.pipe(
debounceTime(400),
distinctUntilChanged()
)
.subscribe(term => {
this.filter.searchTerm = term;
this.filterUpdated.emit(this.filter);
});
this.filterUpdated.emit(this.filter);
}
ngOnDestroy() {
// this.searchFormControl.valueChanges.unsubscribe();
}
onPerPageChange(perPage: number) {
this.filterUpdated.emit(this.filter);
}
onSortChange() {
this.filter.sortDirection = this.filter.sortDirection === 'asc' ? 'desc' : 'asc';
this.filterUpdated.emit(this.filter);
}
onSortByChange(sortBy: string) {
this.filter.sortBy = sortBy;
this.filterUpdated.emit(this.filter);
}
onAddCriteria() {
const criteria = new Criteria();
this.filter.criterions.push(criteria);
}
onDeleteCriteria(criteria: Criteria) {
const idx = this.filter.criterions.indexOf(criteria);
this.filter.criterions.splice(idx, 1);
this.filterUpdated.emit(this.filter);
}
onCriteriaTypeChange(criteriaType: CriteriaType, criteria: Criteria) {
// if (!!this.criteriaValueSelect) {
// this.criteriaValueSelect.selectedOption = null;
// }
// if (!!this.criteriaValuesSelect) {
// this.criteriaValuesSelect.selectedOptions = null;
// }
criteria.configure(criteriaType, this.stashService);
this.filterUpdated.emit(this.filter);
}
onCriteriaValueChange(criteriaValue: string) {
this.filterUpdated.emit(this.filter);
}
onModeChange(mode: DisplayMode) {
this.filter.displayMode = mode;
this.filterUpdated.emit(this.filter);
}
onChange(event) {
// console.debug('filter change', this.filter);
}
}

View File

@@ -1,85 +0,0 @@
<div class="ui segement">
<app-list-filter (filterUpdated)="onFilterUpdate($event)" [filter]="state.filter"></app-list-filter>
<div *ngIf="state.filter.displayMode == DisplayMode.Grid" class="ui dark four doubling cards">
<div *ngIf="state.filter.filterMode == FilterMode.Scenes; then sceneCards"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Galleries; then galleryCards"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Performers; then performerCards"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Studios; then studioCards"></div>
</div>
<div *ngIf="state.filter.displayMode == DisplayMode.List" class="ui dark divided items">
<div *ngIf="state.filter.filterMode == FilterMode.Scenes; then sceneListItems"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Galleries; then galleryListItems"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Performers; then performerListItems"></div>
<div *ngIf="state.filter.filterMode == FilterMode.Studios; then studioListItems"></div>
</div>
<div *ngIf="state.filter.displayMode == DisplayMode.Wall" style="position: relative; left: calc(-50vw + 50.7%); width: 99.2vw;">
<div class="ui five column grid" style="margin: 0;">
<div *ngIf="state.filter.filterMode == FilterMode.Scenes; then sceneWallItems"></div>
<div *ngIf="state.filter.filterMode == FilterMode.SceneMarkers; then sceneMarkerWallItems"></div>
</div>
</div>
<app-sui-pagination #pagination id="main-pagination" (pageChange)="getPage($event)"></app-sui-pagination>
<div class="ui inverted dimmer" [class.active]="loading">
<div class="ui centered large text loader">Loading...</div>
</div>
</div>
<ng-template #sceneCards>
<app-scene-card *ngFor="let scene of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
[scene]="scene">
</app-scene-card>
</ng-template>
<ng-template #sceneListItems>
<app-scene-list-item *ngFor="let scene of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }" [scene]="scene">
</app-scene-list-item>
</ng-template>
<ng-template #sceneWallItems>
<div
*ngFor="let scene of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
class="wall column">
<app-scene-wall-item [scene]="scene"></app-scene-wall-item>
</div>
</ng-template>
<ng-template #galleryCards>
<app-gallery-card *ngFor="let gallery of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
[gallery]="gallery">
</app-gallery-card>
</ng-template>
<ng-template #galleryListItems>
<!--<app-gallery-list-item *ngFor="let gallery of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }" [scene]="scene">
</app-gallery-list-item>-->
</ng-template>
<ng-template #performerCards>
<app-performer-card *ngFor="let performer of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
[performer]="performer">
</app-performer-card>
</ng-template>
<ng-template #performerListItems>
<app-performer-list-item *ngFor="let performer of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }" [performer]="performer">
</app-performer-list-item>
</ng-template>
<ng-template #studioCards>
<app-studio-card *ngFor="let studio of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
[studio]="studio">
</app-studio-card>
</ng-template>
<ng-template #studioListItems>
<!--<app-studio-list-item *ngFor="let studio of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }" [scene]="scene">
</app-studio-list-item>-->
</ng-template>
<ng-template #sceneMarkerWallItems>
<div
*ngFor="let sceneMarker of state.data | paginate: { itemsPerPage: state.filter.itemsPerPage, currentPage: state.filter.currentPage, totalItems: state.totalCount }"
class="wall column">
<app-scene-marker-wall-item
[sceneMarker]="sceneMarker">
</app-scene-marker-wall-item>
</div>
</ng-template>

View File

@@ -1,95 +0,0 @@
import { Component, OnInit, OnDestroy, Input, AfterViewInit } from '@angular/core';
import { StashService } from '../../core/stash.service';
import {
DisplayMode,
FilterMode,
ListFilter,
ListState,
SceneListState,
GalleryListState,
PerformerListState,
StudioListState,
SceneMarkerListState
} from '../../shared/models/list-state.model';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit, OnDestroy, AfterViewInit {
DisplayMode = DisplayMode;
FilterMode = FilterMode;
@Input() state: ListState<any>;
loading = true;
constructor(private stashService: StashService,
private router: Router,
private activatedRoute: ActivatedRoute) {}
ngOnInit() {}
ngOnDestroy() {
this.loading = true;
this.state.reset();
this.state.scrollY = window.scrollY;
}
ngAfterViewInit() {
if (!!this.state.scrollY) {
setTimeout(() => {
window.scroll(0, this.state.scrollY);
}, 1);
} else {
window.scroll(0, 0);
}
}
async getData() {
this.loading = true;
if (this.state instanceof SceneListState) {
const result = await this.stashService.findScenes(this.state.filter.currentPage, this.state.filter).result();
this.state.data = result.data.findScenes.scenes;
this.state.totalCount = result.data.findScenes.count;
} else if (this.state instanceof GalleryListState) {
const result = await this.stashService.findGalleries(this.state.filter.currentPage, this.state.filter).result();
this.state.data = result.data.findGalleries.galleries;
this.state.totalCount = result.data.findGalleries.count;
} else if (this.state instanceof PerformerListState) {
const result = await this.stashService.findPerformers(this.state.filter.currentPage, this.state.filter).result();
this.state.data = result.data.findPerformers.performers;
this.state.totalCount = result.data.findPerformers.count;
} else if (this.state instanceof StudioListState) {
const result = await this.stashService.findStudios(this.state.filter.currentPage, this.state.filter).result();
this.state.data = result.data.findStudios.studios;
this.state.totalCount = result.data.findStudios.count;
} else if (this.state instanceof SceneMarkerListState) {
const result = await this.stashService.findSceneMarkers(this.state.filter.currentPage, this.state.filter).result();
this.state.data = result.data.findSceneMarkers.scene_markers;
this.state.totalCount = result.data.findSceneMarkers.count;
}
this.loading = false;
}
onFilterUpdate(filter: ListFilter) {
console.log('filter update', filter);
const options = Object.assign({relativeTo: this.activatedRoute, replaceUrl: true}, filter.makeQueryParameters());
this.router.navigate([], options);
this.state.filter = filter;
this.getData();
}
getPage(page: number) {
this.state.filter.currentPage = page;
this.onFilterUpdate(this.state.filter);
window.scroll(0, 0);
}
}

View File

@@ -1,5 +0,0 @@
export class GalleryImage {
index: number;
name: string;
path?: string;
}

View File

@@ -1,426 +0,0 @@
import {
SceneFilterType,
ResolutionEnum,
PerformerFilterType,
SceneMarkerFilterType,
SlimSceneData,
PerformerData,
StudioData,
GalleryData,
SceneMarkerData
} from '../../core/graphql-generated';
import { StashService } from '../../core/stash.service';
export enum DisplayMode {
Grid,
List,
Wall
}
export enum FilterMode {
Scenes,
Performers,
Studios,
Galleries,
SceneMarkers
}
export class CustomCriteria {
key: string;
value: string;
constructor(key: string, value: string) {
this.key = key;
this.value = value;
}
}
export enum CriteriaType {
None,
Rating,
Resolution,
Favorite,
HasMarkers,
IsMissing,
Tags,
SceneTags,
Performers
}
export enum CriteriaValueType {
Single,
Multiple
}
export class CriteriaOption {
name: string;
value: CriteriaType;
constructor(type: CriteriaType, name: string = CriteriaType[type]) {
this.name = name;
this.value = type;
}
}
interface CriteriaConfig {
valueType: CriteriaValueType;
parameterName: string;
options: any[];
}
export class Criteria {
type: CriteriaType;
valueType: CriteriaValueType;
options: any[] = [];
parameterName: string;
value: string;
values: string[];
private stashService: StashService;
async configure(type: CriteriaType, stashService: StashService) {
this.type = type;
this.stashService = stashService;
let config: CriteriaConfig = {
valueType: CriteriaValueType.Single,
parameterName: '',
options: []
};
switch (type) {
case CriteriaType.Rating:
config.parameterName = 'rating';
config.options = [1, 2, 3, 4, 5];
break;
case CriteriaType.Resolution:
config.parameterName = 'resolution';
config.options = ['240p', '480p', '720p', '1080p', '4k'];
break;
case CriteriaType.Favorite:
config.parameterName = 'filter_favorites';
config.options = ['true', 'false'];
break;
case CriteriaType.HasMarkers:
config.parameterName = 'has_markers';
config.options = ['true', 'false'];
break;
case CriteriaType.IsMissing:
config.parameterName = 'is_missing';
config.options = ['title', 'url', 'date', 'gallery', 'studio', 'performers'];
break;
case CriteriaType.Tags:
config = await this.configureTags('tags');
break;
case CriteriaType.SceneTags:
config = await this.configureTags('scene_tags');
break;
case CriteriaType.Performers:
config = await this.configurePerformers('performers');
break;
case CriteriaType.None:
default: break;
}
this.valueType = config.valueType;
this.parameterName = config.parameterName;
this.options = config.options;
this.value = ''; // Need this or else we send invalid value to the new filter
// this.values = []; // TODO this seems to break the "Multiple" filters
}
private async configureTags(name: string) {
const result = await this.stashService.allTagsForFilter().result();
return {
valueType: CriteriaValueType.Multiple,
parameterName: name,
options: result.data.allTags.map(item => {
return { id: item.id, name: item.name };
})
};
}
private async configurePerformers(name: string) {
const result = await this.stashService.allPerformersForFilter().result();
return {
valueType: CriteriaValueType.Multiple,
parameterName: name,
options: result.data.allPerformers.map(item => {
return { id: item.id, name: item.name, image_path: item.image_path };
})
};
}
}
export class ListFilter {
searchTerm?: string;
performers?: number[];
currentPage = 1;
itemsPerPage = 40;
sortDirection = 'asc';
sortBy: string;
displayModeOptions: DisplayMode[] = [];
displayMode: DisplayMode;
filterMode: FilterMode;
sortByOptions: string[];
criteriaFilterOpen = false;
criteriaOptions: CriteriaOption[];
criterions: Criteria[] = [];
customCriteria: CustomCriteria[] = [];
configureForFilterMode(filterMode: FilterMode) {
switch (filterMode) {
case FilterMode.Scenes:
if (!!this.sortBy === false) { this.sortBy = 'date'; }
this.sortByOptions = ['title', 'rating', 'date', 'filesize', 'duration', 'framerate', 'bitrate', 'random'];
this.displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List,
DisplayMode.Wall
];
this.criteriaOptions = [
new CriteriaOption(CriteriaType.None),
new CriteriaOption(CriteriaType.Rating),
new CriteriaOption(CriteriaType.Resolution),
new CriteriaOption(CriteriaType.HasMarkers),
new CriteriaOption(CriteriaType.IsMissing),
new CriteriaOption(CriteriaType.Tags)
];
break;
case FilterMode.Performers:
if (!!this.sortBy === false) { this.sortBy = 'name'; }
this.sortByOptions = ['name', 'height', 'birthdate', 'scenes_count'];
this.displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List
];
this.criteriaOptions = [
new CriteriaOption(CriteriaType.None),
new CriteriaOption(CriteriaType.Favorite)
];
break;
case FilterMode.Studios:
if (!!this.sortBy === false) { this.sortBy = 'name'; }
this.sortByOptions = ['name', 'scenes_count'];
this.displayModeOptions = [
DisplayMode.Grid
];
this.criteriaOptions = [
new CriteriaOption(CriteriaType.None)
];
break;
case FilterMode.Galleries:
if (!!this.sortBy === false) { this.sortBy = 'title'; }
this.sortByOptions = ['title', 'path'];
this.displayModeOptions = [
DisplayMode.Grid
];
this.criteriaOptions = [
new CriteriaOption(CriteriaType.None)
];
break;
case FilterMode.SceneMarkers:
if (!!this.sortBy === false) { this.sortBy = 'title'; }
this.sortByOptions = ['title', 'seconds', 'scene_id', 'random', 'scenes_updated_at'];
this.displayModeOptions = [
DisplayMode.Wall
];
this.criteriaOptions = [
new CriteriaOption(CriteriaType.None),
new CriteriaOption(CriteriaType.Tags),
new CriteriaOption(CriteriaType.SceneTags),
new CriteriaOption(CriteriaType.Performers)
];
break;
default:
this.sortByOptions = [];
this.displayModeOptions = [];
this.criteriaOptions = [new CriteriaOption(CriteriaType.None)];
break;
}
if (!!this.displayMode === false) { this.displayMode = this.displayModeOptions[0]; }
}
configureFromQueryParameters(params, stashService: StashService) {
if (params['sortby'] != null) {
this.sortBy = params['sortby'];
}
if (params['sortdir'] != null) {
this.sortDirection = params['sortdir'];
}
if (params['disp'] != null) {
this.displayMode = params['disp'];
}
if (params['q'] != null) {
this.searchTerm = params['q'];
}
if (params['p'] != null) {
this.currentPage = Number(params['p']);
}
if (params['c'] != null) {
this.criterions = [];
let jsonParameters: any[];
if (params['c'] instanceof Array) {
jsonParameters = params['c'];
} else {
jsonParameters = [params['c']];
}
if (jsonParameters.length !== 0) {
this.criteriaFilterOpen = true;
}
jsonParameters.forEach(jsonString => {
const encodedCriteria = JSON.parse(jsonString);
const criteria = new Criteria();
criteria.configure(encodedCriteria.type, stashService);
if (criteria.valueType === CriteriaValueType.Single) {
criteria.value = encodedCriteria.value;
} else {
criteria.values = encodedCriteria.values;
}
this.criterions.push(criteria);
});
}
}
makeQueryParameters(): any {
const encodedCriterion = [];
this.criterions.forEach(criteria => {
const encodedCriteria: any = {};
encodedCriteria.type = criteria.type;
if (criteria.valueType === CriteriaValueType.Single) {
encodedCriteria.value = criteria.value;
} else {
encodedCriteria.values = criteria.values;
}
const jsonCriteria = JSON.stringify(encodedCriteria);
encodedCriterion.push(jsonCriteria);
});
const result = {
queryParams: {
sortby: this.sortBy,
sortdir: this.sortDirection,
disp: this.displayMode,
q: this.searchTerm,
p: this.currentPage,
c: encodedCriterion
},
queryParamsHandling: 'merge'
};
return result;
}
// TODO: These don't support multiple of the same criteria, only the last one set is used.
makeSceneFilter(): SceneFilterType {
const result: SceneFilterType = {};
this.criterions.forEach(criteria => {
switch (criteria.type) {
case CriteriaType.Rating:
result.rating = Number(criteria.value);
break;
case CriteriaType.Resolution: {
switch (criteria.value) {
case '240p': result.resolution = ResolutionEnum.Low; break;
case '480p': result.resolution = ResolutionEnum.Standard; break;
case '720p': result.resolution = ResolutionEnum.StandardHd; break;
case '1080p': result.resolution = ResolutionEnum.FullHd; break;
case '4k': result.resolution = ResolutionEnum.FourK; break;
}
break;
}
case CriteriaType.HasMarkers:
result.has_markers = criteria.value;
break;
case CriteriaType.IsMissing:
result.is_missing = criteria.value;
break;
case CriteriaType.Tags:
result.tags = criteria.values;
break;
}
});
return result;
}
makePerformerFilter(): PerformerFilterType {
const result: PerformerFilterType = {};
this.criterions.forEach(criteria => {
switch (criteria.type) {
case CriteriaType.Favorite:
result.filter_favorites = criteria.value === 'true';
break;
}
});
return result;
}
makeSceneMarkerFilter(): SceneMarkerFilterType {
const result: SceneMarkerFilterType = {};
this.criterions.forEach(criteria => {
switch (criteria.type) {
case CriteriaType.Tags:
result.tags = criteria.values;
break;
case CriteriaType.SceneTags:
result.scene_tags = criteria.values;
break;
case CriteriaType.Performers:
result.performers = criteria.values;
break;
}
});
return result;
}
}
export class ListState<T> {
totalCount: number;
scrollY: number;
filter: ListFilter = new ListFilter();
data: T[];
reset() {
this.data = null;
this.totalCount = null;
}
}
export class SceneListState extends ListState<SlimSceneData.Fragment> {
constructor() {
super();
this.filter.filterMode = FilterMode.Scenes;
}
}
export class PerformerListState extends ListState<PerformerData.Fragment> {
constructor() {
super();
this.filter.filterMode = FilterMode.Performers;
}
}
export class StudioListState extends ListState<StudioData.Fragment> {
constructor() {
super();
this.filter.filterMode = FilterMode.Studios;
}
}
export class GalleryListState extends ListState<GalleryData.Fragment> {
constructor() {
super();
this.filter.filterMode = FilterMode.Galleries;
}
}
export class SceneMarkerListState extends ListState<SceneMarkerData.Fragment> {
constructor() {
super();
this.filter.filterMode = FilterMode.SceneMarkers;
}
}

View File

@@ -1,18 +0,0 @@
<a [routerLink]="['/performers', performer.id]" class="performer image" [style.background-image]="'url(' + performer.image_path + ')'"></a>
<div class="content">
<div class="header">
<div class="ui heart rating">
<i class="icon" [class.active]="performer.favorite"></i>
</div>
{{performer.name}}
</div>
<div class="meta">
<div *ngIf="!!performer.birthdate" class="item">
<span *ngIf="!!ageFromDate" class="date">{{performer.birthdate | age: ageFromDate}} years old in this scene.</span>
<span *ngIf="!!ageFromDate === false" class="date">{{performer.birthdate | age}} years old.</span>
</div>
<div class="item">
<span>Stars in {{performer.scene_count}} scenes.</span>
</div>
</div>
</div>

View File

@@ -1,21 +0,0 @@
import { Component, OnInit, Input, HostBinding } from '@angular/core';
import { PerformerData } from '../../core/graphql-generated';
@Component({
selector: 'app-performer-card',
templateUrl: './performer-card.component.html',
styleUrls: ['./performer-card.component.css']
})
export class PerformerCardComponent implements OnInit {
@Input() performer: PerformerData.Fragment;
@Input() ageFromDate: string;
// The host class needs to be card
@HostBinding('class') class = 'dark card';
constructor() {}
ngOnInit() {}
}

View File

@@ -1,32 +0,0 @@
<a [routerLink]="['/performers', performer.id]" class="image previewable" style="width: 75px;">
<img [src]="performer.image_path" />
</a>
<div class="content">
<div class="ui header">
<div class="ui heart rating">
<i class="icon" [class.active]="performer.favorite"></i>
</div>
{{performer.name}} <span *ngIf="performer.birthdate">- {{performer.birthdate | age}}</span>
<span *ngIf="performer.aliases" class="sub header">({{performer.aliases}})</span>
</div>
<div *ngIf="performer.country" class="meta">Country: {{performer.country}}</div>
<div *ngIf="performer.measurements" class="meta">Measurements: {{performer.measurements}}</div>
<div *ngIf="performer.scene_count > 0" class="meta">
<button class="ui inverted mini basic button" (click)="toggleScenes()">
{{performer.scene_count}} scenes
</button>
</div>
<div [suiCollapse]="!showingScenes">
<div class="extra content">
<div class="header">Sample Scenes</div>
<div class="ui equal width grid" style="margin: 0; align-items: center; overflow: hidden;">
<div *ngFor="let scene of scenes" class="column" style="padding: 0;">
<a [routerLink]="['/scenes', scene.id]">
<img class="ui image" style="max-height: 100%;" [src]="scene.paths.webp" />
</a>
</div>
</div>
</div>
</div>
</div>

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