From 66d2c5ca04100a797ddedf50028af2a3af15be2b Mon Sep 17 00:00:00 2001 From: Stash Dev Date: Fri, 15 Feb 2019 09:15:00 -0800 Subject: [PATCH] UI V2 Squashed commits: [e74bbf9] stuff [28476de] stuff [c7efb7b] stuff [2c78f94] stuff [f79338e] stuff [a697876] stuff [85bb60e] stuff [9f108b2] stuff [d8e00c0] stuff [7787ef9] stuff [f7f10b7] stuff [aa266f7] stuff [511ba6b] stuff [7453747] stuff [db55e2d] stuff [b362623] stuff [7288c17] stuff [86638cd] stuff [879dac4] stuff [65a4996] stuff [c6fb361] stuff [d449ce7] stuff [349dffa] stuff [84206ab] stuff [0253c65] stuff [cc0992e] stuff [3289e7d] stuff [d9ab290] stuff [dcc980d] stuff [7787da8] stuff [5bcf7cd] stuff [00e9316] stuff [54c9398] stuff [72b6ee1] stuff [4b4b26c] stuff [4cbdb06] stuff [1a240b3] stuff [650ea08] stuff [37440ef] stuff [9ee66ba] stuff [b430c86] stuff [37159c3] stuff [deba837] stuff [6ac65f6] stuff [a2ca1a1] stuff [c010229] stuff [3fd7306] stuff [cbe6efc] stuff [997a8d0] stuff [d0708a2] stuff [d316aba] stuff [4fe9900] Added initial files --- pkg/models/model_gallery.go | 2 +- schema/documents/data/studio-slim.graphql | 5 + schema/documents/documents.graphql | 41 +- ui/v2/.env | 1 + ui/v2/.gitignore | 23 + ui/v2/.vscode/launch.json | 18 + ui/v2/.vscode/settings.json | 10 + ui/v2/README.md | 47 + ui/v2/codegen.yml | 17 + ui/v2/package.json | 62 + ui/v2/public/favicon.ico | Bin 0 -> 3870 bytes ui/v2/public/index.html | 41 + ui/v2/public/manifest.json | 15 + ui/v2/src/App.tsx | 35 + ui/v2/src/components/ErrorBoundary.tsx | 34 + ui/v2/src/components/Galleries/Galleries.tsx | 13 + ui/v2/src/components/Galleries/Gallery.tsx | 32 + .../src/components/Galleries/GalleryList.tsx | 54 + .../components/Galleries/GalleryViewer.tsx | 47 + ui/v2/src/components/MainNavbar.tsx | 112 + ui/v2/src/components/PageNotFound.tsx | 7 + ui/v2/src/components/Settings/Settings.tsx | 48 + .../Settings/SettingsAboutPanel.tsx | 19 + .../Settings/SettingsConfigurationPanel.tsx | 19 + .../components/Settings/SettingsLogsPanel.tsx | 19 + .../Settings/SettingsTasksPanel.tsx | 95 + .../components/Shared/DetailsEditNavbar.tsx | 97 + ui/v2/src/components/Stats.tsx | 68 + ui/v2/src/components/Studios/StudioCard.tsx | 33 + .../Studios/StudioDetails/Studio.tsx | 143 + ui/v2/src/components/Studios/StudioList.tsx | 36 + ui/v2/src/components/Studios/Studios.tsx | 13 + ui/v2/src/components/Tags/TagList.tsx | 109 + ui/v2/src/components/Tags/Tags.tsx | 11 + ui/v2/src/components/Wall/Wall.scss | 98 + ui/v2/src/components/Wall/WallItem.tsx | 114 + ui/v2/src/components/Wall/WallPanel.tsx | 89 + ui/v2/src/components/list/AddFilter.tsx | 140 + ui/v2/src/components/list/ListFilter.tsx | 188 + ui/v2/src/components/list/Pagination.tsx | 119 + .../components/performers/PerformerCard.tsx | 50 + .../performers/PerformerDetails/Performer.tsx | 285 + .../components/performers/PerformerList.tsx | 37 + .../src/components/performers/performers.tsx | 13 + ui/v2/src/components/scenes/SceneCard.tsx | 149 + .../components/scenes/SceneDetails/Scene.tsx | 91 + .../scenes/SceneDetails/SceneDetailPanel.tsx | 51 + .../scenes/SceneDetails/SceneEditPanel.tsx | 176 + .../SceneDetails/SceneFileInfoPanel.tsx | 129 + .../scenes/SceneDetails/SceneMarkersPanel.tsx | 272 + .../SceneDetails/ScenePerformerPanel.tsx | 21 + ui/v2/src/components/scenes/SceneList.tsx | 37 + .../src/components/scenes/SceneMarkerList.tsx | 31 + .../scenes/ScenePlayer/ScenePlayer.tsx | 152 + .../ScenePlayer/ScenePlayerScrubber.scss | 128 + .../ScenePlayer/ScenePlayerScrubber.tsx | 316 + ui/v2/src/components/scenes/helpers.tsx | 40 + ui/v2/src/components/scenes/scenes.tsx | 15 + .../components/select/FilterMultiSelect.tsx | 108 + ui/v2/src/components/select/FilterSelect.tsx | 100 + .../select/FreeOnesPerformerSuggest.tsx | 47 + .../components/select/MarkerTitleSuggest.tsx | 61 + .../select/ValidGalleriesSelect.tsx | 71 + ui/v2/src/core/StashService.ts | 204 + ui/v2/src/core/generated-graphql.tsx | 2239 +++ ui/v2/src/hooks/ListHook.tsx | 171 + ui/v2/src/hooks/VideoHover.ts | 69 + ui/v2/src/index.scss | 179 + ui/v2/src/index.tsx | 21 + ui/v2/src/models/base-props.ts | 3 + ui/v2/src/models/index.ts | 2 + .../models/list-filter/criteria/criterion.ts | 92 + .../models/list-filter/criteria/favorite.ts | 19 + .../list-filter/criteria/has-markers.ts | 19 + .../models/list-filter/criteria/is-missing.ts | 19 + ui/v2/src/models/list-filter/criteria/none.ts | 19 + .../models/list-filter/criteria/performers.ts | 26 + .../src/models/list-filter/criteria/rating.ts | 19 + .../models/list-filter/criteria/resolution.ts | 19 + .../models/list-filter/criteria/studios.ts | 26 + ui/v2/src/models/list-filter/criteria/tags.ts | 35 + .../src/models/list-filter/criteria/utils.ts | 33 + ui/v2/src/models/list-filter/filter.ts | 263 + ui/v2/src/models/list-filter/types.ts | 18 + ui/v2/src/models/react-images.d.ts | 5 + ui/v2/src/models/react-jw-player.d.ts | 5 + ui/v2/src/models/types.ts | 1 + ui/v2/src/react-app-env.d.ts | 1 + ui/v2/src/serviceWorker.ts | 143 + ui/v2/src/styles/_blueprint-overrides.scss | 26 + ui/v2/src/styles/_scrollbars.scss | 67 + ui/v2/src/styles/form/_grid.scss | 27 + ui/v2/src/styles/shared/_details.scss | 56 + ui/v2/src/utils/color.ts | 12 + ui/v2/src/utils/errors.ts | 24 + ui/v2/src/utils/navigation.ts | 44 + ui/v2/src/utils/table.tsx | 164 + ui/v2/src/utils/text.ts | 57 + ui/v2/src/utils/toasts.ts | 14 + ui/v2/tsconfig.json | 28 + ui/v2/tslint.json | 11 + ui/v2/yarn.lock | 11827 ++++++++++++++++ 102 files changed, 20432 insertions(+), 29 deletions(-) create mode 100644 schema/documents/data/studio-slim.graphql create mode 100644 ui/v2/.env create mode 100755 ui/v2/.gitignore create mode 100644 ui/v2/.vscode/launch.json create mode 100644 ui/v2/.vscode/settings.json create mode 100755 ui/v2/README.md create mode 100644 ui/v2/codegen.yml create mode 100644 ui/v2/package.json create mode 100755 ui/v2/public/favicon.ico create mode 100755 ui/v2/public/index.html create mode 100755 ui/v2/public/manifest.json create mode 100755 ui/v2/src/App.tsx create mode 100644 ui/v2/src/components/ErrorBoundary.tsx create mode 100644 ui/v2/src/components/Galleries/Galleries.tsx create mode 100644 ui/v2/src/components/Galleries/Gallery.tsx create mode 100644 ui/v2/src/components/Galleries/GalleryList.tsx create mode 100644 ui/v2/src/components/Galleries/GalleryViewer.tsx create mode 100644 ui/v2/src/components/MainNavbar.tsx create mode 100644 ui/v2/src/components/PageNotFound.tsx create mode 100644 ui/v2/src/components/Settings/Settings.tsx create mode 100644 ui/v2/src/components/Settings/SettingsAboutPanel.tsx create mode 100644 ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx create mode 100644 ui/v2/src/components/Settings/SettingsLogsPanel.tsx create mode 100644 ui/v2/src/components/Settings/SettingsTasksPanel.tsx create mode 100644 ui/v2/src/components/Shared/DetailsEditNavbar.tsx create mode 100644 ui/v2/src/components/Stats.tsx create mode 100644 ui/v2/src/components/Studios/StudioCard.tsx create mode 100644 ui/v2/src/components/Studios/StudioDetails/Studio.tsx create mode 100644 ui/v2/src/components/Studios/StudioList.tsx create mode 100644 ui/v2/src/components/Studios/Studios.tsx create mode 100644 ui/v2/src/components/Tags/TagList.tsx create mode 100644 ui/v2/src/components/Tags/Tags.tsx create mode 100644 ui/v2/src/components/Wall/Wall.scss create mode 100644 ui/v2/src/components/Wall/WallItem.tsx create mode 100644 ui/v2/src/components/Wall/WallPanel.tsx create mode 100644 ui/v2/src/components/list/AddFilter.tsx create mode 100644 ui/v2/src/components/list/ListFilter.tsx create mode 100644 ui/v2/src/components/list/Pagination.tsx create mode 100644 ui/v2/src/components/performers/PerformerCard.tsx create mode 100644 ui/v2/src/components/performers/PerformerDetails/Performer.tsx create mode 100644 ui/v2/src/components/performers/PerformerList.tsx create mode 100644 ui/v2/src/components/performers/performers.tsx create mode 100644 ui/v2/src/components/scenes/SceneCard.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/Scene.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/SceneDetailPanel.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/SceneFileInfoPanel.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx create mode 100644 ui/v2/src/components/scenes/SceneDetails/ScenePerformerPanel.tsx create mode 100644 ui/v2/src/components/scenes/SceneList.tsx create mode 100644 ui/v2/src/components/scenes/SceneMarkerList.tsx create mode 100644 ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx create mode 100644 ui/v2/src/components/scenes/ScenePlayer/ScenePlayerScrubber.scss create mode 100644 ui/v2/src/components/scenes/ScenePlayer/ScenePlayerScrubber.tsx create mode 100644 ui/v2/src/components/scenes/helpers.tsx create mode 100644 ui/v2/src/components/scenes/scenes.tsx create mode 100644 ui/v2/src/components/select/FilterMultiSelect.tsx create mode 100644 ui/v2/src/components/select/FilterSelect.tsx create mode 100644 ui/v2/src/components/select/FreeOnesPerformerSuggest.tsx create mode 100644 ui/v2/src/components/select/MarkerTitleSuggest.tsx create mode 100644 ui/v2/src/components/select/ValidGalleriesSelect.tsx create mode 100644 ui/v2/src/core/StashService.ts create mode 100644 ui/v2/src/core/generated-graphql.tsx create mode 100644 ui/v2/src/hooks/ListHook.tsx create mode 100644 ui/v2/src/hooks/VideoHover.ts create mode 100755 ui/v2/src/index.scss create mode 100755 ui/v2/src/index.tsx create mode 100644 ui/v2/src/models/base-props.ts create mode 100644 ui/v2/src/models/index.ts create mode 100644 ui/v2/src/models/list-filter/criteria/criterion.ts create mode 100644 ui/v2/src/models/list-filter/criteria/favorite.ts create mode 100644 ui/v2/src/models/list-filter/criteria/has-markers.ts create mode 100644 ui/v2/src/models/list-filter/criteria/is-missing.ts create mode 100644 ui/v2/src/models/list-filter/criteria/none.ts create mode 100644 ui/v2/src/models/list-filter/criteria/performers.ts create mode 100644 ui/v2/src/models/list-filter/criteria/rating.ts create mode 100644 ui/v2/src/models/list-filter/criteria/resolution.ts create mode 100644 ui/v2/src/models/list-filter/criteria/studios.ts create mode 100644 ui/v2/src/models/list-filter/criteria/tags.ts create mode 100644 ui/v2/src/models/list-filter/criteria/utils.ts create mode 100644 ui/v2/src/models/list-filter/filter.ts create mode 100644 ui/v2/src/models/list-filter/types.ts create mode 100644 ui/v2/src/models/react-images.d.ts create mode 100644 ui/v2/src/models/react-jw-player.d.ts create mode 100644 ui/v2/src/models/types.ts create mode 100644 ui/v2/src/react-app-env.d.ts create mode 100755 ui/v2/src/serviceWorker.ts create mode 100644 ui/v2/src/styles/_blueprint-overrides.scss create mode 100644 ui/v2/src/styles/_scrollbars.scss create mode 100644 ui/v2/src/styles/form/_grid.scss create mode 100644 ui/v2/src/styles/shared/_details.scss create mode 100644 ui/v2/src/utils/color.ts create mode 100644 ui/v2/src/utils/errors.ts create mode 100644 ui/v2/src/utils/navigation.ts create mode 100644 ui/v2/src/utils/table.tsx create mode 100644 ui/v2/src/utils/text.ts create mode 100644 ui/v2/src/utils/toasts.ts create mode 100644 ui/v2/tsconfig.json create mode 100644 ui/v2/tslint.json create mode 100755 ui/v2/yarn.lock diff --git a/pkg/models/model_gallery.go b/pkg/models/model_gallery.go index 078e2a5df..76d494185 100644 --- a/pkg/models/model_gallery.go +++ b/pkg/models/model_gallery.go @@ -58,7 +58,7 @@ func (g *Gallery) GetThumbnail(index int) []byte { if err != nil { return data } - resizedImage := imaging.Resize(srcImage, 512, 0, imaging.Lanczos) + resizedImage := imaging.Resize(srcImage, 100, 0, imaging.NearestNeighbor) buf := new(bytes.Buffer) err = jpeg.Encode(buf, resizedImage, nil) if err != nil { diff --git a/schema/documents/data/studio-slim.graphql b/schema/documents/data/studio-slim.graphql new file mode 100644 index 000000000..0ce2ec675 --- /dev/null +++ b/schema/documents/data/studio-slim.graphql @@ -0,0 +1,5 @@ +fragment SlimStudioData on Studio { + id + name + image_path +} \ No newline at end of file diff --git a/schema/documents/documents.graphql b/schema/documents/documents.graphql index 4fee08df4..46df061b0 100644 --- a/schema/documents/documents.graphql +++ b/schema/documents/documents.graphql @@ -25,34 +25,6 @@ query FindScene($id: ID!, $checksum: String) { } } -query FindSceneForEditing($id: ID) { - findScene(id: $id) { - ...SceneData - } - - allPerformers { - id - name - birthdate - image_path - } - - allTags { - id - name - } - - allStudios { - id - name - } - - validGalleriesForScene(scene_id: $id) { - id - path - } -} - query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) { findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) { count @@ -169,6 +141,12 @@ query AllPerformersForFilter { } } +query AllStudiosForFilter { + allStudios { + ...SlimStudioData + } +} + query AllTagsForFilter { allTags { id @@ -176,6 +154,13 @@ query AllTagsForFilter { } } +query ValidGalleriesForScene($scene_id: ID!) { + validGalleriesForScene(scene_id: $scene_id) { + id + path + } +} + query Stats { stats { scene_count, diff --git a/ui/v2/.env b/ui/v2/.env new file mode 100644 index 000000000..5f323191f --- /dev/null +++ b/ui/v2/.env @@ -0,0 +1 @@ +BROWSER=none \ No newline at end of file diff --git a/ui/v2/.gitignore b/ui/v2/.gitignore new file mode 100755 index 000000000..4d29575de --- /dev/null +++ b/ui/v2/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/ui/v2/.vscode/launch.json b/ui/v2/.vscode/launch.json new file mode 100644 index 000000000..a0b21cbef --- /dev/null +++ b/ui/v2/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Chrome", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}/src", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + } + } + ] +} \ No newline at end of file diff --git a/ui/v2/.vscode/settings.json b/ui/v2/.vscode/settings.json new file mode 100644 index 000000000..ba34e28f0 --- /dev/null +++ b/ui/v2/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "editor.tabSize": 2, + "editor.renderWhitespace": "boundary", + "editor.wordWrap": "bounded", + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative", + "editor.wordWrapColumn": 120, + "editor.rulers": [120] +} \ No newline at end of file diff --git a/ui/v2/README.md b/ui/v2/README.md new file mode 100755 index 000000000..60f58b443 --- /dev/null +++ b/ui/v2/README.md @@ -0,0 +1,47 @@ +* Install gulp `yarn global add gulp` + + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/ui/v2/codegen.yml b/ui/v2/codegen.yml new file mode 100644 index 000000000..c7f185d97 --- /dev/null +++ b/ui/v2/codegen.yml @@ -0,0 +1,17 @@ +overwrite: true +schema: "../../schema/schema.graphql" +documents: "../../schema/documents/**/*.graphql" +generates: + src/core/generated-graphql.tsx: + config: + noNamespaces: true + optionalType: "undefined" + noHOC: true + noComponents: true + withHooks: true + plugins: + - add: "/* tslint:disable */" + - time + - "typescript-common" + - "typescript-client" + - "typescript-react-apollo" diff --git a/ui/v2/package.json b/ui/v2/package.json new file mode 100644 index 000000000..38f437392 --- /dev/null +++ b/ui/v2/package.json @@ -0,0 +1,62 @@ +{ + "name": "stash", + "version": "0.1.0", + "private": true, + "dependencies": { + "@blueprintjs/core": "3.13.0", + "@blueprintjs/select": "3.6.1", + "@types/jest": "24.0.5", + "@types/lodash": "4.14.121", + "@types/node": "11.9.4", + "@types/query-string": "6.2.0", + "@types/react": "16.8.3", + "@types/react-dom": "16.8.1", + "@types/react-router-dom": "4.3.1", + "apollo-boost": "0.1.28", + "axios": "0.18.0", + "bulma": "0.7.4", + "formik": "1.5.1", + "graphql": "14.1.1", + "lodash": "4.17.11", + "node-sass": "4.11.0", + "query-string": "6.2.0", + "react": "16.8.3", + "react-apollo": "2.4.1", + "react-apollo-hooks": "0.4.1", + "react-dom": "16.8.3", + "react-images": "0.5.19", + "react-jw-player": "1.19.0", + "react-photo-gallery": "6.3.2", + "react-router-dom": "4.3.1", + "react-scripts": "2.1.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "lint": "tslint -c ./tslint.json 'src/**/*.{ts,tsx}'", + "lint:fix": "tslint --fix -c ./tslint.json 'src/**/*.{ts,tsx}'", + "gqlgen": "gql-gen --config codegen.yml" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "graphql-code-generator": "0.18.0", + "graphql-codegen-add": "0.18.0", + "graphql-codegen-typescript-client": "0.18.0", + "graphql-codegen-typescript-common": "0.18.0", + "graphql-codegen-typescript-react-apollo": "0.18.0", + "graphql-codegen-time": "0.18.0", + "tslint": "5.13.0", + "tslint-react": "3.6.0", + "typescript": "3.3.3" + } +} \ No newline at end of file diff --git a/ui/v2/public/favicon.ico b/ui/v2/public/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/ui/v2/public/index.html b/ui/v2/public/index.html new file mode 100755 index 000000000..75980d58e --- /dev/null +++ b/ui/v2/public/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/ui/v2/public/manifest.json b/ui/v2/public/manifest.json new file mode 100755 index 000000000..1f2f141fa --- /dev/null +++ b/ui/v2/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/ui/v2/src/App.tsx b/ui/v2/src/App.tsx new file mode 100755 index 000000000..d4422d9af --- /dev/null +++ b/ui/v2/src/App.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import { ErrorBoundary } from "./components/ErrorBoundary"; +import Galleries from "./components/Galleries/Galleries"; +import { MainNavbar } from "./components/MainNavbar"; +import { PageNotFound } from "./components/PageNotFound"; +import Performers from "./components/performers/performers"; +import Scenes from "./components/scenes/scenes"; +import { Settings } from "./components/Settings/Settings"; +import { Stats } from "./components/Stats"; +import Studios from "./components/Studios/Studios"; +import Tags from "./components/Tags/Tags"; + +export class App extends React.Component { + public render() { + return ( +
+ + + + + + {/* */} + + + + + + + + +
+ ); + } +} diff --git a/ui/v2/src/components/ErrorBoundary.tsx b/ui/v2/src/components/ErrorBoundary.tsx new file mode 100644 index 000000000..eb47f4547 --- /dev/null +++ b/ui/v2/src/components/ErrorBoundary.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +export class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props); + this.state = { error: null, errorInfo: null }; + } + + public componentDidCatch(error: any, errorInfo: any) { + this.setState({ + error, + errorInfo, + }); + } + + public render() { + if (this.state.errorInfo) { + // Error path + return ( +
+

Something went wrong.

+
+ {this.state.error && this.state.error.toString()} +
+ {this.state.errorInfo.componentStack} +
+
+ ); + } + + // Normally, just render children + return this.props.children; + } +} diff --git a/ui/v2/src/components/Galleries/Galleries.tsx b/ui/v2/src/components/Galleries/Galleries.tsx new file mode 100644 index 000000000..51db624b5 --- /dev/null +++ b/ui/v2/src/components/Galleries/Galleries.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import { Gallery } from "./Gallery"; +import { GalleryList } from "./GalleryList"; + +const Galleries = () => ( + + + + +); + +export default Galleries; diff --git a/ui/v2/src/components/Galleries/Gallery.tsx b/ui/v2/src/components/Galleries/Gallery.tsx new file mode 100644 index 000000000..f5ea014e3 --- /dev/null +++ b/ui/v2/src/components/Galleries/Gallery.tsx @@ -0,0 +1,32 @@ +import { + Spinner, +} from "@blueprintjs/core"; +import _ from "lodash"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import * as GQL from "../../core/generated-graphql"; +import { StashService } from "../../core/StashService"; +import { IBaseProps } from "../../models"; +import { GalleryViewer } from "./GalleryViewer"; + +interface IProps extends IBaseProps {} + +export const Gallery: FunctionComponent = (props: IProps) => { + const [gallery, setGallery] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + + const { data, error, loading } = StashService.useFindGallery(props.match.params.id); + + useEffect(() => { + setIsLoading(loading); + if (!data || !data.findGallery || !!error) { return; } + setGallery(data.findGallery); + }, [data]); + + if (!data || !data.findGallery || isLoading) { return ; } + if (!!error) { return <>{error.message}; } + return ( +
+ +
+ ); +}; diff --git a/ui/v2/src/components/Galleries/GalleryList.tsx b/ui/v2/src/components/Galleries/GalleryList.tsx new file mode 100644 index 000000000..6ba0f0fae --- /dev/null +++ b/ui/v2/src/components/Galleries/GalleryList.tsx @@ -0,0 +1,54 @@ +import { HTMLTable } from "@blueprintjs/core"; +import _ from "lodash"; +import React, { FunctionComponent } from "react"; +import { QueryHookResult } from "react-apollo-hooks"; +import { Link } from "react-router-dom"; +import { FindGalleriesQuery, FindGalleriesVariables } from "../../core/generated-graphql"; +import { ListHook } from "../../hooks/ListHook"; +import { IBaseProps } from "../../models/base-props"; +import { ListFilterModel } from "../../models/list-filter/filter"; +import { DisplayMode, FilterMode } from "../../models/list-filter/types"; + +interface IProps extends IBaseProps {} + +export const GalleryList: FunctionComponent = (props: IProps) => { + const listData = ListHook.useList({ + filterMode: FilterMode.Galleries, + props, + renderContent, + }); + + function renderContent(result: QueryHookResult, filter: ListFilterModel) { + if (!result.data || !result.data.findGalleries) { return; } + if (filter.displayMode === DisplayMode.Grid) { + return

TODO

; + } else if (filter.displayMode === DisplayMode.List) { + return ( + + + + Preview + Path + + + + {result.data.findGalleries.galleries.map((gallery) => ( + + + + {gallery.files.length > 0 ? : undefined} + + + {gallery.path} + + ))} + + + ); + } else if (filter.displayMode === DisplayMode.Wall) { + return

TODO

; + } + } + + return listData.template; +}; diff --git a/ui/v2/src/components/Galleries/GalleryViewer.tsx b/ui/v2/src/components/Galleries/GalleryViewer.tsx new file mode 100644 index 000000000..5a1e7a3ef --- /dev/null +++ b/ui/v2/src/components/Galleries/GalleryViewer.tsx @@ -0,0 +1,47 @@ +import _ from "lodash"; +import React, { FunctionComponent, useState } from "react"; +import Lightbox from "react-images"; +import Gallery from "react-photo-gallery"; +import * as GQL from "../../core/generated-graphql"; + +interface IProps { + gallery: GQL.GalleryDataFragment; +} + +export const GalleryViewer: FunctionComponent = (props: IProps) => { + const [currentImage, setCurrentImage] = useState(0); + const [lightboxIsOpen, setLightboxIsOpen] = useState(false); + + function openLightbox(event: any, obj: any) { + setCurrentImage(obj.index); + setLightboxIsOpen(true); + } + function closeLightbox() { + setCurrentImage(0); + setLightboxIsOpen(false); + } + function gotoPrevious() { + setCurrentImage(currentImage - 1); + } + function gotoNext() { + setCurrentImage(currentImage + 1); + } + + const photos = props.gallery.files.map((file) => ({src: file.path || "", caption: file.name})); + const thumbs = props.gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1})); + return ( +
+ + window.open(photos[currentImage].src, "_blank")} + width={9999} + /> +
+ ); +}; diff --git a/ui/v2/src/components/MainNavbar.tsx b/ui/v2/src/components/MainNavbar.tsx new file mode 100644 index 000000000..535bd16f4 --- /dev/null +++ b/ui/v2/src/components/MainNavbar.tsx @@ -0,0 +1,112 @@ +import { + Navbar, + NavbarDivider, + NavbarGroup, + NavbarHeading, +} from "@blueprintjs/core"; +import React, { FunctionComponent } from "react"; +import { Link, NavLink } from "react-router-dom"; + +interface IMainNavbarProps {} + +export const MainNavbar: FunctionComponent = (props) => { + let newButtonPath: string | undefined; + let newButtonElement: JSX.Element | undefined; + switch (window.location.pathname) { + case "/performers": { + newButtonPath = "/performers/new"; + break; + } + case "/studios": { + newButtonPath = "/studios/new"; + break; + } + } + if (!!newButtonPath) { + newButtonElement = ( + <> + + New + + + + ); + } + + return ( + +
+ + Stash + + + + Scenes + + + + Markers + + + + Galleries + + + + Performers + + + + Studios + + + + Tags + + + + {newButtonElement} + + +
+
+ ); +}; diff --git a/ui/v2/src/components/PageNotFound.tsx b/ui/v2/src/components/PageNotFound.tsx new file mode 100644 index 000000000..8e7cfb32e --- /dev/null +++ b/ui/v2/src/components/PageNotFound.tsx @@ -0,0 +1,7 @@ +import React, { FunctionComponent } from "react"; + +export const PageNotFound: FunctionComponent = () => { + return ( +

Page not found.

+ ); +}; diff --git a/ui/v2/src/components/Settings/Settings.tsx b/ui/v2/src/components/Settings/Settings.tsx new file mode 100644 index 000000000..2636ace81 --- /dev/null +++ b/ui/v2/src/components/Settings/Settings.tsx @@ -0,0 +1,48 @@ +import { + Card, + Tab, + Tabs, +} from "@blueprintjs/core"; +import queryString from "query-string"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { IBaseProps } from "../../models"; +import { SettingsAboutPanel } from "./SettingsAboutPanel"; +import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel"; +import { SettingsLogsPanel } from "./SettingsLogsPanel"; +import { SettingsTasksPanel } from "./SettingsTasksPanel"; + +interface IProps extends IBaseProps {} + +type TabId = "configuration" | "tasks" | "logs" | "about"; + +export const Settings: FunctionComponent = (props: IProps) => { + const [tabId, setTabId] = useState(getTabId()); + + useEffect(() => { + const location = Object.assign({}, props.history.location); + location.search = queryString.stringify({tab: tabId}, {encode: false}); + props.history.replace(location); + }, [tabId]); + + function getTabId(): TabId { + const queryParams = queryString.parse(props.location.search); + if (!queryParams.tab || typeof queryParams.tab !== "string") { return "tasks"; } + return queryParams.tab as TabId; + } + + return ( + + setTabId(newId as TabId)} + defaultSelectedTabId={getTabId()} + > + } /> + } /> + } /> + } /> + + + ); +}; diff --git a/ui/v2/src/components/Settings/SettingsAboutPanel.tsx b/ui/v2/src/components/Settings/SettingsAboutPanel.tsx new file mode 100644 index 000000000..d2d93ba60 --- /dev/null +++ b/ui/v2/src/components/Settings/SettingsAboutPanel.tsx @@ -0,0 +1,19 @@ +import { + H1, + H4, + H6, + Tag, +} from "@blueprintjs/core"; +import React, { FunctionComponent } from "react"; +import * as GQL from "../../core/generated-graphql"; +import { TextUtils } from "../../utils/text"; + +interface IProps {} + +export const SettingsAboutPanel: FunctionComponent = (props: IProps) => { + return ( + <> + About + + ); +}; diff --git a/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx new file mode 100644 index 000000000..54e7ef427 --- /dev/null +++ b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx @@ -0,0 +1,19 @@ +import { + H1, + H4, + H6, + Tag, +} from "@blueprintjs/core"; +import React, { FunctionComponent } from "react"; +import * as GQL from "../../core/generated-graphql"; +import { TextUtils } from "../../utils/text"; + +interface IProps {} + +export const SettingsConfigurationPanel: FunctionComponent = (props: IProps) => { + return ( + <> + Configuration + + ); +}; diff --git a/ui/v2/src/components/Settings/SettingsLogsPanel.tsx b/ui/v2/src/components/Settings/SettingsLogsPanel.tsx new file mode 100644 index 000000000..5d13bbd9d --- /dev/null +++ b/ui/v2/src/components/Settings/SettingsLogsPanel.tsx @@ -0,0 +1,19 @@ +import { + H1, + H4, + H6, + Tag, +} from "@blueprintjs/core"; +import React, { FunctionComponent } from "react"; +import * as GQL from "../../core/generated-graphql"; +import { TextUtils } from "../../utils/text"; + +interface IProps {} + +export const SettingsLogsPanel: FunctionComponent = (props: IProps) => { + return ( + <> + Logs + + ); +}; diff --git a/ui/v2/src/components/Settings/SettingsTasksPanel.tsx b/ui/v2/src/components/Settings/SettingsTasksPanel.tsx new file mode 100644 index 000000000..a783ab119 --- /dev/null +++ b/ui/v2/src/components/Settings/SettingsTasksPanel.tsx @@ -0,0 +1,95 @@ +import { + Alert, + Button, + Divider, + FormGroup, + H1, + H4, + H6, + InputGroup, + Tag, +} from "@blueprintjs/core"; +import React, { FunctionComponent, useState } from "react"; +import * as GQL from "../../core/generated-graphql"; +import { StashService } from "../../core/StashService"; +import { TextUtils } from "../../utils/text"; + +interface IProps {} + +export const SettingsTasksPanel: FunctionComponent = (props: IProps) => { + const [isImportAlertOpen, setIsImportAlertOpen] = useState(false); + + function onImport() { + setIsImportAlertOpen(false); + StashService.queryMetadataImport(); + } + + function renderImportAlert() { + return ( + setIsImportAlertOpen(false)} + onConfirm={() => onImport()} + > +

