mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Merge from master
This commit is contained in:
commit
019712bff9
216 changed files with 831 additions and 35960 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -18,6 +18,13 @@
|
||||||
# Packr2 artifacts
|
# Packr2 artifacts
|
||||||
**/*-packr.go
|
**/*-packr.go
|
||||||
|
|
||||||
|
# GraphQL generated output
|
||||||
|
pkg/models/generated_*.go
|
||||||
|
ui/v2/src/core/generated-*.tsx
|
||||||
|
|
||||||
|
# packr generated files
|
||||||
|
*-packr.go
|
||||||
|
|
||||||
####
|
####
|
||||||
# Jetbrains
|
# Jetbrains
|
||||||
####
|
####
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ env:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
before_install:
|
before_install:
|
||||||
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
||||||
- cd ui/v2
|
- yarn --cwd ui/v2 install
|
||||||
- yarn install
|
- make generate
|
||||||
- CI=false yarn build # TODO: Fix warnings
|
- CI=false yarn --cwd ui/v2 build # TODO: Fix warnings
|
||||||
- cd ../..
|
|
||||||
#- go get -v github.com/mgechev/revive
|
#- go get -v github.com/mgechev/revive
|
||||||
script:
|
script:
|
||||||
#- make lint
|
#- make lint
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -15,9 +15,9 @@ clean:
|
||||||
packr2 clean
|
packr2 clean
|
||||||
|
|
||||||
# Regenerates GraphQL files
|
# Regenerates GraphQL files
|
||||||
.PHONY: gqlgen
|
.PHONY: generate
|
||||||
gqlgen:
|
generate:
|
||||||
go run scripts/gqlgen.go
|
go generate
|
||||||
cd ui/v2 && yarn run gqlgen
|
cd ui/v2 && yarn run gqlgen
|
||||||
|
|
||||||
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
|
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
|
||||||
|
|
|
||||||
|
|
@ -88,15 +88,17 @@ TODO
|
||||||
|
|
||||||
## Commands
|
## 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 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 vet` - Run `go vet`
|
||||||
* `make lint` - Run the linter
|
* `make lint` - Run the linter
|
||||||
|
|
||||||
## Building a release
|
## Building a release
|
||||||
|
|
||||||
1. cd into the `ui/v2` directory and run `yarn build` to compile the frontend
|
1. Run `make generate` to create generated files
|
||||||
2. cd back to the root directory and run `make build` to build the executable for your current platform
|
2. Run `make ui` to compile the frontend
|
||||||
|
3. Run `make build` to build the executable for your current platform
|
||||||
|
|
||||||
## Cross compiling
|
## Cross compiling
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
version: '3.4'
|
version: '3.4'
|
||||||
services:
|
services:
|
||||||
stash:
|
stash:
|
||||||
image: stashapp/stash:x86_64
|
image: stashapp/stash:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "9999:9999"
|
- "9999:9999"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ FROM ubuntu:18.04 as prep
|
||||||
LABEL MAINTAINER="leopere [at] nixc [dot] us"
|
LABEL MAINTAINER="leopere [at] nixc [dot] us"
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y install curl xz-utils ca-certificates -y && \
|
apt-get -y install curl xz-utils && \
|
||||||
update-ca-certificates && \
|
|
||||||
apt-get autoclean -y && \
|
apt-get autoclean -y && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
@ -16,7 +15,9 @@ RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/rele
|
||||||
mv /ffmpeg*/ /ffmpeg/
|
mv /ffmpeg*/ /ffmpeg/
|
||||||
|
|
||||||
FROM ubuntu:18.04 as app
|
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/
|
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||||
EXPOSE 9999
|
EXPOSE 9999
|
||||||
CMD ["stash"]
|
CMD ["stash"]
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -22,3 +22,5 @@ require (
|
||||||
github.com/vektah/gqlparser v1.1.2
|
github.com/vektah/gqlparser v1.1.2
|
||||||
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
|
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
3
go.sum
|
|
@ -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/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/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=
|
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 h1:oTsAt8YXjEk1fo7uZR7gya1jrH48oPulx5oF6zWTHRw=
|
||||||
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE=
|
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=
|
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 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
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/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/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/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=
|
github.com/aws/aws-sdk-go v1.15.54/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ query MetadataExport {
|
||||||
metadataExport
|
metadataExport
|
||||||
}
|
}
|
||||||
|
|
||||||
query MetadataScan {
|
query MetadataScan($input: ScanMetadataInput!) {
|
||||||
metadataScan
|
metadataScan(input: $input)
|
||||||
}
|
}
|
||||||
|
|
||||||
query MetadataGenerate($input: GenerateMetadataInput!) {
|
query MetadataGenerate($input: GenerateMetadataInput!) {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ type Query {
|
||||||
"""Start an export. Returns the job ID"""
|
"""Start an export. Returns the job ID"""
|
||||||
metadataExport: String!
|
metadataExport: String!
|
||||||
"""Start a scan. Returns the job ID"""
|
"""Start a scan. Returns the job ID"""
|
||||||
metadataScan: String!
|
metadataScan(input: ScanMetadataInput!): String!
|
||||||
"""Start generating content. Returns the job ID"""
|
"""Start generating content. Returns the job ID"""
|
||||||
metadataGenerate(input: GenerateMetadataInput!): String!
|
metadataGenerate(input: GenerateMetadataInput!): String!
|
||||||
"""Clean metadata. Returns the job ID"""
|
"""Clean metadata. Returns the job ID"""
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,8 @@ input GenerateMetadataInput {
|
||||||
previews: Boolean!
|
previews: Boolean!
|
||||||
markers: Boolean!
|
markers: Boolean!
|
||||||
transcodes: Boolean!
|
transcodes: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ScanMetadataInput {
|
||||||
|
nameFromMetadata: Boolean!
|
||||||
}
|
}
|
||||||
2
main.go
2
main.go
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:generate go run github.com/99designs/gqlgen
|
||||||
|
//go:generate go run github.com/gobuffalo/packr/v2/packr2
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -3,38 +3,47 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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) {
|
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
|
||||||
// Populate scene from the input
|
// Populate scene from the input
|
||||||
sceneID, _ := strconv.Atoi(input.ID)
|
sceneID, _ := strconv.Atoi(input.ID)
|
||||||
updatedTime := time.Now()
|
updatedTime := time.Now()
|
||||||
updatedScene := models.Scene{
|
updatedScene := models.ScenePartial{
|
||||||
ID: sceneID,
|
ID: sceneID,
|
||||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
}
|
}
|
||||||
if input.Title != nil {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
if input.StudioID != nil {
|
||||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
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
|
// 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
|
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 {
|
if input.GalleryID != nil {
|
||||||
// Save the gallery
|
// Save the gallery
|
||||||
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/manager"
|
"github.com/stashapp/stash/pkg/manager"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) MetadataScan(ctx context.Context) (string, error) {
|
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||||
manager.GetInstance().Scan()
|
manager.GetInstance().Scan(input.NameFromMetadata)
|
||||||
return "todo", nil
|
return "todo", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/manager"
|
"github.com/stashapp/stash/pkg/manager"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sceneRoutes struct{}
|
type sceneRoutes struct{}
|
||||||
|
|
@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router {
|
||||||
|
|
||||||
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
||||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||||
|
|
||||||
// detect if not a streamable file and try to transcode it instead
|
// detect if not a streamable file and try to transcode it instead
|
||||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
|
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())
|
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start stream based on query param, if provided
|
||||||
|
r.ParseForm()
|
||||||
|
startTime := r.Form.Get("start")
|
||||||
|
|
||||||
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
||||||
|
|
||||||
stream, process, err := encoder.StreamTranscode(*videoFile)
|
stream, process, err := encoder.StreamTranscode(*videoFile, startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
var scheme string
|
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"
|
scheme = "https"
|
||||||
} else {
|
} else {
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
||||||
stdoutString := string(stdoutData)
|
stdoutString := string(stdoutData)
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
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
|
return stdoutString, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,25 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
|
||||||
_, _ = e.run(probeResult, args)
|
_, _ = e.run(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) {
|
func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) {
|
||||||
args := []string{
|
args := []string{}
|
||||||
|
|
||||||
|
if startTime != "" {
|
||||||
|
args = append(args, "-ss", startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args,
|
||||||
"-i", probeResult.Path,
|
"-i", probeResult.Path,
|
||||||
"-c:v", "libvpx-vp9",
|
"-c:v", "libvpx-vp9",
|
||||||
"-vf", "scale=iw:-2",
|
"-vf", "scale=iw:-2",
|
||||||
"-deadline", "realtime",
|
"-deadline", "realtime",
|
||||||
"-cpu-used", "5",
|
"-cpu-used", "5",
|
||||||
|
"-row-mt", "1",
|
||||||
"-crf", "30",
|
"-crf", "30",
|
||||||
"-b:v", "0",
|
"-b:v", "0",
|
||||||
"-f", "webm",
|
"-f", "webm",
|
||||||
"pipe:",
|
"pipe:",
|
||||||
}
|
)
|
||||||
|
|
||||||
return e.stream(probeResult, args)
|
return e.stream(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||||
|
|
||||||
if result.Title == "" {
|
if result.Title == "" {
|
||||||
// default title to filename
|
// default title to filename
|
||||||
result.Title = filepath.Base(result.Path)
|
result.SetTitleFromPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Comment = probeJSON.Format.Tags.Comment
|
result.Comment = probeJSON.Format.Tags.Comment
|
||||||
|
|
@ -161,3 +161,7 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
||||||
|
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *VideoFile) SetTitleFromPath() {
|
||||||
|
v.Title = filepath.Base(v.Path)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *singleton) Scan() {
|
func (s *singleton) Scan(nameFromMetadata bool) {
|
||||||
if s.Status != Idle {
|
if s.Status != Idle {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ func (s *singleton) Scan() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, path := range results {
|
for _, path := range results {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
task := ScanTask{FilePath: path}
|
task := ScanTask{FilePath: path, NameFromMetadata: nameFromMetadata}
|
||||||
go task.Start(&wg)
|
go task.Start(&wg)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScanTask struct {
|
type ScanTask struct {
|
||||||
FilePath string
|
FilePath string
|
||||||
|
NameFromMetadata bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ScanTask) Start(wg *sync.WaitGroup) {
|
func (t *ScanTask) Start(wg *sync.WaitGroup) {
|
||||||
|
|
@ -90,6 +91,11 @@ func (t *ScanTask) scanScene() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override title to be filename if nameFromMetadata is false
|
||||||
|
if !t.NameFromMetadata {
|
||||||
|
videoFile.SetTitleFromPath()
|
||||||
|
}
|
||||||
|
|
||||||
checksum, err := t.calculateChecksum()
|
checksum, err := t.calculateChecksum()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err.Error())
|
logger.Error(err.Error())
|
||||||
|
|
@ -107,8 +113,11 @@ func (t *ScanTask) scanScene() {
|
||||||
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
|
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||||
scene.Path = t.FilePath
|
scenePartial := models.ScenePartial{
|
||||||
_, err = qb.Update(*scene, tx)
|
ID: scene.ID,
|
||||||
|
Path: &t.FilePath,
|
||||||
|
}
|
||||||
|
_, err = qb.Update(scenePartial, tx)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -10,15 +12,11 @@ func IsStreamable(scene *models.Scene) (bool, error) {
|
||||||
if scene == nil {
|
if scene == nil {
|
||||||
return false, fmt.Errorf("nil scene")
|
return false, fmt.Errorf("nil scene")
|
||||||
}
|
}
|
||||||
fileType, err := utils.FileType(scene.Path)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fileType.MIME.Value {
|
videoCodec := scene.VideoCodec.String
|
||||||
case "video/quicktime", "video/mp4", "video/webm", "video/x-m4v":
|
if ffmpeg.IsValidCodec(videoCodec) {
|
||||||
return true, nil
|
return true, nil
|
||||||
default:
|
} else {
|
||||||
hasTranscode, _ := HasTranscode(scene)
|
hasTranscode, _ := HasTranscode(scene)
|
||||||
return hasTranscode, nil
|
return hasTranscode, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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()))
|
|
||||||
}
|
|
||||||
|
|
@ -25,3 +25,25 @@ type Scene struct {
|
||||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_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"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GalleryQueryBuilder struct{}
|
type GalleryQueryBuilder struct{}
|
||||||
|
|
@ -50,6 +51,24 @@ func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gal
|
||||||
return &updatedGallery, nil
|
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) {
|
func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
|
||||||
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
|
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
|
||||||
args := []interface{}{id}
|
args := []interface{}{id}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/stashapp/stash/pkg/database"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/stashapp/stash/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scenesForPerformerQuery = `
|
const scenesForPerformerQuery = `
|
||||||
|
|
@ -60,26 +61,27 @@ func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error)
|
||||||
return &newScene, nil
|
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)
|
ensureTx(tx)
|
||||||
_, err := tx.NamedExec(
|
_, err := tx.NamedExec(
|
||||||
`UPDATE scenes SET `+SQLGenKeys(updatedScene)+` WHERE scenes.id = :id`,
|
`UPDATE scenes SET `+SQLGenKeysPartial(updatedScene)+` WHERE scenes.id = :id`,
|
||||||
updatedScene,
|
updatedScene,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Get(&updatedScene, `SELECT * FROM scenes WHERE id = ? LIMIT 1`, updatedScene.ID); err != nil {
|
return qb.find(updatedScene.ID, tx)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &updatedScene, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
|
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"
|
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
|
||||||
args := []interface{}{id}
|
args := []interface{}{id}
|
||||||
return qb.queryScene(query, args, nil)
|
return qb.queryScene(query, args, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*Scene, error) {
|
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")
|
whereClauses = append(whereClauses, "scenes.studio_id IS NULL")
|
||||||
case "performers":
|
case "performers":
|
||||||
whereClauses = append(whereClauses, "performers_join.scene_id IS NULL")
|
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:
|
default:
|
||||||
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
|
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ package models
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/stashapp/stash/pkg/database"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/stashapp/stash/pkg/database"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var randomSortFloat = rand.Float64()
|
var randomSortFloat = rand.Float64()
|
||||||
|
|
@ -235,6 +236,17 @@ func ensureTx(tx *sqlx.Tx) {
|
||||||
// of keys for non empty key:values. These keys are formated
|
// of keys for non empty key:values. These keys are formated
|
||||||
// keyname=:keyname with a comma seperating them
|
// keyname=:keyname with a comma seperating them
|
||||||
func SQLGenKeys(i interface{}) string {
|
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
|
var query []string
|
||||||
v := reflect.ValueOf(i)
|
v := reflect.ValueOf(i)
|
||||||
for i := 0; i < v.NumField(); i++ {
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
|
@ -246,46 +258,45 @@ func SQLGenKeys(i interface{}) string {
|
||||||
}
|
}
|
||||||
switch t := v.Field(i).Interface().(type) {
|
switch t := v.Field(i).Interface().(type) {
|
||||||
case string:
|
case string:
|
||||||
if t != "" {
|
if partial || t != "" {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case int:
|
case int:
|
||||||
if t != 0 {
|
if partial || t != 0 {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case float64:
|
case float64:
|
||||||
if t != 0 {
|
if partial || t != 0 {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case SQLiteTimestamp:
|
case SQLiteTimestamp:
|
||||||
if !t.Timestamp.IsZero() {
|
if partial || !t.Timestamp.IsZero() {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case SQLiteDate:
|
case SQLiteDate:
|
||||||
if t.Valid {
|
if partial || t.Valid {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case sql.NullString:
|
case sql.NullString:
|
||||||
if t.Valid {
|
if partial || t.Valid {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case sql.NullBool:
|
case sql.NullBool:
|
||||||
if t.Valid {
|
if partial || t.Valid {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case sql.NullInt64:
|
case sql.NullInt64:
|
||||||
if t.Valid {
|
if partial || t.Valid {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
case sql.NullFloat64:
|
case sql.NullFloat64:
|
||||||
if t.Valid {
|
if partial || t.Valid {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
reflectValue := reflect.ValueOf(t)
|
reflectValue := reflect.ValueOf(t)
|
||||||
kind := reflectValue.Kind()
|
|
||||||
isNil := reflectValue.IsNil()
|
isNil := reflectValue.IsNil()
|
||||||
if kind != reflect.Ptr && !isNil {
|
if !isNil {
|
||||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ func GetPerformer(performerName string) (*models.ScrapedPerformer, error) {
|
||||||
if strings.ToLower(s.Text()) == strings.ToLower(performerName) {
|
if strings.ToLower(s.Text()) == strings.ToLower(performerName) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
alias := s.ParentsFiltered(".babeNameBlock").Find(".babeAlias").First();
|
||||||
|
if strings.EqualFold(alias.Text(), "aka " + performerName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/99designs/gqlgen/cmd"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<h1>Page not found</h1>
|
|
||||||
|
|
@ -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() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 { }
|
|
||||||
|
|
@ -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 { }
|
|
||||||
|
|
@ -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() { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"/> {{ 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>
|
|
||||||
|
|
@ -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]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
}
|
|
||||||
|
|
@ -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() {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue