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 000000000..a11777cc4
Binary files /dev/null and b/ui/v2/public/favicon.ico differ
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
+
+
+
+
+ Generated Content
+
+
+
+
+
+
+ Metadata
+
+
+
+
+
+ >
+ );
+};
diff --git a/ui/v2/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2/src/components/Shared/DetailsEditNavbar.tsx
new file mode 100644
index 000000000..47555cd50
--- /dev/null
+++ b/ui/v2/src/components/Shared/DetailsEditNavbar.tsx
@@ -0,0 +1,97 @@
+import {
+ Button,
+ FileInput,
+ Menu,
+ MenuItem,
+ Navbar,
+ NavbarDivider,
+ Popover,
+} from "@blueprintjs/core";
+import _ from "lodash";
+import React, { FunctionComponent } from "react";
+import { Link } from "react-router-dom";
+import * as GQL from "../../core/generated-graphql";
+import { NavigationUtils } from "../../utils/navigation";
+
+interface IProps {
+ performer?: Partial;
+ studio?: Partial;
+ isNew: boolean;
+ isEditing: boolean;
+ onToggleEdit: () => void;
+ onSave: () => void;
+ onImageChange: (event: React.FormEvent) => void;
+
+ // TODO: only for performers. make generic
+ onDisplayFreeOnesDialog?: () => void;
+}
+
+export const DetailsEditNavbar: FunctionComponent = (props: IProps) => {
+ function renderEditButton() {
+ if (props.isNew) { return; }
+ return (
+