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
|
||||
**/*-packr.go
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2/src/core/generated-*.tsx
|
||||
|
||||
# packr generated files
|
||||
*-packr.go
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ env:
|
|||
- GO111MODULE=on
|
||||
before_install:
|
||||
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
||||
- cd ui/v2
|
||||
- yarn install
|
||||
- CI=false yarn build # TODO: Fix warnings
|
||||
- cd ../..
|
||||
- yarn --cwd ui/v2 install
|
||||
- make generate
|
||||
- CI=false yarn --cwd ui/v2 build # TODO: Fix warnings
|
||||
#- go get -v github.com/mgechev/revive
|
||||
script:
|
||||
#- make lint
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -15,9 +15,9 @@ clean:
|
|||
packr2 clean
|
||||
|
||||
# Regenerates GraphQL files
|
||||
.PHONY: gqlgen
|
||||
gqlgen:
|
||||
go run scripts/gqlgen.go
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate
|
||||
cd ui/v2 && yarn run gqlgen
|
||||
|
||||
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
|
||||
|
|
|
|||
|
|
@ -88,15 +88,17 @@ TODO
|
|||
|
||||
## Commands
|
||||
|
||||
* `make generate` - Generate Go GraphQL and packr2 files
|
||||
* `make build` - Builds the binary (make sure to build the UI as well... see below)
|
||||
* `make gqlgen` - Regenerate Go GraphQL files
|
||||
* `make ui` - Builds the frontend
|
||||
* `make vet` - Run `go vet`
|
||||
* `make lint` - Run the linter
|
||||
|
||||
## Building a release
|
||||
|
||||
1. cd into the `ui/v2` directory and run `yarn build` to compile the frontend
|
||||
2. cd back to the root directory and run `make build` to build the executable for your current platform
|
||||
1. Run `make generate` to create generated files
|
||||
2. Run `make ui` to compile the frontend
|
||||
3. Run `make build` to build the executable for your current platform
|
||||
|
||||
## Cross compiling
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
version: '3.4'
|
||||
services:
|
||||
stash:
|
||||
image: stashapp/stash:x86_64
|
||||
image: stashapp/stash:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9999:9999"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ FROM ubuntu:18.04 as prep
|
|||
LABEL MAINTAINER="leopere [at] nixc [dot] us"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install curl xz-utils ca-certificates -y && \
|
||||
update-ca-certificates && \
|
||||
apt-get -y install curl xz-utils && \
|
||||
apt-get autoclean -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /
|
||||
|
|
@ -16,7 +15,9 @@ RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/rele
|
|||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
FROM ubuntu:18.04 as app
|
||||
RUN adduser stash --gecos GECOS --shell /bin/bash --disabled-password --home /home/stash
|
||||
RUN apt-get update && \
|
||||
apt-get -y install ca-certificates && \
|
||||
adduser stash --gecos GECOS --shell /bin/bash --disabled-password --home /home/stash
|
||||
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -22,3 +22,5 @@ require (
|
|||
github.com/vektah/gqlparser v1.1.2
|
||||
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
|
|
|||
3
go.sum
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/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
git.apache.org/thrift.git v0.0.0-20180924222215-a9235805469b/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a h1:oTsAt8YXjEk1fo7uZR7gya1jrH48oPulx5oF6zWTHRw=
|
||||
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE=
|
||||
github.com/99designs/gqlgen v0.8.2 h1:xOkDPWn/MZjkQ32pu6Axx15mNah0NAq9WalFqT+RavA=
|
||||
|
|
@ -38,6 +36,7 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
|
|||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aws/aws-sdk-go v1.15.54/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ query MetadataExport {
|
|||
metadataExport
|
||||
}
|
||||
|
||||
query MetadataScan {
|
||||
metadataScan
|
||||
query MetadataScan($input: ScanMetadataInput!) {
|
||||
metadataScan(input: $input)
|
||||
}
|
||||
|
||||
query MetadataGenerate($input: GenerateMetadataInput!) {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ type Query {
|
|||
"""Start an export. Returns the job ID"""
|
||||
metadataExport: String!
|
||||
"""Start a scan. Returns the job ID"""
|
||||
metadataScan: String!
|
||||
metadataScan(input: ScanMetadataInput!): String!
|
||||
"""Start generating content. Returns the job ID"""
|
||||
metadataGenerate(input: GenerateMetadataInput!): String!
|
||||
"""Clean metadata. Returns the job ID"""
|
||||
|
|
|
|||
|
|
@ -3,4 +3,8 @@ input GenerateMetadataInput {
|
|||
previews: Boolean!
|
||||
markers: 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
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -3,38 +3,47 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, _ := strconv.Atoi(input.ID)
|
||||
updatedTime := time.Now()
|
||||
updatedScene := models.Scene{
|
||||
updatedScene := models.ScenePartial{
|
||||
ID: sceneID,
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedScene.Title = sql.NullString{String: *input.Title, Valid: true}
|
||||
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedScene.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedScene.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedScene.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedScene.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedScene.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedScene.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
|
||||
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedScene.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
|
|
@ -47,6 +56,14 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Clear the existing gallery value
|
||||
gqb := models.NewGalleryQueryBuilder()
|
||||
err = gqb.ClearGalleryId(sceneID, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.GalleryID != nil {
|
||||
// Save the gallery
|
||||
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) MetadataScan(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Scan()
|
||||
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Scan(input.NameFromMetadata)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type sceneRoutes struct{}
|
||||
|
|
@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router {
|
|||
|
||||
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
|
||||
// detect if not a streamable file and try to transcode it instead
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
|
||||
|
||||
|
|
@ -58,10 +59,14 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
|||
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// start stream based on query param, if provided
|
||||
r.ParseForm()
|
||||
startTime := r.Form.Get("start")
|
||||
|
||||
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
||||
|
||||
stream, process, err := encoder.StreamTranscode(*videoFile)
|
||||
stream, process, err := encoder.StreamTranscode(*videoFile, startTime)
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
||||
return
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
|
|||
ctx := r.Context()
|
||||
|
||||
var scheme string
|
||||
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" {
|
||||
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
|||
stdoutString := string(stdoutData)
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
logger.Errorf("ffmpeg error when running command <%s>", strings.Join(cmd.Args, " "))
|
||||
logger.Errorf("ffmpeg error when running command <%s>: %s", strings.Join(cmd.Args, " "), stdoutString)
|
||||
return stdoutString, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,17 +26,25 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
|
|||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) {
|
||||
args := []string{
|
||||
func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) {
|
||||
args := []string{}
|
||||
|
||||
if startTime != "" {
|
||||
args = append(args, "-ss", startTime)
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "libvpx-vp9",
|
||||
"-vf", "scale=iw:-2",
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
"-f", "webm",
|
||||
"pipe:",
|
||||
}
|
||||
)
|
||||
|
||||
return e.stream(probeResult, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
|||
|
||||
if result.Title == "" {
|
||||
// default title to filename
|
||||
result.Title = filepath.Base(result.Path)
|
||||
result.SetTitleFromPath()
|
||||
}
|
||||
|
||||
result.Comment = probeJSON.Format.Tags.Comment
|
||||
|
|
@ -161,3 +161,7 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
|||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (v *VideoFile) SetTitleFromPath() {
|
||||
v.Title = filepath.Base(v.Path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *singleton) Scan() {
|
||||
func (s *singleton) Scan(nameFromMetadata bool) {
|
||||
if s.Status != Idle {
|
||||
return
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ func (s *singleton) Scan() {
|
|||
var wg sync.WaitGroup
|
||||
for _, path := range results {
|
||||
wg.Add(1)
|
||||
task := ScanTask{FilePath: path}
|
||||
task := ScanTask{FilePath: path, NameFromMetadata: nameFromMetadata}
|
||||
go task.Start(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import (
|
|||
)
|
||||
|
||||
type ScanTask struct {
|
||||
FilePath string
|
||||
FilePath string
|
||||
NameFromMetadata bool
|
||||
}
|
||||
|
||||
func (t *ScanTask) Start(wg *sync.WaitGroup) {
|
||||
|
|
@ -90,6 +91,11 @@ func (t *ScanTask) scanScene() {
|
|||
return
|
||||
}
|
||||
|
||||
// Override title to be filename if nameFromMetadata is false
|
||||
if !t.NameFromMetadata {
|
||||
videoFile.SetTitleFromPath()
|
||||
}
|
||||
|
||||
checksum, err := t.calculateChecksum()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
|
|
@ -107,8 +113,11 @@ func (t *ScanTask) scanScene() {
|
|||
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
|
||||
} else {
|
||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||
scene.Path = t.FilePath
|
||||
_, err = qb.Update(*scene, tx)
|
||||
scenePartial := models.ScenePartial{
|
||||
ID: scene.ID,
|
||||
Path: &t.FilePath,
|
||||
}
|
||||
_, err = qb.Update(scenePartial, tx)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package manager
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
|
@ -10,15 +12,11 @@ func IsStreamable(scene *models.Scene) (bool, error) {
|
|||
if scene == nil {
|
||||
return false, fmt.Errorf("nil scene")
|
||||
}
|
||||
fileType, err := utils.FileType(scene.Path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch fileType.MIME.Value {
|
||||
case "video/quicktime", "video/mp4", "video/webm", "video/x-m4v":
|
||||
videoCodec := scene.VideoCodec.String
|
||||
if ffmpeg.IsValidCodec(videoCodec) {
|
||||
return true, nil
|
||||
default:
|
||||
} else {
|
||||
hasTranscode, _ := HasTranscode(scene)
|
||||
return hasTranscode, nil
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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"`
|
||||
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 (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type GalleryQueryBuilder struct{}
|
||||
|
|
@ -50,6 +51,24 @@ func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gal
|
|||
return &updatedGallery, nil
|
||||
}
|
||||
|
||||
type GalleryNullSceneID struct {
|
||||
SceneID sql.NullInt64
|
||||
}
|
||||
|
||||
func (qb *GalleryQueryBuilder) ClearGalleryId(sceneID int, tx *sqlx.Tx) error {
|
||||
ensureTx(tx)
|
||||
_, err := tx.NamedExec(
|
||||
`UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`,
|
||||
GalleryNullSceneID{
|
||||
SceneID: sql.NullInt64{
|
||||
Int64: int64(sceneID),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
|
||||
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
|
||||
args := []interface{}{id}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package models
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
)
|
||||
|
||||
const scenesForPerformerQuery = `
|
||||
|
|
@ -60,26 +61,27 @@ func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error)
|
|||
return &newScene, nil
|
||||
}
|
||||
|
||||
func (qb *SceneQueryBuilder) Update(updatedScene Scene, tx *sqlx.Tx) (*Scene, error) {
|
||||
func (qb *SceneQueryBuilder) Update(updatedScene ScenePartial, tx *sqlx.Tx) (*Scene, error) {
|
||||
ensureTx(tx)
|
||||
_, err := tx.NamedExec(
|
||||
`UPDATE scenes SET `+SQLGenKeys(updatedScene)+` WHERE scenes.id = :id`,
|
||||
`UPDATE scenes SET `+SQLGenKeysPartial(updatedScene)+` WHERE scenes.id = :id`,
|
||||
updatedScene,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Get(&updatedScene, `SELECT * FROM scenes WHERE id = ? LIMIT 1`, updatedScene.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &updatedScene, nil
|
||||
return qb.find(updatedScene.ID, tx)
|
||||
}
|
||||
|
||||
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
|
||||
return qb.find(id, nil)
|
||||
}
|
||||
|
||||
func (qb *SceneQueryBuilder) find(id int, tx *sqlx.Tx) (*Scene, error) {
|
||||
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
|
||||
args := []interface{}{id}
|
||||
return qb.queryScene(query, args, nil)
|
||||
return qb.queryScene(query, args, tx)
|
||||
}
|
||||
|
||||
func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*Scene, error) {
|
||||
|
|
@ -206,6 +208,8 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
|||
whereClauses = append(whereClauses, "scenes.studio_id IS NULL")
|
||||
case "performers":
|
||||
whereClauses = append(whereClauses, "performers_join.scene_id IS NULL")
|
||||
case "date":
|
||||
whereClauses = append(whereClauses, "scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"")
|
||||
default:
|
||||
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package models
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var randomSortFloat = rand.Float64()
|
||||
|
|
@ -235,6 +236,17 @@ func ensureTx(tx *sqlx.Tx) {
|
|||
// of keys for non empty key:values. These keys are formated
|
||||
// keyname=:keyname with a comma seperating them
|
||||
func SQLGenKeys(i interface{}) string {
|
||||
return sqlGenKeys(i, false)
|
||||
}
|
||||
|
||||
// support a partial interface. When a partial interface is provided,
|
||||
// keys will always be included if the value is not null. The partial
|
||||
// interface must therefore consist of pointers
|
||||
func SQLGenKeysPartial(i interface{}) string {
|
||||
return sqlGenKeys(i, true)
|
||||
}
|
||||
|
||||
func sqlGenKeys(i interface{}, partial bool) string {
|
||||
var query []string
|
||||
v := reflect.ValueOf(i)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
|
|
@ -246,46 +258,45 @@ func SQLGenKeys(i interface{}) string {
|
|||
}
|
||||
switch t := v.Field(i).Interface().(type) {
|
||||
case string:
|
||||
if t != "" {
|
||||
if partial || t != "" {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case int:
|
||||
if t != 0 {
|
||||
if partial || t != 0 {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case float64:
|
||||
if t != 0 {
|
||||
if partial || t != 0 {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case SQLiteTimestamp:
|
||||
if !t.Timestamp.IsZero() {
|
||||
if partial || !t.Timestamp.IsZero() {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case SQLiteDate:
|
||||
if t.Valid {
|
||||
if partial || t.Valid {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case sql.NullString:
|
||||
if t.Valid {
|
||||
if partial || t.Valid {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case sql.NullBool:
|
||||
if t.Valid {
|
||||
if partial || t.Valid {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case sql.NullInt64:
|
||||
if t.Valid {
|
||||
if partial || t.Valid {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
case sql.NullFloat64:
|
||||
if t.Valid {
|
||||
if partial || t.Valid {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
default:
|
||||
reflectValue := reflect.ValueOf(t)
|
||||
kind := reflectValue.Kind()
|
||||
isNil := reflectValue.IsNil()
|
||||
if kind != reflect.Ptr && !isNil {
|
||||
if !isNil {
|
||||
query = append(query, fmt.Sprintf("%s=:%s", key, key))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ func GetPerformer(performerName string) (*models.ScrapedPerformer, error) {
|
|||
if strings.ToLower(s.Text()) == strings.ToLower(performerName) {
|
||||
return true
|
||||
}
|
||||
alias := s.ParentsFiltered(".babeNameBlock").Find(".babeAlias").First();
|
||||
if strings.EqualFold(alias.Text(), "aka " + performerName) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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