mirror of https://github.com/stashapp/stash.git
Scene ui improvements (#232)
* Move duration and resolution to overlay * Improve display of portrait videos * Condense filter controls * Add performer images to scene tags * Add studio overlay to scene cards * Fade out scene overlays on hover * CSS grid tweaks * Align overlay to bottom of video preview * Fix opacity value * Fix performer thumbnails * Show studio overlay on mouseover * Correct display colour for display mode buttons * Add scene zoom slider * Add show studio as text option * Move select all/none to more button
This commit is contained in:
parent
c14153ab5a
commit
12c7faab4e
|
@ -17,6 +17,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||
wallShowTitle
|
||||
maximumLoopDuration
|
||||
autostartVideo
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
}
|
||||
|
|
|
@ -66,6 +66,8 @@ input ConfigInterfaceInput {
|
|||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
autostartVideo: Boolean
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
showStudioAsText: Boolean
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
@ -80,6 +82,8 @@ type ConfigInterfaceResult {
|
|||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
autostartVideo: Boolean
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
showStudioAsText: Boolean
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
|
|
@ -98,6 +98,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
|||
config.Set(config.AutostartVideo, *input.AutostartVideo)
|
||||
}
|
||||
|
||||
if input.ShowStudioAsText != nil {
|
||||
config.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
}
|
||||
|
||||
css := ""
|
||||
|
||||
if input.CSS != nil {
|
||||
|
|
|
@ -53,6 +53,7 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
|||
wallShowTitle := config.GetWallShowTitle()
|
||||
maximumLoopDuration := config.GetMaximumLoopDuration()
|
||||
autostartVideo := config.GetAutostartVideo()
|
||||
showStudioAsText := config.GetShowStudioAsText()
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
|
||||
|
@ -61,6 +62,7 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
|||
WallShowTitle: &wallShowTitle,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ const SoundOnPreview = "sound_on_preview"
|
|||
const WallShowTitle = "wall_show_title"
|
||||
const MaximumLoopDuration = "maximum_loop_duration"
|
||||
const AutostartVideo = "autostart_video"
|
||||
const ShowStudioAsText = "show_studio_as_text"
|
||||
const CSSEnabled = "cssEnabled"
|
||||
|
||||
// Logging options
|
||||
|
@ -191,6 +192,11 @@ func GetAutostartVideo() bool {
|
|||
return viper.GetBool(AutostartVideo)
|
||||
}
|
||||
|
||||
func GetShowStudioAsText() bool {
|
||||
viper.SetDefault(ShowStudioAsText, false)
|
||||
return viper.GetBool(ShowStudioAsText)
|
||||
}
|
||||
|
||||
func GetCSSPath() string {
|
||||
// use custom.css in the same directory as the config file
|
||||
configFileUsed := viper.ConfigFileUsed()
|
||||
|
|
|
@ -22,6 +22,7 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||
const [wallShowTitle, setWallShowTitle] = useState<boolean>();
|
||||
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
|
||||
const [autostartVideo, setAutostartVideo] = useState<boolean>();
|
||||
const [showStudioAsText, setShowStudioAsText] = useState<boolean>();
|
||||
const [css, setCSS] = useState<string>();
|
||||
const [cssEnabled, setCSSEnabled] = useState<boolean>();
|
||||
|
||||
|
@ -30,6 +31,7 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||
wallShowTitle,
|
||||
maximumLoopDuration,
|
||||
autostartVideo,
|
||||
showStudioAsText,
|
||||
css,
|
||||
cssEnabled
|
||||
});
|
||||
|
@ -42,6 +44,7 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||
setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true);
|
||||
setMaximumLoopDuration(iCfg.maximumLoopDuration || 0);
|
||||
setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false);
|
||||
setShowStudioAsText(iCfg.showStudioAsText !== undefined ? iCfg.showStudioAsText : false);
|
||||
setCSS(config.data.configuration.interface.css || "");
|
||||
setCSSEnabled(config.data.configuration.interface.cssEnabled || false);
|
||||
}
|
||||
|
@ -78,6 +81,18 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label="Scene List"
|
||||
>
|
||||
<Checkbox
|
||||
checked={showStudioAsText}
|
||||
label="Show Studios as text"
|
||||
onChange={() => {
|
||||
setShowStudioAsText(!showStudioAsText)
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label="Scene Player"
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
FormGroup,
|
||||
HTMLSelect,
|
||||
InputGroup,
|
||||
Tooltip,
|
||||
} from "@blueprintjs/core";
|
||||
import _ from "lodash";
|
||||
import React, { FunctionComponent, useEffect, useRef, useState } from "react";
|
||||
|
@ -188,7 +189,19 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
|
|||
const title = !props.editingCriterion ? "Add Filter" : "Update Filter";
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => onToggle()} active={isOpen} large={true}>Filter</Button>
|
||||
<Tooltip
|
||||
hoverOpenDelay={200}
|
||||
content="Filter"
|
||||
>
|
||||
<Button
|
||||
icon="filter"
|
||||
onClick={() => onToggle()}
|
||||
active={isOpen}
|
||||
large={true}
|
||||
>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Dialog isOpen={isOpen} onClose={() => onToggle()} title={title}>
|
||||
<div className="dialog-content">
|
||||
{maybeRenderFilterSelect()}
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
MenuItem,
|
||||
Popover,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Slider,
|
||||
} from "@blueprintjs/core";
|
||||
import { debounce } from "lodash";
|
||||
import React, { FunctionComponent, SyntheticEvent, useEffect, useRef, useState } from "react";
|
||||
|
@ -25,6 +27,8 @@ interface IListFilterProps {
|
|||
onChangeDisplayMode: (displayMode: DisplayMode) => void;
|
||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||
onRemoveCriterion: (criterion: Criterion) => void;
|
||||
zoomIndex?: number;
|
||||
onChangeZoom?: (zoomIndex: number) => void;
|
||||
onSelectAll?: () => void;
|
||||
onSelectNone?: () => void;
|
||||
filter: ListFilterModel;
|
||||
|
@ -111,13 +115,14 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
|||
}
|
||||
}
|
||||
return props.filter.displayModeOptions.map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
active={props.filter.displayMode === option}
|
||||
onClick={() => onChangeDisplayMode(option)}
|
||||
icon={getIcon(option)}
|
||||
text={getLabel(option)}
|
||||
/>
|
||||
<Tooltip content={getLabel(option)} hoverOpenDelay={200}>
|
||||
<Button
|
||||
key={option}
|
||||
active={props.filter.displayMode === option}
|
||||
onClick={() => onChangeDisplayMode(option)}
|
||||
icon={getIcon(option)}
|
||||
/>
|
||||
</Tooltip>
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -150,23 +155,63 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
|||
|
||||
function renderSelectAll() {
|
||||
if (props.onSelectAll) {
|
||||
return <Button onClick={() => onSelectAll()} text="Select All"/>;
|
||||
return <MenuItem onClick={() => onSelectAll()} text="Select All" />;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectNone() {
|
||||
if (props.onSelectNone) {
|
||||
return <Button onClick={() => onSelectNone()} text="Select None"/>;
|
||||
return <MenuItem onClick={() => onSelectNone()} text="Select None" />;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectAllNone() {
|
||||
return (
|
||||
<>
|
||||
{renderSelectAll()}
|
||||
{renderSelectNone()}
|
||||
</>
|
||||
);
|
||||
function renderMore() {
|
||||
let options = [];
|
||||
options.push(renderSelectAll());
|
||||
options.push(renderSelectNone());
|
||||
options = options.filter((o) => !!o);
|
||||
|
||||
let menuItems = options as JSX.Element[];
|
||||
|
||||
function renderMoreOptions() {
|
||||
return (
|
||||
<>
|
||||
{menuItems}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (menuItems.length > 0) {
|
||||
return (
|
||||
<Popover position="bottom">
|
||||
<Button icon="more"/>
|
||||
<Menu>{renderMoreOptions()}</Menu>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeZoom(v : number) {
|
||||
if (props.onChangeZoom) {
|
||||
props.onChangeZoom(v);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderZoom() {
|
||||
if (props.onChangeZoom) {
|
||||
return (
|
||||
<span className="zoom-slider">
|
||||
<Slider
|
||||
min={0}
|
||||
value={props.zoomIndex}
|
||||
initialValue={props.zoomIndex}
|
||||
max={3}
|
||||
labelRenderer={false}
|
||||
onChange={(v) => onChangeZoom(v)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
|
@ -188,18 +233,23 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
|||
value={props.filter.itemsPerPage}
|
||||
className="filter-item"
|
||||
/>
|
||||
<ControlGroup className="filter-item">
|
||||
<AnchorButton
|
||||
rightIcon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"}
|
||||
onClick={onChangeSortDirection}
|
||||
>
|
||||
{props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}
|
||||
</AnchorButton>
|
||||
<ButtonGroup className="filter-item">
|
||||
<Popover position="bottom">
|
||||
<Button large={true}>{props.filter.sortBy}</Button>
|
||||
<Menu>{renderSortByOptions()}</Menu>
|
||||
</Popover>
|
||||
</ControlGroup>
|
||||
|
||||
<Tooltip
|
||||
content={props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}
|
||||
hoverOpenDelay={200}
|
||||
>
|
||||
<Button
|
||||
rightIcon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"}
|
||||
onClick={onChangeSortDirection}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
</ButtonGroup>
|
||||
|
||||
<AddFilter
|
||||
filter={props.filter}
|
||||
|
@ -212,8 +262,10 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
|
|||
{renderDisplayModeOptions()}
|
||||
</ButtonGroup>
|
||||
|
||||
{maybeRenderZoom()}
|
||||
|
||||
<ButtonGroup className="filter-item">
|
||||
{renderSelectAllNone()}
|
||||
{renderMore()}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div style={{display: "flex", justifyContent: "center", margin: "10px auto"}}>
|
||||
|
|
|
@ -17,10 +17,13 @@ import { ColorUtils } from "../../utils/color";
|
|||
import { TextUtils } from "../../utils/text";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { SceneHelpers } from "./helpers";
|
||||
import { ZoomUtils } from "../../utils/zoom";
|
||||
import { StashService } from "../../core/StashService";
|
||||
|
||||
interface ISceneCardProps {
|
||||
scene: GQL.SlimSceneDataFragment;
|
||||
selected: boolean | undefined;
|
||||
zoomIndex: number;
|
||||
onSelectedChanged: (selected : boolean, shiftKey : boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -28,6 +31,8 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
||||
|
||||
const config = StashService.useConfiguration();
|
||||
const showStudioAsText = !!config.data && !!config.data.configuration ? config.data.configuration.interface.showStudioAsText : false;
|
||||
|
||||
function maybeRenderRatingBanner() {
|
||||
if (!props.scene.rating) { return; }
|
||||
|
@ -38,6 +43,43 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
);
|
||||
}
|
||||
|
||||
function maybeRenderSceneSpecsOverlay() {
|
||||
return (
|
||||
<div className={`scene-specs-overlay`}>
|
||||
{!!props.scene.file.height ? <span className={`overlay-resolution`}> {TextUtils.resolution(props.scene.file.height)}</span> : undefined}
|
||||
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderSceneStudioOverlay() {
|
||||
if (!props.scene.studio) {
|
||||
return;
|
||||
}
|
||||
|
||||
let style: React.CSSProperties = {
|
||||
backgroundImage: `url('${props.scene.studio.image_path}')`,
|
||||
};
|
||||
|
||||
let text = "";
|
||||
|
||||
if (showStudioAsText) {
|
||||
style = {};
|
||||
text = props.scene.studio.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`scene-studio-overlay`}>
|
||||
<Link
|
||||
to={`/studios/${props.scene.studio.id}`}
|
||||
style={style}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderTagPopoverButton() {
|
||||
if (props.scene.tags.length <= 0) { return; }
|
||||
|
||||
|
@ -58,9 +100,20 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
function maybeRenderPerformerPopoverButton() {
|
||||
if (props.scene.performers.length <= 0) { return; }
|
||||
|
||||
const performers = props.scene.performers.map((performer) => (
|
||||
<TagLink key={performer.id} performer={performer} />
|
||||
));
|
||||
const performers = props.scene.performers.map((performer) => {
|
||||
return (
|
||||
<>
|
||||
<div className="performer-tag-container">
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag previewable image"
|
||||
style={{backgroundImage: `url(${performer.image_path})`}}
|
||||
></Link>
|
||||
<TagLink key={performer.id} performer={performer} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Popover interactionKind={"hover"} position="bottom">
|
||||
<Button
|
||||
|
@ -119,11 +172,38 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
setPreviewPath("");
|
||||
}
|
||||
|
||||
function isPortrait() {
|
||||
let file = props.scene.file;
|
||||
let width = file.width ? file.width : 0;
|
||||
let height = file.height ? file.height : 0;
|
||||
return height > width;
|
||||
}
|
||||
|
||||
function getLinkClassName() {
|
||||
let ret = "image previewable";
|
||||
|
||||
if (isPortrait()) {
|
||||
ret += " portrait";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getVideoClassName() {
|
||||
let ret = "preview";
|
||||
|
||||
if (isPortrait()) {
|
||||
ret += " portrait";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
var shiftKey = false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="grid-item"
|
||||
className={"grid-item scene-card " + ZoomUtils.classForZoom(props.zoomIndex)}
|
||||
elevation={Elevation.ONE}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
@ -134,11 +214,15 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
||||
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { shiftKey = event.shiftKey; event.stopPropagation(); } }
|
||||
/>
|
||||
<Link to={`/scenes/${props.scene.id}`} className="image previewable">
|
||||
{maybeRenderRatingBanner()}
|
||||
<video className="preview" loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
|
||||
{!!previewPath ? <source src={previewPath} /> : ""}
|
||||
</video>
|
||||
<Link to={`/scenes/${props.scene.id}`} className={getLinkClassName()}>
|
||||
<div className="video-container">
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
{maybeRenderSceneStudioOverlay()}
|
||||
<video className={getVideoClassName()} loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
|
||||
{!!previewPath ? <source src={previewPath} /> : ""}
|
||||
</video>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="card-section">
|
||||
<H4 style={{textOverflow: "ellipsis", overflow: "hidden"}}>
|
||||
|
@ -149,16 +233,6 @@ export const SceneCard: FunctionComponent<ISceneCardProps> = (props: ISceneCardP
|
|||
</div>
|
||||
|
||||
{maybeRenderPopoverButtonGroup()}
|
||||
|
||||
<Divider />
|
||||
<span className="card-section centered">
|
||||
{props.scene.file.size !== undefined ? TextUtils.fileSize(parseInt(props.scene.file.size, 10)) : ""}
|
||||
|
|
||||
{props.scene.file.duration !== undefined ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ""}
|
||||
|
|
||||
{props.scene.file.width} x {props.scene.file.height}
|
||||
</span>
|
||||
{SceneHelpers.maybeRenderStudio(props.scene, 50, true)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListP
|
|||
const listData = ListHook.useList({
|
||||
filterMode: FilterMode.Scenes,
|
||||
props,
|
||||
zoomable: true,
|
||||
renderContent,
|
||||
renderSelectedOptions
|
||||
});
|
||||
|
@ -45,23 +46,24 @@ export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListP
|
|||
);
|
||||
}
|
||||
|
||||
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>) {
|
||||
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>, zoomIndex: number) {
|
||||
return (
|
||||
<SceneCard
|
||||
key={scene.id}
|
||||
scene={scene}
|
||||
zoomIndex={zoomIndex}
|
||||
selected={selectedIds.has(scene.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) => listData.onSelectChange(scene.id, selected, shiftKey)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderContent(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel, selectedIds: Set<string>) {
|
||||
function renderContent(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) {
|
||||
if (!result.data || !result.data.findScenes) { return; }
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="grid">
|
||||
{result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds))}
|
||||
{result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds, zoomIndex))}
|
||||
</div>
|
||||
);
|
||||
} else if (filter.displayMode === DisplayMode.List) {
|
||||
|
|
|
@ -21,7 +21,8 @@ export interface IListHookData {
|
|||
export interface IListHookOptions {
|
||||
filterMode: FilterMode;
|
||||
props: IBaseProps;
|
||||
renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>) => JSX.Element | undefined;
|
||||
zoomable?: boolean
|
||||
renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) => JSX.Element | undefined;
|
||||
renderSelectedOptions?: (result: QueryHookResult<any, any>, selectedIds: Set<string>) => JSX.Element | undefined;
|
||||
}
|
||||
|
||||
|
@ -31,6 +32,7 @@ export class ListHook {
|
|||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>(undefined);
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||
|
||||
// Update the filter when the query parameters change
|
||||
useEffect(() => {
|
||||
|
@ -254,6 +256,10 @@ export class ListHook {
|
|||
setLastClickedId(undefined);
|
||||
}
|
||||
|
||||
function onChangeZoom(newZoomIndex : number) {
|
||||
setZoomIndex(newZoomIndex);
|
||||
}
|
||||
|
||||
const template = (
|
||||
<div>
|
||||
<ListFilter
|
||||
|
@ -266,12 +272,14 @@ export class ListHook {
|
|||
onRemoveCriterion={onRemoveCriterion}
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
zoomIndex={options.zoomable ? zoomIndex : undefined}
|
||||
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
|
||||
filter={filter}
|
||||
/>
|
||||
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
|
||||
{result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
||||
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
||||
{options.renderContent(result, filter, selectedIds)}
|
||||
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
||||
<Pagination
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
|
|
|
@ -38,7 +38,7 @@ code {
|
|||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
margin: $pt-grid-size $pt-grid-size 0 0;
|
||||
padding: 0 calc(10%);
|
||||
padding: 0 100px;
|
||||
|
||||
&.wall {
|
||||
padding: 0;
|
||||
|
@ -67,7 +67,7 @@ code {
|
|||
|
||||
.grid-item {
|
||||
// flex: auto;
|
||||
width: calc(25% - 1.5em);
|
||||
width: 320px;
|
||||
min-width: 185px;
|
||||
margin: 0px 0 $pt-grid-size $pt-grid-size;
|
||||
overflow: hidden;
|
||||
|
@ -76,6 +76,47 @@ code {
|
|||
width: calc(20%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.zoom-0 {
|
||||
width: 240px;
|
||||
|
||||
& .previewable {
|
||||
max-height: 180px;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
&.zoom-1 {
|
||||
width: 320px;
|
||||
|
||||
& .previewable {
|
||||
max-height: 240px;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
&.zoom-2 {
|
||||
width: 480px;
|
||||
|
||||
& .previewable {
|
||||
max-height: 360px;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
&.zoom-3 {
|
||||
width: 640px;
|
||||
|
||||
& .previewable {
|
||||
max-height: 480px;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.previewable {
|
||||
|
@ -85,6 +126,11 @@ code {
|
|||
width: calc(100% + 40px);
|
||||
margin: -20px 0 0 -20px;
|
||||
position: relative;
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
.previewable.portrait {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.grid-item label.card-select {
|
||||
|
@ -95,12 +141,24 @@ code {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video.preview {
|
||||
// height: 225px; // slows down the page
|
||||
width: 100%;
|
||||
// width: calc(100% + 40px);
|
||||
// margin: -20px 0 0 -20px;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
video.preview.portrait {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.filter-item, .operation-item {
|
||||
|
@ -161,6 +219,67 @@ video.preview {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.scene-specs-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
right: .7em;
|
||||
font-weight: 400;
|
||||
color: #f5f8fa;
|
||||
letter-spacing: -.03em;
|
||||
text-shadow: 0 0 3px #000;
|
||||
}
|
||||
|
||||
.scene-studio-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: .7em;
|
||||
right: .7em;
|
||||
font-weight: 900;
|
||||
width: 40%;
|
||||
height: 20%;
|
||||
opacity: 0.75;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.scene-studio-overlay a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
background-position: right top;
|
||||
background-repeat: no-repeat;
|
||||
letter-spacing: -.03em;
|
||||
text-shadow: 0 0 3px #000;
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
color: #f5f8fa;
|
||||
}
|
||||
|
||||
.overlay-resolution {
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin-right:.3em;
|
||||
}
|
||||
|
||||
.scene-card {
|
||||
& .scene-specs-overlay, .rating-banner, .scene-studio-overlay {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-card:hover {
|
||||
& .scene-specs-overlay, .rating-banner, .scene-studio-overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.scene-studio-overlay:hover {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
#jwplayer-container {
|
||||
margin: 10px auto;
|
||||
width: 75%;
|
||||
|
@ -223,6 +342,19 @@ span.block {
|
|||
background-repeat: no-repeat !important;
|
||||
}
|
||||
|
||||
.performer-tag-container {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.performer-tag.image {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.studio.image {
|
||||
height: 100px;
|
||||
background-size: contain !important;
|
||||
|
@ -331,4 +463,13 @@ span.block {
|
|||
float: right;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-slider {
|
||||
margin: auto 5px;
|
||||
width: 100px;
|
||||
|
||||
& .bp3-slider {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
|
@ -18,7 +18,17 @@ export class TextUtils {
|
|||
}
|
||||
|
||||
public static secondsToTimestamp(seconds: number): string {
|
||||
return new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
|
||||
if (ret.startsWith("00")) {
|
||||
// strip hours if under one hour
|
||||
ret = ret.substr(3);
|
||||
}
|
||||
if (ret.startsWith("0")) {
|
||||
// for duration under a minute, leave one leading zero
|
||||
ret = ret.substr(1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static fileNameFromPath(path: string): string {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export class ZoomUtils {
|
||||
public static classForZoom(zoomIndex: number): string {
|
||||
return "zoom-" + zoomIndex;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue