mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Date picker (#3572)
* Add date picker dependency * Add DateInput component * Add DateInput to edit panels * Add DateInput to DateFilter * Add time to DateInput and add to Timestamp filter * Use calendar icon for button
This commit is contained in:
@@ -39,6 +39,7 @@ import { galleryTitle } from "src/core/galleries";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
@@ -458,11 +459,18 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
|
||||
@@ -20,6 +20,7 @@ import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
@@ -205,11 +206,18 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { IDateValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IDateFilterProps {
|
||||
criterion: Criterion<IDateValue>;
|
||||
@@ -18,11 +19,7 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
||||
|
||||
const { value } = criterion;
|
||||
|
||||
function onChanged(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
property: "value" | "value2"
|
||||
) {
|
||||
const newValue = event.target.value;
|
||||
function onChanged(newValue: string, property: "value" | "value2") {
|
||||
const valueCopy = { ...value };
|
||||
|
||||
valueCopy[property] = newValue;
|
||||
@@ -36,16 +33,10 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
||||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.value" }) + " (YYYY-MM-DD)"
|
||||
}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
@@ -59,17 +50,10 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
||||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||
" (YYYY-MM-DD)"
|
||||
}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
@@ -83,25 +67,21 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
||||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(
|
||||
e,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
<DateInput
|
||||
value={
|
||||
(criterion.modifier === CriterionModifier.LessThan
|
||||
? value?.value
|
||||
: value?.value2) ?? ""
|
||||
}
|
||||
placeholder={
|
||||
intl.formatMessage({ id: "criterion.less_than" }) + " (YYYY-MM-DD)"
|
||||
onValueChange={(v) =>
|
||||
onChanged(
|
||||
v,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { ITimestampValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface ITimestampFilterProps {
|
||||
criterion: Criterion<ITimestampValue>;
|
||||
@@ -18,11 +19,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
|
||||
const { value } = criterion;
|
||||
|
||||
function onChanged(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
property: "value" | "value2"
|
||||
) {
|
||||
const newValue = event.target.value;
|
||||
function onChanged(newValue: string, property: "value" | "value2") {
|
||||
const valueCopy = { ...value };
|
||||
|
||||
valueCopy[property] = newValue;
|
||||
@@ -36,7 +33,13 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -47,7 +50,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
intl.formatMessage({ id: "criterion.value" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
@@ -60,7 +63,13 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={value?.value ?? ""}
|
||||
onValueChange={(v) => onChanged(v, "value")}
|
||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -71,7 +80,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +93,24 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
<DateInput
|
||||
value={
|
||||
(criterion.modifier === CriterionModifier.LessThan
|
||||
? value?.value
|
||||
: value?.value2) ?? ""
|
||||
}
|
||||
onValueChange={(v) =>
|
||||
onChanged(
|
||||
v,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
isTime
|
||||
/>
|
||||
{/* <Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -104,7 +130,7 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
||||
intl.formatMessage({ id: "criterion.less_than" }) +
|
||||
" (YYYY-MM-DD HH:MM)"
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IMovieEditPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
@@ -410,11 +411,18 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
const isScraper = (
|
||||
scraper: GQL.Scraper | GQL.StashBox
|
||||
@@ -927,8 +928,35 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderField("birthdate", { placeholder: "YYYY-MM-DD" })}
|
||||
{renderField("death_date", { placeholder: "YYYY-MM-DD" })}
|
||||
<Form.Group controlId="birthdate" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
<FormattedMessage id="birthdate" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<DateInput
|
||||
value={formik.values.birthdate}
|
||||
onValueChange={(value) =>
|
||||
formik.setFieldValue("birthdate", value)
|
||||
}
|
||||
error={formik.errors.birthdate}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="death_date" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
<FormattedMessage id="death_date" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<DateInput
|
||||
value={formik.values.death_date}
|
||||
onValueChange={(value) =>
|
||||
formik.setFieldValue("death_date", value)
|
||||
}
|
||||
error={formik.errors.death_date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
|
||||
@@ -52,6 +52,7 @@ import { galleryTitle } from "src/core/galleries";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
@@ -769,11 +770,20 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<DateInput
|
||||
value={formik.values.date}
|
||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||
error={formik.errors.date}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"director",
|
||||
intl.formatMessage({ id: "director" })
|
||||
|
||||
105
ui/v2.5/src/components/Shared/DateInput.tsx
Normal file
105
ui/v2.5/src/components/Shared/DateInput.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
|
||||
import React, { useMemo } from "react";
|
||||
import { Button, InputGroup, Form } from "react-bootstrap";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean;
|
||||
value: string | undefined;
|
||||
isTime?: boolean;
|
||||
onValueChange(value: string): void;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const DateInput: React.FC<IProps> = (props: IProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const date = useMemo(() => {
|
||||
const toDate = props.isTime
|
||||
? TextUtils.stringToFuzzyDateTime
|
||||
: TextUtils.stringToFuzzyDate;
|
||||
if (props.value) {
|
||||
const ret = toDate(props.value);
|
||||
if (!ret || isNaN(ret.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}, [props.value, props.isTime]);
|
||||
|
||||
function maybeRenderButton() {
|
||||
if (!props.disabled) {
|
||||
const ShowPickerButton = ({
|
||||
onClick,
|
||||
}: {
|
||||
onClick: (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => (
|
||||
<Button variant="secondary" onClick={onClick}>
|
||||
<Icon icon={faCalendar} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const dateToString = props.isTime
|
||||
? TextUtils.dateTimeToString
|
||||
: TextUtils.dateToString;
|
||||
|
||||
return (
|
||||
<ReactDatePicker
|
||||
selected={date}
|
||||
onChange={(v) => {
|
||||
props.onValueChange(v ? dateToString(v) : "");
|
||||
}}
|
||||
customInput={React.createElement(ShowPickerButton)}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
scrollableMonthYearDropdown
|
||||
scrollableYearDropdown
|
||||
maxDate={new Date()}
|
||||
yearDropdownItemNumber={100}
|
||||
portalId="date-picker-portal"
|
||||
showTimeSelect={props.isTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderText = intl.formatMessage({
|
||||
id: props.isTime ? "datetime_format" : "date_format",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputGroup hasValidation>
|
||||
<Form.Control
|
||||
className="date-input text-input"
|
||||
disabled={props.disabled}
|
||||
value={props.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onValueChange(e.currentTarget.value)
|
||||
}
|
||||
placeholder={
|
||||
!props.disabled
|
||||
? props.placeholder
|
||||
? `${props.placeholder} (${placeholderText})`
|
||||
: placeholderText
|
||||
: undefined
|
||||
}
|
||||
isInvalid={!!props.error}
|
||||
/>
|
||||
<InputGroup.Append>{maybeRenderButton()}</InputGroup.Append>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{props.error}
|
||||
</Form.Control.Feedback>
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -323,3 +323,97 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
||||
}
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
||||
.date-input.form-control:focus {
|
||||
// z-index gets set to 3 in input groups
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
div.react-datepicker {
|
||||
background-color: $body-bg;
|
||||
border-color: $card-bg;
|
||||
color: $text-color;
|
||||
|
||||
.react-datepicker__header,
|
||||
.react-datepicker-time__header {
|
||||
background-color: $secondary;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
color: $text-color;
|
||||
&.react-datepicker__day--disabled {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
div.react-datepicker__time-container div.react-datepicker__time {
|
||||
background-color: $body-bg;
|
||||
color: $text-color;
|
||||
|
||||
ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover {
|
||||
background-color: rgba(138, 155, 168, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datepicker__day-name {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
// replace the current month with the dropdowns
|
||||
.react-datepicker__current-month {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__triangle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__month-dropdown-container {
|
||||
margin-left: 0;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.react-datepicker__year-dropdown-container {
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.react-datepicker__month-dropdown-container
|
||||
.react-datepicker__month-read-view,
|
||||
.react-datepicker__year-dropdown-container .react-datepicker__year-read-view {
|
||||
font-weight: bold;
|
||||
font-size: 0.944rem;
|
||||
|
||||
// react-datepicker hides these fields when the dropdown is shown
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
// hide the dropdown arrows
|
||||
.react-datepicker__month-dropdown-container
|
||||
.react-datepicker__month-read-view--down-arrow,
|
||||
.react-datepicker__year-dropdown-container
|
||||
.react-datepicker__year-read-view--down-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-datepicker__year-dropdown,
|
||||
.react-datepicker__month-dropdown {
|
||||
background-color: $body-bg;
|
||||
|
||||
.react-datepicker__year-option:hover,
|
||||
.react-datepicker__month-option:hover {
|
||||
background-color: #8a9ba826;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* stylelint-enable */
|
||||
|
||||
#date-picker-portal .react-datepicker-popper {
|
||||
z-index: 1600;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ Once migrated, these files can be deleted. The files can be optionally deleted d
|
||||
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Added date/time pickers for date and timestamp fields. ([#3572](https://github.com/stashapp/stash/pull/3572))
|
||||
* Added folder browser to path filter UI. ([#3570](https://github.com/stashapp/stash/pull/3570))
|
||||
* Include Organized flag in merge dialog. ([#3565](https://github.com/stashapp/stash/pull/3565))
|
||||
* Scene cover generation is now optional during scanning, and can be generated using the Generate task. ([#3187](https://github.com/stashapp/stash/pull/3187))
|
||||
|
||||
@@ -728,6 +728,8 @@
|
||||
"custom": "Custom",
|
||||
"date": "Date",
|
||||
"death_date": "Death Date",
|
||||
"date_format": "YYYY-MM-DD",
|
||||
"datetime_format": "YYYY-MM-DD HH:MM",
|
||||
"death_year": "Death Year",
|
||||
"descending": "Descending",
|
||||
"description": "Description",
|
||||
|
||||
@@ -189,6 +189,70 @@ const stringToDate = (dateString: string) => {
|
||||
return new Date(year, monthIndex, day, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const stringToFuzzyDate = (dateString: string) => {
|
||||
if (!dateString) return null;
|
||||
|
||||
const parts = dateString.split("-");
|
||||
// Invalid date string
|
||||
let year = Number(parts[0]);
|
||||
if (isNaN(year)) year = new Date().getFullYear();
|
||||
let monthIndex = 0;
|
||||
if (parts.length > 1) {
|
||||
monthIndex = Math.max(0, Number(parts[1]) - 1);
|
||||
if (monthIndex > 11 || isNaN(monthIndex)) monthIndex = 0;
|
||||
}
|
||||
let day = 1;
|
||||
if (parts.length > 2) {
|
||||
day = Number(parts[2]);
|
||||
if (day > 31 || isNaN(day)) day = 1;
|
||||
}
|
||||
|
||||
return new Date(year, monthIndex, day, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const stringToFuzzyDateTime = (dateString: string) => {
|
||||
if (!dateString) return null;
|
||||
|
||||
const dateTime = dateString.split(" ");
|
||||
|
||||
let date: Date | null = null;
|
||||
if (dateTime.length > 0) {
|
||||
date = stringToFuzzyDate(dateTime[0]);
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
if (dateTime.length > 1) {
|
||||
const timeParts = dateTime[1].split(":");
|
||||
if (date && timeParts.length > 0) {
|
||||
date.setHours(Number(timeParts[0]));
|
||||
}
|
||||
if (date && timeParts.length > 1) {
|
||||
date.setMinutes(Number(timeParts[1]));
|
||||
}
|
||||
if (date && timeParts.length > 2) {
|
||||
date.setSeconds(Number(timeParts[2]));
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
function dateToString(date: Date) {
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function dateTimeToString(date: Date) {
|
||||
return `${dateToString(date)} ${date
|
||||
.getHours()
|
||||
.toString()
|
||||
.padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
const getAge = (dateString?: string | null, fromDateString?: string | null) => {
|
||||
if (!dateString) return 0;
|
||||
|
||||
@@ -355,6 +419,10 @@ const TextUtils = {
|
||||
secondsToTimestamp,
|
||||
fileNameFromPath,
|
||||
stringToDate,
|
||||
stringToFuzzyDate,
|
||||
stringToFuzzyDateTime,
|
||||
dateToString,
|
||||
dateTimeToString,
|
||||
age: getAge,
|
||||
bitRate,
|
||||
resolution,
|
||||
|
||||
Reference in New Issue
Block a user