+ Are you sure you want to import? This will delete the database and re-import from + your exported metadata. +

+
+ ); + } + + return ( + <> + {renderImportAlert()} + +

Library

+ + + setEditingTag(undefined)} + title={!!editingTag && !!editingTag.id ? "Edit Tag" : "New Tag"} + > +
+ + setName(newValue.target.value)} + value={name} + /> + +
+
+
+ +
+
+
+ + {tagElements} + + ); +}; diff --git a/ui/v2/src/components/Tags/Tags.tsx b/ui/v2/src/components/Tags/Tags.tsx new file mode 100644 index 000000000..9c1f591f9 --- /dev/null +++ b/ui/v2/src/components/Tags/Tags.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import { TagList } from "./TagList"; + +const Tags = () => ( + + + +); + +export default Tags; diff --git a/ui/v2/src/components/Wall/Wall.scss b/ui/v2/src/components/Wall/Wall.scss new file mode 100644 index 000000000..1a87e65ea --- /dev/null +++ b/ui/v2/src/components/Wall/Wall.scss @@ -0,0 +1,98 @@ +.wall-overlay { + background-color: rgba(0,0,0,.8); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + pointer-events: none; + transition: transform .5s ease-in-out; +} +.visible { + opacity: 1; + transition: opacity .5s ease-in-out; +} +.hidden { + opacity: 0; + transition: opacity .5s ease-in-out; +} +.visible-unanimated { + opacity: 1; +} +.hidden-unanimated { + opacity: 0; +} + + +.double-scale { + position: absolute; + z-index: 2; + transform: scale(2); + background-color: black; +} + +.double-scale img { + opacity: 0; +} + +.scene-wall-item-container { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + transition: transform .5s; +} + +.scene-wall-item-container video { + position: absolute; + width: 100%; + height: 100%; + z-index: -1; +} + +.scene-wall-item-text-container { + position: absolute; + font-weight: 700; + color: #444; + padding: 5px; + width: 100%; + bottom: 0; + background: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.65)); + overflow: hidden; + text-align: center; + + & span { + line-height: 1; + font-weight: 400; + font-size: 10px; + margin: 0 3px; + } +} + +.scene-wall-item-blur { + position: absolute; + top: -5px; + left: -5px; + right: -5px; + bottom: -5px; + /*background-color: rgba(255, 255, 255, 0.75);*/ + /*backdrop-filter: blur(5px);*/ + z-index: -1; +} + +.wall.grid-item video, .wall.grid-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.wall.grid-item { + padding: 0 !important; + line-height: 0; + overflow: visible; + position: relative; +} diff --git a/ui/v2/src/components/Wall/WallItem.tsx b/ui/v2/src/components/Wall/WallItem.tsx new file mode 100644 index 000000000..e7e04921e --- /dev/null +++ b/ui/v2/src/components/Wall/WallItem.tsx @@ -0,0 +1,114 @@ +import _ from "lodash"; +import React, { FunctionComponent, useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import * as GQL from "../../core/generated-graphql"; +import { VideoHoverHook } from "../../hooks/VideoHover"; +import { TextUtils } from "../../utils/text"; + +interface IWallItemProps { + scene?: GQL.SlimSceneDataFragment; + sceneMarker?: GQL.SceneMarkerDataFragment; + origin?: string; + onOverlay: (show: boolean) => void; + clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void; +} + +export const WallItem: FunctionComponent = (props: IWallItemProps) => { + const [videoPath, setVideoPath] = useState(undefined); + const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true}); + + function onMouseEnter() { + VideoHoverHook.onMouseEnter(videoHoverHook); + if (!videoPath || videoPath === "") { + if (!!props.sceneMarker) { + setVideoPath(props.sceneMarker.stream || ""); + } else if (!!props.scene) { + setVideoPath(props.scene.paths.preview || ""); + } + } + props.onOverlay(true); + } + const debouncedOnMouseEnter = useRef(_.debounce(onMouseEnter, 500)); + + function onMouseLeave() { + VideoHoverHook.onMouseLeave(videoHoverHook); + setVideoPath(""); + debouncedOnMouseEnter.current.cancel(); + props.onOverlay(false); + } + + function onClick() { + if (props.clickHandler === undefined) { return; } + if (props.scene !== undefined) { + props.clickHandler(props.scene); + } else if (props.sceneMarker !== undefined) { + props.clickHandler(props.sceneMarker); + } + } + + let linkSrc: string = "#"; + if (props.clickHandler === undefined) { + if (props.scene !== undefined) { + linkSrc = `/scenes/${props.scene.id}`; + } else if (props.sceneMarker !== undefined) { + linkSrc = `/scenes/${props.sceneMarker.scene.id}?t=${props.sceneMarker.seconds}`; + } + } + + function onTransitionEnd(event: React.TransitionEvent) { + const target = (event.target as any); + if (target.classList.contains("double-scale")) { + target.parentElement.style.zIndex = 10; + } else { + target.parentElement.style.zIndex = null; + } + } + + let previewSrc: string = ""; + let title: string = ""; + let tags: JSX.Element[] = []; + if (!!props.sceneMarker) { + previewSrc = props.sceneMarker.preview; + title = `${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`; + tags = props.sceneMarker.tags.map((tag) => ({tag.name})); + tags.unshift({props.sceneMarker.primary_tag.name}); + } else if (!!props.scene) { + previewSrc = props.scene.paths.webp || ""; + title = props.scene.title || ""; + // tags = props.scene.tags.map((tag) => ({tag.name})); + } + + const className = ["scene-wall-item-container"]; + if (videoHoverHook.isHovering.current) { className.push("double-scale"); } + const style: React.CSSProperties = {}; + if (!!props.origin) { style.transformOrigin = props.origin; } + return ( +
+
debouncedOnMouseEnter.current()} + onMouseMove={() => debouncedOnMouseEnter.current()} + onMouseLeave={onMouseLeave} + > + onClick()} to={linkSrc}> +
+
+ ); +}; diff --git a/ui/v2/src/components/Wall/WallPanel.tsx b/ui/v2/src/components/Wall/WallPanel.tsx new file mode 100644 index 000000000..df466f727 --- /dev/null +++ b/ui/v2/src/components/Wall/WallPanel.tsx @@ -0,0 +1,89 @@ +import _ from "lodash"; +import React, { FunctionComponent, useState } from "react"; +import * as GQL from "../../core/generated-graphql"; +import "./Wall.scss"; +import { WallItem } from "./WallItem"; + +interface IWallPanelProps { + scenes?: GQL.SlimSceneDataFragment[]; + sceneMarkers?: GQL.SceneMarkerDataFragment[]; + clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void; +} + +export const WallPanel: FunctionComponent = (props: IWallPanelProps) => { + const [showOverlay, setShowOverlay] = useState(false); + + function onOverlay(show: boolean) { + setShowOverlay(show); + } + + function getOrigin(index: number, rowSize: number, total: number): string { + const isAtStart = index % rowSize === 0; + const isAtEnd = index % rowSize === rowSize - 1; + const endRemaining = total % rowSize; + + // First row + if (total === 1) { return "top"; } + if (index === 0) { return "top left"; } + if (index === rowSize - 1 || (total < rowSize && index === total - 1)) { return "top right"; } + if (index < rowSize) { return "top"; } + + // Bottom row + if (isAtEnd && index === total - 1) { return "bottom right"; } + if (isAtStart && index === total - rowSize) { return "bottom left"; } + if (endRemaining !== 0 && index >= total - endRemaining) { return "bottom"; } + if (endRemaining === 0 && index >= total - rowSize) { return "bottom"; } + + // Everything else + if (isAtStart) { return "center left"; } + if (isAtEnd) { return "center right"; } + return "center"; + } + + function maybeRenderScenes() { + if (props.scenes === undefined) { return; } + return props.scenes.map((scene, index) => { + const origin = getOrigin(index, 5, props.scenes!.length); + return ( + + ); + }); + } + + function maybeRenderSceneMarkers() { + if (props.sceneMarkers === undefined) { return; } + return props.sceneMarkers.map((marker, index) => { + const origin = getOrigin(index, 5, props.sceneMarkers!.length); + return ( + + ); + }); + } + + function render() { + const overlayClassName = showOverlay ? "visible" : "hidden"; + return ( + <> +
+
+ {maybeRenderScenes()} + {maybeRenderSceneMarkers()} +
+ + ); + } + + return render(); +}; diff --git a/ui/v2/src/components/list/AddFilter.tsx b/ui/v2/src/components/list/AddFilter.tsx new file mode 100644 index 000000000..4486de6bb --- /dev/null +++ b/ui/v2/src/components/list/AddFilter.tsx @@ -0,0 +1,140 @@ +import { + Button, + Classes, + Dialog, + FormGroup, + HTMLSelect, +} from "@blueprintjs/core"; +import _ from "lodash"; +import React, { FunctionComponent, useEffect, useRef, useState } from "react"; +import { isArray } from "util"; +import { Criterion, CriterionType } from "../../models/list-filter/criteria/criterion"; +import { NoneCriterion } from "../../models/list-filter/criteria/none"; +import { PerformersCriterion } from "../../models/list-filter/criteria/performers"; +import { StudiosCriterion } from "../../models/list-filter/criteria/studios"; +import { TagsCriterion } from "../../models/list-filter/criteria/tags"; +import { makeCriteria } from "../../models/list-filter/criteria/utils"; +import { ListFilterModel } from "../../models/list-filter/filter"; +import { FilterMultiSelect } from "../select/FilterMultiSelect"; + +interface IAddFilterProps { + onAddCriterion: (criterion: Criterion) => void; + onCancel: () => void; + filter: ListFilterModel; + editingCriterion?: Criterion; +} + +export const AddFilter: FunctionComponent = (props: IAddFilterProps) => { + const singleValueSelect = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + const [criterion, setCriterion] = useState>(new NoneCriterion()); + + // Configure if we are editing an existing criterion + useEffect(() => { + if (!props.editingCriterion) { return; } + setIsOpen(true); + setCriterion(props.editingCriterion); + }, [props.editingCriterion]); + + function onChangedCriteriaType(event: React.ChangeEvent) { + const newCriterionType = event.target.value as CriterionType; + const newCriterion = makeCriteria(newCriterionType); + setCriterion(newCriterion); + } + + function onChangedSingleSelect(event: React.ChangeEvent) { + const newCriterion = _.cloneDeep(criterion); + newCriterion.value = event.target.value; + setCriterion(newCriterion); + } + + function onAddFilter() { + if (!isArray(criterion.value) && !!singleValueSelect.current) { + const value = singleValueSelect.current.props.defaultValue; + if (value === undefined || value === "" || typeof value === "number") { criterion.value = criterion.options[0]; } + } + props.onAddCriterion(criterion); + onToggle(); + } + + function onToggle() { + if (isOpen) { + props.onCancel(); + } + setIsOpen(!isOpen); + setCriterion(makeCriteria()); + } + + const maybeRenderFilterPopoverContents = () => { + if (criterion.type === "none") { return; } + + function renderSelect() { + if (isArray(criterion.value)) { + let type: "performers" | "studios" | "tags" | "" = ""; + if (criterion instanceof PerformersCriterion) { + type = "performers"; + } else if (criterion instanceof StudiosCriterion) { + type = "studios"; + } else if (criterion instanceof TagsCriterion) { + type = "tags"; + } + + if (type === "") { + return (<>todo); + } else { + return ( + criterion.value = items.map((i) => ({id: i.id, label: i.name!}))} + openOnKeyDown={true} + initialIds={criterion.value.map((labeled) => labeled.id)} + /> + ); + } + } else { + return ( + + ); + } + } + return {renderSelect()}; + }; + + function maybeRenderFilterSelect() { + if (!!props.editingCriterion) { return; } + return ( + + + + ); + } + + const title = !props.editingCriterion ? "Add Filter" : "Update Filter"; + return ( + <> + + onToggle()} title={title}> +
+ {maybeRenderFilterSelect()} + {maybeRenderFilterPopoverContents()} +
+
+
+ +
+
+
+ + ); +}; diff --git a/ui/v2/src/components/list/ListFilter.tsx b/ui/v2/src/components/list/ListFilter.tsx new file mode 100644 index 000000000..b821322e6 --- /dev/null +++ b/ui/v2/src/components/list/ListFilter.tsx @@ -0,0 +1,188 @@ +import { + AnchorButton, + Button, + ButtonGroup, + ControlGroup, + HTMLSelect, + InputGroup, + Menu, + MenuItem, + Popover, + Tag, +} from "@blueprintjs/core"; +import { debounce } from "lodash"; +import React, { FunctionComponent, SyntheticEvent, useEffect, useRef, useState } from "react"; +import { Criterion } from "../../models/list-filter/criteria/criterion"; +import { ListFilterModel } from "../../models/list-filter/filter"; +import { DisplayMode } from "../../models/list-filter/types"; +import { AddFilter } from "./AddFilter"; + +interface IListFilterProps { + onChangePageSize: (pageSize: number) => void; + onChangeQuery: (query: string) => void; + onChangeSortDirection: (sortDirection: "asc" | "desc") => void; + onChangeSortBy: (sortBy: string) => void; + onChangeDisplayMode: (displayMode: DisplayMode) => void; + onAddCriterion: (criterion: Criterion) => void; + onRemoveCriterion: (criterion: Criterion) => void; + filter: ListFilterModel; +} + +const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"]; + +export const ListFilter: FunctionComponent = (props: IListFilterProps) => { + let searchCallback: any; + + const [editingCriterion, setEditingCriterion] = useState(undefined); + + useEffect(() => { + searchCallback = debounce((event) => { + props.onChangeQuery(event.target.value); + }, 500); + }); + + function onChangePageSize(event: SyntheticEvent) { + const val = event!.currentTarget!.value; + props.onChangePageSize(parseInt(val, 10)); + } + + function onChangeQuery(event: SyntheticEvent) { + event.persist(); + searchCallback(event); + } + + function onChangeSortDirection(_: any) { + if (props.filter.sortDirection === "asc") { + props.onChangeSortDirection("desc"); + } else { + props.onChangeSortDirection("asc"); + } + } + + function onChangeSortBy(event: React.MouseEvent) { + props.onChangeSortBy(event.currentTarget.text); + } + + function onChangeDisplayMode(displayMode: DisplayMode) { + props.onChangeDisplayMode(displayMode); + } + + function onAddCriterion(criterion: Criterion) { + props.onAddCriterion(criterion); + } + + function onCancelAddCriterion() { + setEditingCriterion(undefined); + } + + let removedCriterionId = ""; + function onRemoveCriterionTag(criterion?: Criterion) { + if (!criterion) { return; } + setEditingCriterion(undefined); + removedCriterionId = criterion.getId(); + props.onRemoveCriterion(criterion); + } + function onClickCriterionTag(criterion?: Criterion) { + if (!criterion || removedCriterionId !== "") { return; } + setEditingCriterion(criterion); + } + + function renderSortByOptions() { + return props.filter.sortByOptions.map((option) => ( + + )); + } + + function renderDisplayModeOptions() { + function getIcon(option: DisplayMode) { + switch (option) { + case DisplayMode.Grid: return "grid-view"; + case DisplayMode.List: return "list"; + case DisplayMode.Wall: return "symbol-square"; + } + } + function getLabel(option: DisplayMode) { + switch (option) { + case DisplayMode.Grid: return "Grid"; + case DisplayMode.List: return "List"; + case DisplayMode.Wall: return "Wall"; + } + } + return props.filter.displayModeOptions.map((option) => ( + + {renderSortByOptions()} + + + + + + + {renderDisplayModeOptions()} + +
+
+ {renderFilterTags()} +
+ + ); + } + + return render(); +}; diff --git a/ui/v2/src/components/list/Pagination.tsx b/ui/v2/src/components/list/Pagination.tsx new file mode 100644 index 000000000..057e9c0a4 --- /dev/null +++ b/ui/v2/src/components/list/Pagination.tsx @@ -0,0 +1,119 @@ +import { Button, ButtonGroup } from "@blueprintjs/core"; +import React from "react"; + +interface IPaginationProps { + itemsPerPage: number; + currentPage: number; + totalItems: number; + onChangePage: (page: number) => void; +} + +interface IPaginationState { + pages: number[]; + totalPages: number; +} + +export class Pagination extends React.Component { + constructor(props: IPaginationProps) { + super(props); + this.state = { + pages: [], + totalPages: Number.MAX_SAFE_INTEGER, + }; + } + + public componentWillMount() { + this.setPage(this.props.currentPage, false); + } + + public componentDidUpdate(prevProps: IPaginationProps) { + if (this.props.totalItems !== prevProps.totalItems || this.props.itemsPerPage !== prevProps.itemsPerPage) { + this.setPage(this.props.currentPage); + } + } + + public render() { + if (!this.state || !this.state.pages || this.state.pages.length <= 1) { return null; } + + return ( + + + + + + ); + } + + return ( + <> + {renderScraperDialog()} +
+
+ +
+
+ { setIsEditing(!isEditing); updatePerformerEditState(performer); }} + onSave={onSave} + onImageChange={onImageChange} + onDisplayFreeOnesDialog={onDisplayFreeOnesDialog} + /> +

+ setName(value)} + /> +

