mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
13
frontend/src/Utilities/Array/getIndexOfFirstCharacter.js
Normal file
13
frontend/src/Utilities/Array/getIndexOfFirstCharacter.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function getIndexOfFirstCharacter(items, character) {
|
||||
return _.findIndex(items, (item) => {
|
||||
const firstCharacter = item.sortTitle.charAt(0);
|
||||
|
||||
if (character === '#') {
|
||||
return !isNaN(firstCharacter);
|
||||
}
|
||||
|
||||
return firstCharacter === character;
|
||||
});
|
||||
}
|
5
frontend/src/Utilities/Array/sortByName.js
Normal file
5
frontend/src/Utilities/Array/sortByName.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function sortByName(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export default sortByName;
|
10
frontend/src/Utilities/Command/findCommand.js
Normal file
10
frontend/src/Utilities/Command/findCommand.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import isSameCommand from './isSameCommand';
|
||||
|
||||
function findCommand(commands, options) {
|
||||
return _.findLast(commands, (command) => {
|
||||
return isSameCommand(command.body, options);
|
||||
});
|
||||
}
|
||||
|
||||
export default findCommand;
|
5
frontend/src/Utilities/Command/index.js
Normal file
5
frontend/src/Utilities/Command/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as findCommand } from './findCommand';
|
||||
export { default as isCommandComplete } from './isCommandComplete';
|
||||
export { default as isCommandExecuting } from './isCommandExecuting';
|
||||
export { default as isCommandFailed } from './isCommandFailed';
|
||||
export { default as isSameCommand } from './isSameCommand';
|
9
frontend/src/Utilities/Command/isCommandComplete.js
Normal file
9
frontend/src/Utilities/Command/isCommandComplete.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function isCommandComplete(command) {
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return command.status === 'complete';
|
||||
}
|
||||
|
||||
export default isCommandComplete;
|
9
frontend/src/Utilities/Command/isCommandExecuting.js
Normal file
9
frontend/src/Utilities/Command/isCommandExecuting.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function isCommandExecuting(command) {
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return command.status === 'queued' || command.status === 'started';
|
||||
}
|
||||
|
||||
export default isCommandExecuting;
|
12
frontend/src/Utilities/Command/isCommandFailed.js
Normal file
12
frontend/src/Utilities/Command/isCommandFailed.js
Normal file
@@ -0,0 +1,12 @@
|
||||
function isCommandFailed(command) {
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return command.status === 'failed' ||
|
||||
command.status === 'aborted' ||
|
||||
command.status === 'cancelled' ||
|
||||
command.status === 'orphaned';
|
||||
}
|
||||
|
||||
export default isCommandFailed;
|
24
frontend/src/Utilities/Command/isSameCommand.js
Normal file
24
frontend/src/Utilities/Command/isSameCommand.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function isSameCommand(commandA, commandB) {
|
||||
if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in commandB) {
|
||||
if (key !== 'name') {
|
||||
const value = commandB[key];
|
||||
if (Array.isArray(value)) {
|
||||
if (_.difference(value, commandA[key]).length > 0) {
|
||||
return false;
|
||||
}
|
||||
} else if (value !== commandA[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default isSameCommand;
|
7
frontend/src/Utilities/Constants/keyCodes.js
Normal file
7
frontend/src/Utilities/Constants/keyCodes.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const TAB = 9;
|
||||
export const ENTER = 13;
|
||||
export const SHIFT = 16;
|
||||
export const CONTROL = 17;
|
||||
export const ESCAPE = 27;
|
||||
export const UP_ARROW = 38;
|
||||
export const DOWN_ARROW = 40;
|
33
frontend/src/Utilities/Date/dateFilterPredicate.js
Normal file
33
frontend/src/Utilities/Date/dateFilterPredicate.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import moment from 'moment';
|
||||
import isAfter from 'Utilities/Date/isAfter';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
|
||||
export default function(itemValue, filterValue, type) {
|
||||
if (!itemValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case filterTypes.LESS_THAN:
|
||||
return moment(itemValue).isBefore(filterValue);
|
||||
|
||||
case filterTypes.GREATER_THAN:
|
||||
return moment(itemValue).isAfter(filterValue);
|
||||
|
||||
case filterTypes.IN_LAST:
|
||||
return (
|
||||
isAfter(itemValue, { [filterValue.time]: filterValue.value * -1 }) &&
|
||||
isBefore(itemValue)
|
||||
);
|
||||
|
||||
case filterTypes.IN_NEXT:
|
||||
return (
|
||||
isAfter(itemValue) &&
|
||||
isBefore(itemValue, { [filterValue.time]: filterValue.value })
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
11
frontend/src/Utilities/Date/formatDate.js
Normal file
11
frontend/src/Utilities/Date/formatDate.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function formatDate(date, dateFormat) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return moment(date).format(dateFormat);
|
||||
}
|
||||
|
||||
export default formatDate;
|
39
frontend/src/Utilities/Date/formatDateTime.js
Normal file
39
frontend/src/Utilities/Date/formatDateTime.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import moment from 'moment';
|
||||
import formatTime from './formatTime';
|
||||
import isToday from './isToday';
|
||||
import isTomorrow from './isTomorrow';
|
||||
import isYesterday from './isYesterday';
|
||||
|
||||
function getRelativeDay(date, includeRelativeDate) {
|
||||
if (!includeRelativeDate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return 'Yesterday, ';
|
||||
}
|
||||
|
||||
if (isToday(date)) {
|
||||
return 'Today, ';
|
||||
}
|
||||
|
||||
if (isTomorrow(date)) {
|
||||
return 'Tomorrow, ';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, includeRelativeDay = false } = {}) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const relativeDay = getRelativeDay(date, includeRelativeDay);
|
||||
const formattedDate = moment(date).format(dateFormat);
|
||||
const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
|
||||
|
||||
return `${relativeDay}${formattedDate} ${formattedTime}`;
|
||||
}
|
||||
|
||||
export default formatDateTime;
|
19
frontend/src/Utilities/Date/formatTime.js
Normal file
19
frontend/src/Utilities/Date/formatTime.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function formatTime(date, timeFormat, { includeMinuteZero = false, includeSeconds = false } = {}) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (includeSeconds) {
|
||||
timeFormat = timeFormat.replace(/\(?:mm\)?/, ':mm:ss');
|
||||
} else if (includeMinuteZero) {
|
||||
timeFormat = timeFormat.replace('(:mm)', ':mm');
|
||||
} else {
|
||||
timeFormat = timeFormat.replace('(:mm)', '');
|
||||
}
|
||||
|
||||
return moment(date).format(timeFormat);
|
||||
}
|
||||
|
||||
export default formatTime;
|
24
frontend/src/Utilities/Date/formatTimeSpan.js
Normal file
24
frontend/src/Utilities/Date/formatTimeSpan.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import moment from 'moment';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
|
||||
function formatTimeSpan(timeSpan) {
|
||||
if (!timeSpan) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const duration = moment.duration(timeSpan);
|
||||
const days = duration.get('days');
|
||||
const hours = padNumber(duration.get('hours'), 2);
|
||||
const minutes = padNumber(duration.get('minutes'), 2);
|
||||
const seconds = padNumber(duration.get('seconds'), 2);
|
||||
|
||||
const time = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${time}`;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
export default formatTimeSpan;
|
42
frontend/src/Utilities/Date/getRelativeDate.js
Normal file
42
frontend/src/Utilities/Date/getRelativeDate.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import moment from 'moment';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import isInNextWeek from 'Utilities/Date/isInNextWeek';
|
||||
import isToday from 'Utilities/Date/isToday';
|
||||
import isTomorrow from 'Utilities/Date/isTomorrow';
|
||||
import isYesterday from 'Utilities/Date/isYesterday';
|
||||
|
||||
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isTodayDate = isToday(date);
|
||||
|
||||
if (isTodayDate && timeForToday && timeFormat) {
|
||||
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
|
||||
}
|
||||
|
||||
if (!showRelativeDates) {
|
||||
return moment(date).format(shortDateFormat);
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return 'Yesterday';
|
||||
}
|
||||
|
||||
if (isTodayDate) {
|
||||
return 'Today';
|
||||
}
|
||||
|
||||
if (isTomorrow(date)) {
|
||||
return 'Tomorrow';
|
||||
}
|
||||
|
||||
if (isInNextWeek(date)) {
|
||||
return moment(date).format('dddd');
|
||||
}
|
||||
|
||||
return moment(date).format(shortDateFormat);
|
||||
}
|
||||
|
||||
export default getRelativeDate;
|
17
frontend/src/Utilities/Date/isAfter.js
Normal file
17
frontend/src/Utilities/Date/isAfter.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isAfter(date, offsets = {}) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const offsetTime = moment();
|
||||
|
||||
Object.keys(offsets).forEach((key) => {
|
||||
offsetTime.add(offsets[key], key);
|
||||
});
|
||||
|
||||
return moment(date).isAfter(offsetTime);
|
||||
}
|
||||
|
||||
export default isAfter;
|
17
frontend/src/Utilities/Date/isBefore.js
Normal file
17
frontend/src/Utilities/Date/isBefore.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isBefore(date, offsets = {}) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const offsetTime = moment();
|
||||
|
||||
Object.keys(offsets).forEach((key) => {
|
||||
offsetTime.add(offsets[key], key);
|
||||
});
|
||||
|
||||
return moment(date).isBefore(offsetTime);
|
||||
}
|
||||
|
||||
export default isBefore;
|
11
frontend/src/Utilities/Date/isInNextWeek.js
Normal file
11
frontend/src/Utilities/Date/isInNextWeek.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isInNextWeek(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
const now = moment();
|
||||
return moment(date).isBetween(now, now.clone().add(6, 'days').endOf('day'));
|
||||
}
|
||||
|
||||
export default isInNextWeek;
|
11
frontend/src/Utilities/Date/isSameWeek.js
Normal file
11
frontend/src/Utilities/Date/isSameWeek.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isSameWeek(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment(), 'week');
|
||||
}
|
||||
|
||||
export default isSameWeek;
|
11
frontend/src/Utilities/Date/isToday.js
Normal file
11
frontend/src/Utilities/Date/isToday.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isToday(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment(), 'day');
|
||||
}
|
||||
|
||||
export default isToday;
|
11
frontend/src/Utilities/Date/isTomorrow.js
Normal file
11
frontend/src/Utilities/Date/isTomorrow.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isTomorrow(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment().add(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
export default isTomorrow;
|
11
frontend/src/Utilities/Date/isYesterday.js
Normal file
11
frontend/src/Utilities/Date/isYesterday.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isYesterday(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment().subtract(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
export default isYesterday;
|
21
frontend/src/Utilities/Episode/updateEpisodes.js
Normal file
21
frontend/src/Utilities/Episode/updateEpisodes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import _ from 'lodash';
|
||||
import { update } from 'Store/Actions/baseActions';
|
||||
|
||||
function updateEpisodes(section, episodes, episodeIds, options) {
|
||||
const data = _.reduce(episodes, (result, item) => {
|
||||
if (episodeIds.indexOf(item.id) > -1) {
|
||||
result.push({
|
||||
...item,
|
||||
...options
|
||||
});
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return update({ section, data });
|
||||
}
|
||||
|
||||
export default updateEpisodes;
|
19
frontend/src/Utilities/Filter/findSelectedFilters.js
Normal file
19
frontend/src/Utilities/Filter/findSelectedFilters.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default function findSelectedFilters(selectedFilterKey, filters = [], customFilters = []) {
|
||||
if (!selectedFilterKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let selectedFilter = filters.find((f) => f.key === selectedFilterKey);
|
||||
|
||||
if (!selectedFilter) {
|
||||
selectedFilter = customFilters.find((f) => f.id === selectedFilterKey);
|
||||
}
|
||||
|
||||
if (!selectedFilter) {
|
||||
// TODO: throw in dev
|
||||
console.error('Matching filter not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
return selectedFilter.filters;
|
||||
}
|
11
frontend/src/Utilities/Filter/getFilterValue.js
Normal file
11
frontend/src/Utilities/Filter/getFilterValue.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) {
|
||||
const filter = filters.find((f) => f.key === filterKey);
|
||||
|
||||
if (!filter) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const filterValue = filter.filters.find((f) => f.key === filterValueKey);
|
||||
|
||||
return filterValue ? filterValue.value : defaultValue;
|
||||
}
|
18
frontend/src/Utilities/Movie/getNewMovie.js
Normal file
18
frontend/src/Utilities/Movie/getNewMovie.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
function getNewMovie(movie, payload) {
|
||||
const {
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
tags
|
||||
} = payload;
|
||||
|
||||
movie.monitored = monitor === 'true';
|
||||
movie.qualityProfileId = qualityProfileId;
|
||||
movie.rootFolderPath = rootFolderPath;
|
||||
movie.tags = tags;
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
export default getNewMovie;
|
23
frontend/src/Utilities/Movie/getProgressBarKind.js
Normal file
23
frontend/src/Utilities/Movie/getProgressBarKind.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getProgressBarKind(status, monitored, hasFile) {
|
||||
if (status === 'announced') {
|
||||
return kinds.PRIMARY;
|
||||
}
|
||||
|
||||
if (hasFile && monitored) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (hasFile && !monitored) {
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
if (monitored) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.WARNING;
|
||||
}
|
||||
|
||||
export default getProgressBarKind;
|
16
frontend/src/Utilities/Number/convertToBytes.js
Normal file
16
frontend/src/Utilities/Number/convertToBytes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
function convertToBytes(input, power, binaryPrefix) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = binaryPrefix ? 1024 : 1000;
|
||||
const multiplier = Math.pow(prefix, power);
|
||||
const result = size * multiplier;
|
||||
|
||||
return Math.round(result);
|
||||
}
|
||||
|
||||
export default convertToBytes;
|
17
frontend/src/Utilities/Number/formatAge.js
Normal file
17
frontend/src/Utilities/Number/formatAge.js
Normal file
@@ -0,0 +1,17 @@
|
||||
function formatAge(age, ageHours, ageMinutes) {
|
||||
age = Math.round(age);
|
||||
ageHours = parseFloat(ageHours);
|
||||
ageMinutes = ageMinutes && parseFloat(ageMinutes);
|
||||
|
||||
if (age < 2 && ageHours) {
|
||||
if (ageHours < 2 && !!ageMinutes) {
|
||||
return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`;
|
||||
}
|
||||
|
||||
return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`;
|
||||
}
|
||||
|
||||
return `${age} ${age === 1 ? 'day' : 'days'}`;
|
||||
}
|
||||
|
||||
export default formatAge;
|
16
frontend/src/Utilities/Number/formatBytes.js
Normal file
16
frontend/src/Utilities/Number/formatBytes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import filesize from 'filesize';
|
||||
|
||||
function formatBytes(input) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return filesize(size, {
|
||||
base: 2,
|
||||
round: 1
|
||||
});
|
||||
}
|
||||
|
||||
export default formatBytes;
|
10
frontend/src/Utilities/Number/padNumber.js
Normal file
10
frontend/src/Utilities/Number/padNumber.js
Normal file
@@ -0,0 +1,10 @@
|
||||
function padNumber(input, width, paddingCharacter = 0) {
|
||||
if (input == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
input = `${input}`;
|
||||
return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input;
|
||||
}
|
||||
|
||||
export default padNumber;
|
5
frontend/src/Utilities/Number/roundNumber.js
Normal file
5
frontend/src/Utilities/Number/roundNumber.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function roundNumber(input, decimalPlaces = 1) {
|
||||
const multiplier = Math.pow(10, decimalPlaces);
|
||||
|
||||
return Math.round(input * multiplier) / multiplier;
|
||||
}
|
11
frontend/src/Utilities/Object/getErrorMessage.js
Normal file
11
frontend/src/Utilities/Object/getErrorMessage.js
Normal file
@@ -0,0 +1,11 @@
|
||||
function getErrorMessage(xhr, fallbackErrorMessage) {
|
||||
if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) {
|
||||
return fallbackErrorMessage;
|
||||
}
|
||||
|
||||
const message = xhr.responseJSON.message;
|
||||
|
||||
return message || fallbackErrorMessage;
|
||||
}
|
||||
|
||||
export default getErrorMessage;
|
10
frontend/src/Utilities/Object/hasDifferentItems.js
Normal file
10
frontend/src/Utilities/Object/hasDifferentItems.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function hasDifferentItems(prevItems, currentItems, idProp = 'id') {
|
||||
const diff1 = _.differenceBy(prevItems, currentItems, (item) => item[idProp]);
|
||||
const diff2 = _.differenceBy(currentItems, prevItems, (item) => item[idProp]);
|
||||
|
||||
return diff1.length > 0 || diff2.length > 0;
|
||||
}
|
||||
|
||||
export default hasDifferentItems;
|
15
frontend/src/Utilities/Object/selectUniqueIds.js
Normal file
15
frontend/src/Utilities/Object/selectUniqueIds.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function selectUniqueIds(items, idProp) {
|
||||
const ids = _.reduce(items, (result, item) => {
|
||||
if (item[idProp]) {
|
||||
result.push(item[idProp]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return _.uniq(ids);
|
||||
}
|
||||
|
||||
export default selectUniqueIds;
|
16
frontend/src/Utilities/Quality/getQualities.js
Normal file
16
frontend/src/Utilities/Quality/getQualities.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function getQualities(qualities) {
|
||||
if (!qualities) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return qualities.reduce((acc, item) => {
|
||||
if (item.quality) {
|
||||
acc.push(item.quality);
|
||||
} else {
|
||||
const groupQualities = item.items.map((i) => i.quality);
|
||||
acc.push(...groupQualities);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
26
frontend/src/Utilities/ResolutionUtility.js
Normal file
26
frontend/src/Utilities/ResolutionUtility.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
module.exports = {
|
||||
resolutions: {
|
||||
desktopLarge: 1200,
|
||||
desktop: 992,
|
||||
tablet: 768,
|
||||
mobile: 480
|
||||
},
|
||||
|
||||
isDesktopLarge() {
|
||||
return $(window).width() < this.resolutions.desktopLarge;
|
||||
},
|
||||
|
||||
isDesktop() {
|
||||
return $(window).width() < this.resolutions.desktop;
|
||||
},
|
||||
|
||||
isTablet() {
|
||||
return $(window).width() < this.resolutions.tablet;
|
||||
},
|
||||
|
||||
isMobile() {
|
||||
return $(window).width() < this.resolutions.mobile;
|
||||
}
|
||||
};
|
42
frontend/src/Utilities/State/getProviderState.js
Normal file
42
frontend/src/Utilities/State/getProviderState.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import _ from 'lodash';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
|
||||
function getProviderState(payload, getState, section) {
|
||||
const {
|
||||
id,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const state = getSectionState(getState(), section, true);
|
||||
const pendingChanges = Object.assign({}, state.pendingChanges, otherPayload);
|
||||
const pendingFields = state.pendingChanges.fields || {};
|
||||
delete pendingChanges.fields;
|
||||
|
||||
const item = id ? _.find(state.items, { id }) : state.selectedSchema || state.schema || {};
|
||||
|
||||
if (item.fields) {
|
||||
pendingChanges.fields = _.reduce(item.fields, (result, field) => {
|
||||
const name = field.name;
|
||||
|
||||
const value = pendingFields.hasOwnProperty(name) ?
|
||||
pendingFields[name] :
|
||||
field.value;
|
||||
|
||||
// Only send the name and value to the server
|
||||
result.push({
|
||||
name,
|
||||
value
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const result = Object.assign({}, item, pendingChanges);
|
||||
|
||||
delete result.presets;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default getProviderState;
|
22
frontend/src/Utilities/State/getSectionState.js
Normal file
22
frontend/src/Utilities/State/getSectionState.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function getSectionState(state, section, isFullStateTree = false) {
|
||||
if (isFullStateTree) {
|
||||
return _.get(state, section);
|
||||
}
|
||||
|
||||
const [, subSection] = section.split('.');
|
||||
|
||||
if (subSection) {
|
||||
return Object.assign({}, state[subSection]);
|
||||
}
|
||||
|
||||
// TODO: Remove in favour of using subSection
|
||||
if (state.hasOwnProperty(section)) {
|
||||
return Object.assign({}, state[section]);
|
||||
}
|
||||
|
||||
return Object.assign({}, state);
|
||||
}
|
||||
|
||||
export default getSectionState;
|
34
frontend/src/Utilities/State/selectProviderSchema.js
Normal file
34
frontend/src/Utilities/State/selectProviderSchema.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import _ from 'lodash';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
function applySchemaDefaults(selectedSchema, schemaDefaults) {
|
||||
if (!schemaDefaults) {
|
||||
return selectedSchema;
|
||||
} else if (_.isFunction(schemaDefaults)) {
|
||||
return schemaDefaults(selectedSchema);
|
||||
}
|
||||
|
||||
return Object.assign(selectedSchema, schemaDefaults);
|
||||
}
|
||||
|
||||
function selectProviderSchema(state, section, payload, schemaDefaults) {
|
||||
const newState = getSectionState(state, section);
|
||||
|
||||
const {
|
||||
implementation,
|
||||
presetName
|
||||
} = payload;
|
||||
|
||||
const selectedImplementation = _.find(newState.schema, { implementation });
|
||||
|
||||
const selectedSchema = presetName ?
|
||||
_.find(selectedImplementation.presets, { name: presetName }) :
|
||||
selectedImplementation;
|
||||
|
||||
newState.selectedSchema = applySchemaDefaults(_.cloneDeep(selectedSchema), schemaDefaults);
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
|
||||
export default selectProviderSchema;
|
16
frontend/src/Utilities/State/updateSectionState.js
Normal file
16
frontend/src/Utilities/State/updateSectionState.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function updateSectionState(state, section, newState) {
|
||||
const [, subSection] = section.split('.');
|
||||
|
||||
if (subSection) {
|
||||
return Object.assign({}, state, { [subSection]: newState });
|
||||
}
|
||||
|
||||
// TODO: Remove in favour of using subSection
|
||||
if (state.hasOwnProperty(section)) {
|
||||
return Object.assign({}, state, { [section]: newState });
|
||||
}
|
||||
|
||||
return Object.assign({}, state, newState);
|
||||
}
|
||||
|
||||
export default updateSectionState;
|
5
frontend/src/Utilities/String/combinePath.js
Normal file
5
frontend/src/Utilities/String/combinePath.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function combinePath(isWindows, basePath, paths = []) {
|
||||
const slash = isWindows ? '\\' : '/';
|
||||
|
||||
return `${basePath}${slash}${paths.join(slash)}`;
|
||||
}
|
6
frontend/src/Utilities/String/generateUUIDv4.js
Normal file
6
frontend/src/Utilities/String/generateUUIDv4.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function generateUUIDv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
3
frontend/src/Utilities/String/isString.js
Normal file
3
frontend/src/Utilities/String/isString.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function isString(possibleString) {
|
||||
return typeof possibleString === 'string' || possibleString instanceof String;
|
||||
}
|
36
frontend/src/Utilities/String/parseUrl.js
Normal file
36
frontend/src/Utilities/String/parseUrl.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import _ from 'lodash';
|
||||
import qs from 'qs';
|
||||
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
export default function parseUrl(url) {
|
||||
anchor.href = url;
|
||||
|
||||
// The `origin`, `password`, and `username` properties are unavailable in
|
||||
// Opera Presto. We synthesize `origin` if it's not present. While `password`
|
||||
// and `username` are ignored intentionally.
|
||||
const properties = _.pick(
|
||||
anchor,
|
||||
'hash',
|
||||
'host',
|
||||
'hostname',
|
||||
'href',
|
||||
'origin',
|
||||
'pathname',
|
||||
'port',
|
||||
'protocol',
|
||||
'search'
|
||||
);
|
||||
|
||||
properties.isAbsolute = (/^[\w:]*\/\//).test(url);
|
||||
|
||||
if (properties.search) {
|
||||
// Remove leading ? from querystring before parsing.
|
||||
properties.params = qs.parse(properties.search.substring(1));
|
||||
} else {
|
||||
properties.params = {};
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
17
frontend/src/Utilities/String/split.js
Normal file
17
frontend/src/Utilities/String/split.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function split(input, separator = ',') {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.reduce(input.split(separator), (result, s) => {
|
||||
if (s) {
|
||||
result.push(s);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default split;
|
11
frontend/src/Utilities/String/titleCase.js
Normal file
11
frontend/src/Utilities/String/titleCase.js
Normal file
@@ -0,0 +1,11 @@
|
||||
function titleCase(input) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return input.replace(/\b\w+/g, (match) => {
|
||||
return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
export default titleCase;
|
17
frontend/src/Utilities/Table/areAllSelected.js
Normal file
17
frontend/src/Utilities/Table/areAllSelected.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function areAllSelected(selectedState) {
|
||||
let allSelected = true;
|
||||
let allUnselected = true;
|
||||
|
||||
Object.keys(selectedState).forEach((key) => {
|
||||
if (selectedState[key]) {
|
||||
allUnselected = false;
|
||||
} else {
|
||||
allSelected = false;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
allSelected,
|
||||
allUnselected
|
||||
};
|
||||
}
|
15
frontend/src/Utilities/Table/getSelectedIds.js
Normal file
15
frontend/src/Utilities/Table/getSelectedIds.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function getSelectedIds(selectedState, { parseIds = true } = {}) {
|
||||
return _.reduce(selectedState, (result, value, id) => {
|
||||
if (value) {
|
||||
const parsedId = parseIds ? parseInt(id) : id;
|
||||
|
||||
result.push(parsedId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default getSelectedIds;
|
23
frontend/src/Utilities/Table/getToggledRange.js
Normal file
23
frontend/src/Utilities/Table/getToggledRange.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function getToggledRange(items, id, lastToggled) {
|
||||
const lastToggledIndex = _.findIndex(items, { id: lastToggled });
|
||||
const changedIndex = _.findIndex(items, { id });
|
||||
let lower = 0;
|
||||
let upper = 0;
|
||||
|
||||
if (lastToggledIndex > changedIndex) {
|
||||
lower = changedIndex;
|
||||
upper = lastToggledIndex + 1;
|
||||
} else {
|
||||
lower = lastToggledIndex;
|
||||
upper = changedIndex;
|
||||
}
|
||||
|
||||
return {
|
||||
lower,
|
||||
upper
|
||||
};
|
||||
}
|
||||
|
||||
export default getToggledRange;
|
16
frontend/src/Utilities/Table/removeOldSelectedState.js
Normal file
16
frontend/src/Utilities/Table/removeOldSelectedState.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import areAllSelected from './areAllSelected';
|
||||
|
||||
export default function removeOldSelectedState(state, prevItems) {
|
||||
const selectedState = {
|
||||
...state.selectedState
|
||||
};
|
||||
|
||||
prevItems.forEach((item) => {
|
||||
delete selectedState[item.id];
|
||||
});
|
||||
|
||||
return {
|
||||
...areAllSelected(selectedState),
|
||||
selectedState
|
||||
};
|
||||
}
|
17
frontend/src/Utilities/Table/selectAll.js
Normal file
17
frontend/src/Utilities/Table/selectAll.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
function selectAll(selectedState, selected) {
|
||||
const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => {
|
||||
result[item] = selected;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
allSelected: selected,
|
||||
allUnselected: !selected,
|
||||
lastToggled: null,
|
||||
selectedState: newSelectedState
|
||||
};
|
||||
}
|
||||
|
||||
export default selectAll;
|
30
frontend/src/Utilities/Table/toggleSelected.js
Normal file
30
frontend/src/Utilities/Table/toggleSelected.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import areAllSelected from './areAllSelected';
|
||||
import getToggledRange from './getToggledRange';
|
||||
|
||||
function toggleSelected(state, items, id, selected, shiftKey) {
|
||||
const lastToggled = state.lastToggled;
|
||||
const selectedState = {
|
||||
...state.selectedState,
|
||||
[id]: selected
|
||||
};
|
||||
|
||||
if (selected == null) {
|
||||
delete selectedState[id];
|
||||
}
|
||||
|
||||
if (shiftKey && lastToggled) {
|
||||
const { lower, upper } = getToggledRange(items, id, lastToggled);
|
||||
|
||||
for (let i = lower; i < upper; i++) {
|
||||
selectedState[items[i].id] = selected;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...areAllSelected(selectedState),
|
||||
lastToggled: id,
|
||||
selectedState
|
||||
};
|
||||
}
|
||||
|
||||
export default toggleSelected;
|
30
frontend/src/Utilities/createAjaxRequest.js
Normal file
30
frontend/src/Utilities/createAjaxRequest.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
export default function createAjaxRequest(ajaxOptions) {
|
||||
const requestXHR = new window.XMLHttpRequest();
|
||||
let aborted = false;
|
||||
let complete = false;
|
||||
|
||||
function abortRequest() {
|
||||
if (!complete) {
|
||||
aborted = true;
|
||||
requestXHR.abort();
|
||||
}
|
||||
}
|
||||
|
||||
const request = $.ajax({
|
||||
xhr: () => requestXHR,
|
||||
...ajaxOptions
|
||||
}).then(null, (xhr, textStatus, errorThrown) => {
|
||||
xhr.aborted = aborted;
|
||||
|
||||
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
|
||||
}).always(() => {
|
||||
complete = true;
|
||||
});
|
||||
|
||||
return {
|
||||
request,
|
||||
abortRequest
|
||||
};
|
||||
}
|
3
frontend/src/Utilities/getPathWithUrlBase.js
Normal file
3
frontend/src/Utilities/getPathWithUrlBase.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function getPathWithUrlBase(path) {
|
||||
return `${window.Radarr.urlBase}${path}`;
|
||||
}
|
7
frontend/src/Utilities/getUniqueElementId.js
Normal file
7
frontend/src/Utilities/getUniqueElementId.js
Normal file
@@ -0,0 +1,7 @@
|
||||
let i = 0;
|
||||
|
||||
// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
|
||||
|
||||
export default function getUniqueElementId() {
|
||||
return `id-${i++}`;
|
||||
}
|
7
frontend/src/Utilities/isMobile.js
Normal file
7
frontend/src/Utilities/isMobile.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import MobileDetect from 'mobile-detect';
|
||||
|
||||
export default function isMobile() {
|
||||
const mobileDetect = new MobileDetect(window.navigator.userAgent);
|
||||
|
||||
return mobileDetect.mobile() != null;
|
||||
}
|
28
frontend/src/Utilities/pagePopulator.js
Normal file
28
frontend/src/Utilities/pagePopulator.js
Normal file
@@ -0,0 +1,28 @@
|
||||
let currentPopulator = null;
|
||||
let currentReasons = [];
|
||||
|
||||
export function registerPagePopulator(populator, reasons = []) {
|
||||
currentPopulator = populator;
|
||||
currentReasons = reasons;
|
||||
}
|
||||
|
||||
export function unregisterPagePopulator(populator) {
|
||||
if (currentPopulator === populator) {
|
||||
currentPopulator = null;
|
||||
currentReasons = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function repopulatePage(reason) {
|
||||
if (!currentPopulator) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
currentPopulator();
|
||||
}
|
||||
|
||||
if (reason && currentReasons.includes(reason)) {
|
||||
currentPopulator();
|
||||
}
|
||||
}
|
9
frontend/src/Utilities/pages.js
Normal file
9
frontend/src/Utilities/pages.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const pages = {
|
||||
FIRST: 'first',
|
||||
PREVIOUS: 'previous',
|
||||
NEXT: 'next',
|
||||
LAST: 'last',
|
||||
EXACT: 'exact'
|
||||
};
|
||||
|
||||
export default pages;
|
40
frontend/src/Utilities/requestAction.js
Normal file
40
frontend/src/Utilities/requestAction.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
function flattenProviderData(providerData) {
|
||||
return _.reduce(Object.keys(providerData), (result, key) => {
|
||||
const property = providerData[key];
|
||||
|
||||
if (key === 'fields') {
|
||||
result[key] = property;
|
||||
} else {
|
||||
result[key] = property.value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function requestAction(payload) {
|
||||
const {
|
||||
provider,
|
||||
action,
|
||||
providerData,
|
||||
queryParams
|
||||
} = payload;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `/${provider}/action/${action}`,
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(flattenProviderData(providerData))
|
||||
};
|
||||
|
||||
if (queryParams) {
|
||||
ajaxOptions.url += `?${$.param(queryParams, true)}`;
|
||||
}
|
||||
|
||||
return $.ajax(ajaxOptions);
|
||||
}
|
||||
|
||||
export default requestAction;
|
6
frontend/src/Utilities/sectionTypes.js
Normal file
6
frontend/src/Utilities/sectionTypes.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const sectionTypes = {
|
||||
COLLECTION: 'collection',
|
||||
MODEL: 'model'
|
||||
};
|
||||
|
||||
export default sectionTypes;
|
12
frontend/src/Utilities/serverSideCollectionHandlers.js
Normal file
12
frontend/src/Utilities/serverSideCollectionHandlers.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const serverSideCollectionHandlers = {
|
||||
FETCH: 'fetch',
|
||||
FIRST_PAGE: 'firstPage',
|
||||
PREVIOUS_PAGE: 'previousPage',
|
||||
NEXT_PAGE: 'nextPage',
|
||||
LAST_PAGE: 'lastPage',
|
||||
EXACT_PAGE: 'exactPage',
|
||||
SORT: 'sort',
|
||||
FILTER: 'filter'
|
||||
};
|
||||
|
||||
export default serverSideCollectionHandlers;
|
Reference in New Issue
Block a user