mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
WIP
This commit is contained in:
12
ui/v2.5/src/utils/color.ts
Normal file
12
ui/v2.5/src/utils/color.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export class ColorUtils {
|
||||
public static classForRating(rating: number): string {
|
||||
switch (rating) {
|
||||
case 5: return "rating-5";
|
||||
case 4: return "rating-4";
|
||||
case 3: return "rating-3";
|
||||
case 2: return "rating-2";
|
||||
case 1: return "rating-1";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
71
ui/v2.5/src/utils/editabletext.tsx
Normal file
71
ui/v2.5/src/utils/editabletext.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { HTMLSelect, InputGroup, IOptionProps, TextArea, Label } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
export class EditableTextUtils {
|
||||
public static renderTextArea(options: {
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void)
|
||||
}) {
|
||||
let element: JSX.Element;
|
||||
if (options.isEditing) {
|
||||
element = (
|
||||
<TextArea
|
||||
fill={true}
|
||||
onChange={(newValue) => options.onChange(newValue.target.value)}
|
||||
value={options.value}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
element = <p className="pre">{options.value}</p>;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public static renderInputGroup(options: {
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
placeholder?: string,
|
||||
onChange: ((value: string) => void),
|
||||
}) {
|
||||
let element: JSX.Element;
|
||||
if (options.isEditing) {
|
||||
element = (
|
||||
<InputGroup
|
||||
onChange={(newValue: any) => options.onChange(newValue.target.value)}
|
||||
value={options.value}
|
||||
placeholder={options.placeholder}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
element = <Label>{options.value}</Label>;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public static renderHtmlSelect(options: {
|
||||
value: string | number | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
selectOptions: Array<string | number | IOptionProps>,
|
||||
}) {
|
||||
let stringValue = options.value;
|
||||
if (typeof stringValue === "number") {
|
||||
stringValue = stringValue.toString();
|
||||
}
|
||||
|
||||
let element: JSX.Element;
|
||||
if (options.isEditing) {
|
||||
element = (
|
||||
<HTMLSelect
|
||||
options={options.selectOptions}
|
||||
onChange={(event) => options.onChange(event.target.value)}
|
||||
value={stringValue}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
element = <span>{options.value}</span>;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
24
ui/v2.5/src/utils/errors.ts
Normal file
24
ui/v2.5/src/utils/errors.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Intent, Position, Toaster } from "@blueprintjs/core";
|
||||
import { ApolloError } from "apollo-boost";
|
||||
|
||||
const toaster = Toaster.create({
|
||||
position: Position.TOP,
|
||||
});
|
||||
|
||||
export class ErrorUtils {
|
||||
public static handle(error: any) {
|
||||
console.error(error);
|
||||
toaster.show({
|
||||
message: error.toString(),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
|
||||
public static handleApolloError(error: ApolloError) {
|
||||
console.error(error);
|
||||
toaster.show({
|
||||
message: error.message,
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
}
|
||||
34
ui/v2.5/src/utils/image.tsx
Normal file
34
ui/v2.5/src/utils/image.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export class ImageUtils {
|
||||
|
||||
private static readImage(file: File, onLoadEnd: (this: FileReader) => any) {
|
||||
const reader: FileReader = new FileReader();
|
||||
|
||||
reader.onloadend = onLoadEnd;
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
public static onImageChange(event: React.FormEvent<HTMLInputElement>, onLoadEnd: (this: FileReader) => any) {
|
||||
const file: File = (event.target as any).files[0];
|
||||
ImageUtils.readImage(file, onLoadEnd);
|
||||
}
|
||||
|
||||
public static pasteImage(e : any, onLoadEnd: (this: FileReader) => any) {
|
||||
if (e.clipboardData.files.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file: File = e.clipboardData.files[0];
|
||||
ImageUtils.readImage(file, onLoadEnd);
|
||||
}
|
||||
|
||||
public static addPasteImageHook(onLoadEnd: (this: FileReader) => any) {
|
||||
useEffect(() => {
|
||||
const pasteImage = (e: any) => { ImageUtils.pasteImage(e, onLoadEnd) }
|
||||
window.addEventListener("paste", pasteImage);
|
||||
|
||||
return () => window.removeEventListener("paste", pasteImage);
|
||||
});
|
||||
}
|
||||
}
|
||||
49
ui/v2.5/src/utils/navigation.ts
Normal file
49
ui/v2.5/src/utils/navigation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as GQL from "../core/generated-graphql";
|
||||
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 { ListFilterModel } from "../models/list-filter/filter";
|
||||
import { FilterMode } from "../models/list-filter/types";
|
||||
|
||||
export class NavigationUtils {
|
||||
public static makePerformerScenesUrl(performer: Partial<GQL.PerformerDataFragment>): string {
|
||||
if (performer.id === undefined) { return "#"; }
|
||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [{ id: performer.id, label: performer.name || `Performer ${performer.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
}
|
||||
|
||||
public static makeStudioScenesUrl(studio: Partial<GQL.StudioDataFragment>): string {
|
||||
if (studio.id === undefined) { return "#"; }
|
||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||
const criterion = new StudiosCriterion();
|
||||
criterion.value = [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
}
|
||||
|
||||
public static makeTagScenesUrl(tag: Partial<GQL.TagDataFragment>): string {
|
||||
if (tag.id === undefined) { return "#"; }
|
||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||
const criterion = new TagsCriterion("tags");
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
}
|
||||
|
||||
public static makeTagSceneMarkersUrl(tag: Partial<GQL.TagDataFragment>): string {
|
||||
if (tag.id === undefined) { return "#"; }
|
||||
const filter = new ListFilterModel(FilterMode.SceneMarkers);
|
||||
const criterion = new TagsCriterion("tags");
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
return `/scenes/markers?${filter.makeQueryParameters()}`;
|
||||
}
|
||||
|
||||
public static makeSceneMarkerUrl(sceneMarker: Partial<GQL.SceneMarkerDataFragment>): string {
|
||||
if (sceneMarker.id === undefined || sceneMarker.scene === undefined) { return "#"; }
|
||||
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
|
||||
}
|
||||
}
|
||||
138
ui/v2.5/src/utils/table.tsx
Normal file
138
ui/v2.5/src/utils/table.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { EditableText, IOptionProps } from "@blueprintjs/core";
|
||||
import { Form } from 'react-bootstrap';
|
||||
import React from "react";
|
||||
import { EditableTextUtils } from "./editabletext";
|
||||
import { FilterMultiSelect } from "../components/select/FilterMultiSelect";
|
||||
import { FilterSelect } from "../components/select/FilterSelect";
|
||||
import _ from "lodash";
|
||||
|
||||
export class TableUtils {
|
||||
public static renderEditableTextTableRow(options: {
|
||||
title: string;
|
||||
value: string | number | undefined;
|
||||
isEditing: boolean;
|
||||
onChange: ((value: string) => void);
|
||||
}) {
|
||||
let stringValue = options.value;
|
||||
if (typeof stringValue === "number") {
|
||||
stringValue = stringValue.toString();
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<EditableText
|
||||
disabled={!options.isEditing}
|
||||
value={stringValue}
|
||||
placeholder={options.title}
|
||||
multiline={true}
|
||||
onChange={(newValue) => options.onChange(newValue)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public static renderTextArea(options: {
|
||||
title: string,
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
}) {
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
{EditableTextUtils.renderTextArea(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public static renderInputGroup(options: {
|
||||
title: string,
|
||||
placeholder?: string,
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
}) {
|
||||
let optionsCopy = _.clone(options);
|
||||
optionsCopy.placeholder = options.placeholder || options.title;
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
{ !options.isEditing
|
||||
? <h4>{optionsCopy.value}</h4>
|
||||
: <Form.Control
|
||||
defaultValue={options.value}
|
||||
placeholder={optionsCopy.placeholder}
|
||||
onChange={ (event:any) => options.onChange(event.target.value) }
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public static renderHtmlSelect(options: {
|
||||
title: string,
|
||||
value: string | number | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
selectOptions: Array<string | number | IOptionProps>,
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
{EditableTextUtils.renderHtmlSelect(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: isediting
|
||||
public static renderFilterSelect(options: {
|
||||
title: string,
|
||||
type: "performers" | "studios" | "tags",
|
||||
initialId: string | undefined,
|
||||
onChange: ((id: string | undefined) => void),
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
onSelectItem={(item) => options.onChange(item ? item.id : undefined)}
|
||||
initialId={options.initialId}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: isediting
|
||||
public static renderMultiSelect(options: {
|
||||
title: string,
|
||||
type: "performers" | "studios" | "tags",
|
||||
initialIds: string[] | undefined,
|
||||
onChange: ((ids: string[]) => void),
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<FilterMultiSelect
|
||||
type={options.type}
|
||||
onUpdate={(items) => options.onChange(items.map((i) => i.id))}
|
||||
openOnKeyDown={true}
|
||||
initialIds={options.initialIds}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
83
ui/v2.5/src/utils/text.ts
Normal file
83
ui/v2.5/src/utils/text.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export class TextUtils {
|
||||
|
||||
public static truncate(value?: string, limit: number = 100, tail: string = "..."): string {
|
||||
if (!value) { return ""; }
|
||||
return value.length > limit ? value.substring(0, limit) + tail : value;
|
||||
}
|
||||
|
||||
public static fileSize(bytes: number = 0, precision: number = 2): string {
|
||||
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) { return "?"; }
|
||||
|
||||
let unit = 0;
|
||||
while ( bytes >= 1024 ) {
|
||||
bytes /= 1024;
|
||||
unit++;
|
||||
}
|
||||
|
||||
return bytes.toFixed(+precision) + " " + this.units[unit];
|
||||
}
|
||||
|
||||
public static secondsToTimestamp(seconds: number): string {
|
||||
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 {
|
||||
if (!!path === false) { return "No File Name"; }
|
||||
return path.replace(/^.*[\\\/]/, "");
|
||||
}
|
||||
|
||||
public static age(dateString?: string, fromDateString?: string): number {
|
||||
if (!dateString) { return 0; }
|
||||
|
||||
const birthdate = new Date(dateString);
|
||||
const fromDate = !!fromDateString ? new Date(fromDateString) : new Date();
|
||||
|
||||
let age = fromDate.getFullYear() - birthdate.getFullYear();
|
||||
if (birthdate.getMonth() > fromDate.getMonth() ||
|
||||
(birthdate.getMonth() >= fromDate.getMonth() && birthdate.getDay() > fromDate.getDay())) {
|
||||
age -= 1;
|
||||
}
|
||||
|
||||
return age;
|
||||
}
|
||||
|
||||
public static bitRate(bitrate: number) {
|
||||
const megabits = bitrate / 1000000;
|
||||
return `${megabits.toFixed(2)} megabits per second`;
|
||||
}
|
||||
|
||||
public static resolution(height: number) {
|
||||
if (height >= 240 && height < 480) {
|
||||
return "240p";
|
||||
} else if (height >= 480 && height < 720) {
|
||||
return "480p";
|
||||
} else if (height >= 720 && height < 1080) {
|
||||
return "720p";
|
||||
} else if (height >= 1080 && height < 2160) {
|
||||
return "1080p";
|
||||
} else if (height >= 2160) {
|
||||
return "4K";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private static units = [
|
||||
"bytes",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
];
|
||||
}
|
||||
14
ui/v2.5/src/utils/toasts.ts
Normal file
14
ui/v2.5/src/utils/toasts.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Intent, Position, Toaster } from "@blueprintjs/core";
|
||||
|
||||
const toaster = Toaster.create({
|
||||
position: Position.TOP,
|
||||
});
|
||||
|
||||
export class ToastUtils {
|
||||
public static success(message: string) {
|
||||
toaster.show({
|
||||
message,
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
}
|
||||
}
|
||||
6
ui/v2.5/src/utils/zoom.ts
Normal file
6
ui/v2.5/src/utils/zoom.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export class ZoomUtils {
|
||||
public static classForZoom(zoomIndex: number): string {
|
||||
return "zoom-" + zoomIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user