+
+ Aliases: + setAliases(value)} + /> +
+
+ Favorite: +
+ + + + {TableUtils.renderEditableTextTableRow( + {title: "Birthdate (YYYY-MM-DD)", value: birthdate, isEditing, onChange: setBirthdate})} + {renderEthnicity()} + {TableUtils.renderEditableTextTableRow( + {title: "Eye Color", value: eyeColor, isEditing, onChange: setEyeColor})} + {TableUtils.renderEditableTextTableRow( + {title: "Country", value: country, isEditing, onChange: setCountry})} + {TableUtils.renderEditableTextTableRow( + {title: "Height (CM)", value: height, isEditing, onChange: setHeight})} + {TableUtils.renderEditableTextTableRow( + {title: "Measurements", value: measurements, isEditing, onChange: setMeasurements})} + {TableUtils.renderEditableTextTableRow( + {title: "Fake Tits", value: fakeTits, isEditing, onChange: setFakeTits})} + {TableUtils.renderEditableTextTableRow( + {title: "Career Length", value: careerLength, isEditing, onChange: setCareerLength})} + {TableUtils.renderEditableTextTableRow( + {title: "Tattoos", value: tattoos, isEditing, onChange: setTattoos})} + {TableUtils.renderEditableTextTableRow( + {title: "Piercings", value: piercings, isEditing, onChange: setPiercings})} + {TableUtils.renderEditableTextTableRow( + {title: "URL", value: url, isEditing, onChange: setUrl})} + {TableUtils.renderEditableTextTableRow( + {title: "Twitter", value: twitter, isEditing, onChange: setTwitter})} + {TableUtils.renderEditableTextTableRow( + {title: "Instagram", value: instagram, isEditing, onChange: setInstagram})} + + +
+
+ + ); +}; diff --git a/ui/v2/src/components/performers/PerformerList.tsx b/ui/v2/src/components/performers/PerformerList.tsx new file mode 100644 index 000000000..965aeb1cb --- /dev/null +++ b/ui/v2/src/components/performers/PerformerList.tsx @@ -0,0 +1,37 @@ +import _ from "lodash"; +import React, { FunctionComponent } from "react"; +import { QueryHookResult } from "react-apollo-hooks"; +import { FindPerformersQuery, FindPerformersVariables } from "../../core/generated-graphql"; +import { ListHook } from "../../hooks/ListHook"; +import { IBaseProps } from "../../models/base-props"; +import { ListFilterModel } from "../../models/list-filter/filter"; +import { DisplayMode, FilterMode } from "../../models/list-filter/types"; +import { PerformerCard } from "./PerformerCard"; + +interface IPerformerListProps extends IBaseProps {} + +export const PerformerList: FunctionComponent = (props: IPerformerListProps) => { + const listData = ListHook.useList({ + filterMode: FilterMode.Performers, + props, + renderContent, + }); + + function renderContent( + result: QueryHookResult, filter: ListFilterModel) { + if (!result.data || !result.data.findPerformers) { return; } + if (filter.displayMode === DisplayMode.Grid) { + return ( +
+ {result.data.findPerformers.performers.map((p) => ())} +
+ ); + } else if (filter.displayMode === DisplayMode.List) { + return

TODO

; + } else if (filter.displayMode === DisplayMode.Wall) { + return; + } + } + + return listData.template; +}; diff --git a/ui/v2/src/components/performers/performers.tsx b/ui/v2/src/components/performers/performers.tsx new file mode 100644 index 000000000..e6a2347c8 --- /dev/null +++ b/ui/v2/src/components/performers/performers.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import { Performer } from "./PerformerDetails/Performer"; +import { PerformerList } from "./PerformerList"; + +const Performers = () => ( + + + + +); + +export default Performers; diff --git a/ui/v2/src/components/scenes/SceneCard.tsx b/ui/v2/src/components/scenes/SceneCard.tsx new file mode 100644 index 000000000..90102b582 --- /dev/null +++ b/ui/v2/src/components/scenes/SceneCard.tsx @@ -0,0 +1,149 @@ +import { + Button, + ButtonGroup, + Card, + Divider, + Elevation, + H4, + Popover, + Tag, +} from "@blueprintjs/core"; +import React, { FunctionComponent, RefObject, useEffect, useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import * as GQL from "../../core/generated-graphql"; +import { VideoHoverHook } from "../../hooks/VideoHover"; +import { ColorUtils } from "../../utils/color"; +import { TextUtils } from "../../utils/text"; +import { SceneHelpers } from "./helpers"; + +interface ISceneCardProps { + scene: GQL.SlimSceneDataFragment; +} + +export const SceneCard: FunctionComponent = (props: ISceneCardProps) => { + const [previewPath, setPreviewPath] = useState(undefined); + const videoHoverHook = VideoHoverHook.useVideoHover(); + + function maybeRenderRatingBanner() { + if (!props.scene.rating) { return; } + return ( +
+ RATING: {props.scene.rating} +
+ ); + } + + function maybeRenderTagPopoverButton() { + if (props.scene.tags.length <= 0) { return; } + + const tags = props.scene.tags.map((tag) => ( + {tag.name} + )); + return ( + +