Merge from master

This commit is contained in:
WithoutPants 2019-10-17 10:16:36 +11:00
commit 019712bff9
216 changed files with 831 additions and 35960 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

@ -15,9 +15,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

@ -3,7 +3,7 @@
version: '3.4'
services:
stash:
image: stashapp/stash:x86_64
image: stashapp/stash:latest
restart: unless-stopped
ports:
- "9999:9999"

View file

@ -2,8 +2,7 @@ FROM ubuntu:18.04 as prep
LABEL MAINTAINER="leopere [at] nixc [dot] us"
RUN apt-get update && \
apt-get -y install curl xz-utils ca-certificates -y && \
update-ca-certificates && \
apt-get -y install curl xz-utils && \
apt-get autoclean -y && \
rm -rf /var/lib/apt/lists/*
WORKDIR /
@ -16,7 +15,9 @@ RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/rele
mv /ffmpeg*/ /ffmpeg/
FROM ubuntu:18.04 as app
RUN adduser stash --gecos GECOS --shell /bin/bash --disabled-password --home /home/stash
RUN apt-get update && \
apt-get -y install ca-certificates && \
adduser stash --gecos GECOS --shell /bin/bash --disabled-password --home /home/stash
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
EXPOSE 9999
CMD ["stash"]

2
go.mod
View file

@ -22,3 +22,5 @@ require (
github.com/vektah/gqlparser v1.1.2
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999

3
go.sum
View file

@ -9,8 +9,6 @@ dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20180924222215-a9235805469b/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a h1:oTsAt8YXjEk1fo7uZR7gya1jrH48oPulx5oF6zWTHRw=
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE=
github.com/99designs/gqlgen v0.8.2 h1:xOkDPWn/MZjkQ32pu6Axx15mNah0NAq9WalFqT+RavA=
@ -38,6 +36,7 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.15.54/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=

View file

@ -6,8 +6,8 @@ query MetadataExport {
metadataExport
}
query MetadataScan {
metadataScan
query MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}
query MetadataGenerate($input: GenerateMetadataInput!) {

View file

@ -57,7 +57,7 @@ type Query {
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Start a scan. Returns the job ID"""
metadataScan: String!
metadataScan(input: ScanMetadataInput!): String!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
"""Clean metadata. Returns the job ID"""

View file

@ -3,4 +3,8 @@ input GenerateMetadataInput {
previews: Boolean!
markers: Boolean!
transcodes: Boolean!
}
input ScanMetadataInput {
nameFromMetadata: Boolean!
}

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 (

View file

@ -3,38 +3,47 @@ package api
import (
"context"
"database/sql"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
// Populate scene from the input
sceneID, _ := strconv.Atoi(input.ID)
updatedTime := time.Now()
updatedScene := models.Scene{
updatedScene := models.ScenePartial{
ID: sceneID,
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedScene.Title = sql.NullString{String: *input.Title, Valid: true}
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Details != nil {
updatedScene.Details = sql.NullString{String: *input.Details, Valid: true}
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedScene.URL = sql.NullString{String: *input.URL, Valid: true}
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedScene.Date = models.SQLiteDate{String: *input.Date, Valid: true}
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
updatedScene.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
updatedScene.Rating = &sql.NullInt64{Valid: false}
}
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedScene.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedScene.StudioID = &sql.NullInt64{Valid: false}
}
// Start the transaction and save the scene marker
@ -47,6 +56,14 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
return nil, err
}
// Clear the existing gallery value
gqb := models.NewGalleryQueryBuilder()
err = gqb.ClearGalleryId(sceneID, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
if input.GalleryID != nil {
// Save the gallery
galleryID, _ := strconv.Atoi(*input.GalleryID)

View file

@ -2,12 +2,13 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) MetadataScan(ctx context.Context) (string, error) {
manager.GetInstance().Scan()
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
manager.GetInstance().Scan(input.NameFromMetadata)
return "todo", nil
}

View file

@ -1,17 +1,18 @@
package api
import (
"io"
"context"
"io"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
"github.com/stashapp/stash/pkg/ffmpeg"
"net/http"
"strconv"
"strings"
)
type sceneRoutes struct{}
@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router {
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
// detect if not a streamable file and try to transcode it instead
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
@ -58,10 +59,14 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
logger.Errorf("[stream] error reading video file: %s", err.Error())
return
}
// start stream based on query param, if provided
r.ParseForm()
startTime := r.Form.Get("start")
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
stream, process, err := encoder.StreamTranscode(*videoFile)
stream, process, err := encoder.StreamTranscode(*videoFile, startTime)
if err != nil {
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
return

View file

@ -235,7 +235,7 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
ctx := r.Context()
var scheme string
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" {
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
} else {
scheme = "http"

View file

@ -57,7 +57,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
stdoutString := string(stdoutData)
if err := cmd.Wait(); err != nil {
logger.Errorf("ffmpeg error when running command <%s>", strings.Join(cmd.Args, " "))
logger.Errorf("ffmpeg error when running command <%s>: %s", strings.Join(cmd.Args, " "), stdoutString)
return stdoutString, err
}

View file

@ -26,17 +26,25 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
_, _ = e.run(probeResult, args)
}
func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) {
args := []string{
func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) {
args := []string{}
if startTime != "" {
args = append(args, "-ss", startTime)
}
args = append(args,
"-i", probeResult.Path,
"-c:v", "libvpx-vp9",
"-vf", "scale=iw:-2",
"-deadline", "realtime",
"-cpu-used", "5",
"-row-mt", "1",
"-crf", "30",
"-b:v", "0",
"-f", "webm",
"pipe:",
}
)
return e.stream(probeResult, args)
}

View file

@ -89,7 +89,7 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
if result.Title == "" {
// default title to filename
result.Title = filepath.Base(result.Path)
result.SetTitleFromPath()
}
result.Comment = probeJSON.Format.Tags.Comment
@ -161,3 +161,7 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
return -1
}
func (v *VideoFile) SetTitleFromPath() {
v.Title = filepath.Base(v.Path)
}

View file

@ -11,7 +11,7 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
func (s *singleton) Scan() {
func (s *singleton) Scan(nameFromMetadata bool) {
if s.Status != Idle {
return
}
@ -31,7 +31,7 @@ func (s *singleton) Scan() {
var wg sync.WaitGroup
for _, path := range results {
wg.Add(1)
task := ScanTask{FilePath: path}
task := ScanTask{FilePath: path, NameFromMetadata: nameFromMetadata}
go task.Start(&wg)
wg.Wait()
}

View file

@ -16,7 +16,8 @@ import (
)
type ScanTask struct {
FilePath string
FilePath string
NameFromMetadata bool
}
func (t *ScanTask) Start(wg *sync.WaitGroup) {
@ -90,6 +91,11 @@ func (t *ScanTask) scanScene() {
return
}
// Override title to be filename if nameFromMetadata is false
if !t.NameFromMetadata {
videoFile.SetTitleFromPath()
}
checksum, err := t.calculateChecksum()
if err != nil {
logger.Error(err.Error())
@ -107,8 +113,11 @@ func (t *ScanTask) scanScene() {
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
} else {
logger.Infof("%s already exists. Updating path...", t.FilePath)
scene.Path = t.FilePath
_, err = qb.Update(*scene, tx)
scenePartial := models.ScenePartial{
ID: scene.ID,
Path: &t.FilePath,
}
_, err = qb.Update(scenePartial, tx)
}
} else {
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)

View file

@ -2,6 +2,8 @@ package manager
import (
"fmt"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
@ -10,15 +12,11 @@ func IsStreamable(scene *models.Scene) (bool, error) {
if scene == nil {
return false, fmt.Errorf("nil scene")
}
fileType, err := utils.FileType(scene.Path)
if err != nil {
return false, err
}
switch fileType.MIME.Value {
case "video/quicktime", "video/mp4", "video/webm", "video/x-m4v":
videoCodec := scene.VideoCodec.String
if ffmpeg.IsValidCodec(videoCodec) {
return true, nil
default:
} else {
hasTranscode, _ := HasTranscode(scene)
return hasTranscode, nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,454 +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 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 Version struct {
Hash string `json:"hash"`
BuildTime string `json:"build_time"`
}
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

@ -25,3 +25,25 @@ type Scene struct {
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
type ScenePartial struct {
ID int `db:"id" json:"id"`
Checksum *string `db:"checksum" json:"checksum"`
Path *string `db:"path" json:"path"`
Title *sql.NullString `db:"title" json:"title"`
Details *sql.NullString `db:"details" json:"details"`
URL *sql.NullString `db:"url" json:"url"`
Date *SQLiteDate `db:"date" json:"date"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Size *sql.NullString `db:"size" json:"size"`
Duration *sql.NullFloat64 `db:"duration" json:"duration"`
VideoCodec *sql.NullString `db:"video_codec" json:"video_codec"`
AudioCodec *sql.NullString `db:"audio_codec" json:"audio_codec"`
Width *sql.NullInt64 `db:"width" json:"width"`
Height *sql.NullInt64 `db:"height" json:"height"`
Framerate *sql.NullFloat64 `db:"framerate" json:"framerate"`
Bitrate *sql.NullInt64 `db:"bitrate" json:"bitrate"`
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View file

@ -2,9 +2,10 @@ package models
import (
"database/sql"
"path/filepath"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"path/filepath"
)
type GalleryQueryBuilder struct{}
@ -50,6 +51,24 @@ func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gal
return &updatedGallery, nil
}
type GalleryNullSceneID struct {
SceneID sql.NullInt64
}
func (qb *GalleryQueryBuilder) ClearGalleryId(sceneID int, tx *sqlx.Tx) error {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`,
GalleryNullSceneID{
SceneID: sql.NullInt64{
Int64: int64(sceneID),
Valid: true,
},
},
)
return err
}
func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
args := []interface{}{id}

View file

@ -2,10 +2,11 @@ package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
const scenesForPerformerQuery = `
@ -60,26 +61,27 @@ func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error)
return &newScene, nil
}
func (qb *SceneQueryBuilder) Update(updatedScene Scene, tx *sqlx.Tx) (*Scene, error) {
func (qb *SceneQueryBuilder) Update(updatedScene ScenePartial, tx *sqlx.Tx) (*Scene, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE scenes SET `+SQLGenKeys(updatedScene)+` WHERE scenes.id = :id`,
`UPDATE scenes SET `+SQLGenKeysPartial(updatedScene)+` WHERE scenes.id = :id`,
updatedScene,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedScene, `SELECT * FROM scenes WHERE id = ? LIMIT 1`, updatedScene.ID); err != nil {
return nil, err
}
return &updatedScene, nil
return qb.find(updatedScene.ID, tx)
}
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
return qb.find(id, nil)
}
func (qb *SceneQueryBuilder) find(id int, tx *sqlx.Tx) (*Scene, error) {
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryScene(query, args, nil)
return qb.queryScene(query, args, tx)
}
func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*Scene, error) {
@ -206,6 +208,8 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
whereClauses = append(whereClauses, "scenes.studio_id IS NULL")
case "performers":
whereClauses = append(whereClauses, "performers_join.scene_id IS NULL")
case "date":
whereClauses = append(whereClauses, "scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"")
default:
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
}

View file

@ -3,13 +3,14 @@ package models
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
"math/rand"
"reflect"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
)
var randomSortFloat = rand.Float64()
@ -235,6 +236,17 @@ func ensureTx(tx *sqlx.Tx) {
// of keys for non empty key:values. These keys are formated
// keyname=:keyname with a comma seperating them
func SQLGenKeys(i interface{}) string {
return sqlGenKeys(i, false)
}
// support a partial interface. When a partial interface is provided,
// keys will always be included if the value is not null. The partial
// interface must therefore consist of pointers
func SQLGenKeysPartial(i interface{}) string {
return sqlGenKeys(i, true)
}
func sqlGenKeys(i interface{}, partial bool) string {
var query []string
v := reflect.ValueOf(i)
for i := 0; i < v.NumField(); i++ {
@ -246,46 +258,45 @@ func SQLGenKeys(i interface{}) string {
}
switch t := v.Field(i).Interface().(type) {
case string:
if t != "" {
if partial || t != "" {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case int:
if t != 0 {
if partial || t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case float64:
if t != 0 {
if partial || t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case SQLiteTimestamp:
if !t.Timestamp.IsZero() {
if partial || !t.Timestamp.IsZero() {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case SQLiteDate:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullString:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullBool:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullInt64:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullFloat64:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
default:
reflectValue := reflect.ValueOf(t)
kind := reflectValue.Kind()
isNil := reflectValue.IsNil()
if kind != reflect.Ptr && !isNil {
if !isNil {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
}

View file

@ -65,6 +65,10 @@ func GetPerformer(performerName string) (*models.ScrapedPerformer, error) {
if strings.ToLower(s.Text()) == strings.ToLower(performerName) {
return true
}
alias := s.ParentsFiltered(".babeNameBlock").Find(".babeAlias").First();
if strings.EqualFold(alias.Text(), "aka " + performerName) {
return true
}
return false
})

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(/^.*[\\\/]/, '');
}
}

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