diff --git a/.editorconfig b/.editorconfig index 8b577f8d4..d91f68d99 100644 --- a/.editorconfig +++ b/.editorconfig @@ -268,7 +268,7 @@ dotnet_diagnostic.CA5397.severity = suggestion dotnet_diagnostic.SYSLIB0006.severity = none -[*.{js,html,js,hbs,less,css}] +[*.{js,html,hbs,less,css,ts,tsx}] charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 82601a415..733e2bc4d 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -36,7 +36,7 @@ module.exports = (env) => { }, entry: { - index: 'index.js' + index: 'index.ts' }, resolve: { @@ -67,23 +67,23 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: '[name].js', + filename: '[name]-[contenthash].js', sourceMapFilename: '[file].map' }, optimization: { moduleIds: 'deterministic', - chunkIds: 'named', - splitChunks: { - chunks: 'initial', - name: 'vendors' - } + chunkIds: isProduction ? 'deterministic' : 'named' }, performance: { hints: false }, + experiments: { + topLevelAwait: true + }, + plugins: [ new webpack.DefinePlugin({ __DEV__: !isProduction, @@ -97,7 +97,8 @@ module.exports = (env) => { new HtmlWebpackPlugin({ template: 'frontend/src/index.ejs', filename: 'index.html', - publicPath: '/' + publicPath: '/', + inject: false }), new FileManagerPlugin({ diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 9c005d246..32d73f204 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -4,13 +4,14 @@ import IconButton from 'Components/Link/IconButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; import episodeEntities from 'Episode/episodeEntities'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons } from 'Helpers/Props'; +import { icons, tooltipPositions } from 'Helpers/Props'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import HistoryDetailsModal from './Details/HistoryDetailsModal'; @@ -210,7 +211,14 @@ class HistoryRow extends Component { key={name} className={styles.customFormatScore} > - {formatPreferredWordScore(customFormatScore)} + } + position={tooltipPositions.BOTTOM} + /> ); } @@ -294,4 +302,8 @@ HistoryRow.propTypes = { onMarkAsFailedPress: PropTypes.func.isRequired }; +HistoryRow.defaultProps = { + customFormats: [] +}; + export default HistoryRow; diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 868b870c6..0f66fbfa4 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -8,12 +8,13 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons, kinds } from 'Helpers/Props'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -267,7 +268,14 @@ class QueueRow extends Component { key={name} className={styles.customFormatScore} > - {formatPreferredWordScore(customFormatScore)} + } + position={tooltipPositions.BOTTOM} + /> ); } @@ -450,6 +458,7 @@ QueueRow.propTypes = { }; QueueRow.defaultProps = { + customFormats: [], isGrabbing: false, isRemoving: false }; diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index 781b2ca10..ea29231c2 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history }) { +function App({ store, history, hasTranslationsError }) { return ( - + @@ -25,7 +25,8 @@ function App({ store, history }) { App.propTypes = { store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired + history: PropTypes.object.isRequired, + hasTranslationsError: PropTypes.bool.isRequired }; export default App; diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 7f305e65c..2304a0d67 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -69,7 +69,7 @@ class QualityProfileSelectInputConnector extends Component { // Listeners onChange = ({ name, value }) => { - this.props.onChange({ name, value: parseInt(value) }); + this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) }); }; // diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js index fbbc1af25..e7b436d61 100644 --- a/frontend/src/Components/Page/ErrorPage.js +++ b/frontend/src/Components/Page/ErrorPage.js @@ -7,6 +7,7 @@ function ErrorPage(props) { const { version, isLocalStorageSupported, + hasTranslationsError, seriesError, customFiltersError, tagsError, @@ -19,6 +20,8 @@ function ErrorPage(props) { if (!isLocalStorageSupported) { errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; + } else if (hasTranslationsError) { + errorMessage = 'Failed to load translations from API'; } else if (seriesError) { errorMessage = getErrorMessage(seriesError, 'Failed to load series from API'); } else if (customFiltersError) { @@ -49,6 +52,7 @@ function ErrorPage(props) { ErrorPage.propTypes = { version: PropTypes.string.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired, + hasTranslationsError: PropTypes.bool.isRequired, seriesError: PropTypes.object, customFiltersError: PropTypes.object, tagsError: PropTypes.object, diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index a3127eddf..37c7bf8d0 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -220,6 +220,7 @@ class PageConnector extends Component { render() { const { + hasTranslationsError, isPopulated, hasError, dispatchFetchSeries, @@ -232,11 +233,12 @@ class PageConnector extends Component { ...otherProps } = this.props; - if (hasError || !this.state.isLocalStorageSupported) { + if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) { return ( ); } @@ -257,6 +259,7 @@ class PageConnector extends Component { } PageConnector.propTypes = { + hasTranslationsError: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired, diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index c7c6844a8..fcf4bc02f 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -10,6 +10,7 @@ import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; import styles from './PageSidebar.css'; @@ -20,16 +21,16 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); const links = [ { iconName: icons.SERIES_CONTINUING, - title: 'Series', + title: translate('Series'), to: '/', alias: '/series', children: [ { - title: 'Add New', + title: translate('AddNew'), to: '/add/new' }, { - title: 'Library Import', + title: translate('LibraryImport'), to: '/add/import' } ] @@ -37,26 +38,26 @@ const links = [ { iconName: icons.CALENDAR, - title: 'Calendar', + title: translate('Calendar'), to: '/calendar' }, { iconName: icons.ACTIVITY, - title: 'Activity', + title: translate('Activity'), to: '/activity/queue', children: [ { - title: 'Queue', + title: translate('Queue'), to: '/activity/queue', statusComponent: QueueStatusConnector }, { - title: 'History', + title: translate('History'), to: '/activity/history' }, { - title: 'Blocklist', + title: translate('Blocklist'), to: '/activity/blocklist' } ] @@ -64,15 +65,15 @@ const links = [ { iconName: icons.WARNING, - title: 'Wanted', + title: translate('Wanted'), to: '/wanted/missing', children: [ { - title: 'Missing', + title: translate('Missing'), to: '/wanted/missing' }, { - title: 'Cutoff Unmet', + title: translate('CutoffUnmet'), to: '/wanted/cutoffunmet' } ] @@ -80,59 +81,59 @@ const links = [ { iconName: icons.SETTINGS, - title: 'Settings', + title: translate('Settings'), to: '/settings', children: [ { - title: 'Media Management', + title: translate('MediaManagement'), to: '/settings/mediamanagement' }, { - title: 'Profiles', + title: translate('Profiles'), to: '/settings/profiles' }, { - title: 'Quality', + title: translate('Quality'), to: '/settings/quality' }, { - title: 'Custom Formats', + title: translate('CustomFormats'), to: '/settings/customformats' }, { - title: 'Indexers', + title: translate('Indexers'), to: '/settings/indexers' }, { - title: 'Download Clients', + title: translate('DownloadClients'), to: '/settings/downloadclients' }, { - title: 'Import Lists', + title: translate('ImportLists'), to: '/settings/importlists' }, { - title: 'Connect', + title: translate('Connect'), to: '/settings/connect' }, { - title: 'Metadata', + title: translate('Metadata'), to: '/settings/metadata' }, { - title: 'Metadata Source', + title: translate('MetadataSource'), to: '/settings/metadatasource' }, { - title: 'Tags', + title: translate('Tags'), to: '/settings/tags' }, { - title: 'General', + title: translate('General'), to: '/settings/general' }, { - title: 'UI', + title: translate('UI'), to: '/settings/ui' } ] @@ -140,32 +141,32 @@ const links = [ { iconName: icons.SYSTEM, - title: 'System', + title: translate('System'), to: '/system/status', children: [ { - title: 'Status', + title: translate('Status'), to: '/system/status', statusComponent: HealthStatusConnector }, { - title: 'Tasks', + title: translate('Tasks'), to: '/system/tasks' }, { - title: 'Backup', + title: translate('Backup'), to: '/system/backup' }, { - title: 'Updates', + title: translate('Updates'), to: '/system/updates' }, { - title: 'Events', + title: translate('Events'), to: '/system/events' }, { - title: 'Log Files', + title: translate('LogFiles'), to: '/system/logs/files' } ] diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 763d697cb..dd740afef 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -30,6 +30,8 @@ import { import { SelectStateInputProps } from 'typings/props'; import Rejection from 'typings/Rejection'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; +import translate from 'Utilities/String/translate'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; import styles from './InteractiveImportRow.css'; @@ -57,6 +59,7 @@ interface InteractiveImportRowProps { languages?: Language[]; size: number; customFormats?: object[]; + customFormatScore?: number; rejections: Rejection[]; columns: Column[]; episodeFileId?: number; @@ -80,6 +83,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { releaseGroup, size, customFormats, + customFormatScore, rejections, isReprocessing, isSelected, @@ -427,8 +431,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { {customFormats?.length ? ( } - title="Formats" + anchor={formatPreferredWordScore(customFormatScore)} + title={translate('CustomFormats')} body={
diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx index 2e788cd35..cdff08376 100644 --- a/frontend/src/Parse/ParseModalContent.tsx +++ b/frontend/src/Parse/ParseModalContent.tsx @@ -11,6 +11,7 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import { icons } from 'Helpers/Props'; import { clear, fetch } from 'Store/Actions/parseActions'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; import ParseResult from './ParseResult'; import parseStateSelector from './parseStateSelector'; import styles from './ParseModalContent.css'; @@ -58,7 +59,7 @@ function ParseModalContent(props: ParseModalContentProps) { return ( - Test Parsing + {translate('TestParsing')}
@@ -115,7 +116,7 @@ function ParseModalContent(props: ParseModalContentProps) { - + ); diff --git a/frontend/src/Parse/ParseResult.css b/frontend/src/Parse/ParseResult.css index d5de120fa..c49c4e3fa 100644 --- a/frontend/src/Parse/ParseResult.css +++ b/frontend/src/Parse/ParseResult.css @@ -1,20 +1,8 @@ -.item { +.container { display: flex; + flex-wrap: wrap; } -.title { - margin-right: 20px; - width: 250px; - text-align: right; - font-weight: bold; -} - -.description { - /* composes: description from '~Components/DescriptionList/DescriptionListItemTitle.css'; */ -} - -@media (max-width: $breakpointSmall) { - .item { - display: block; - } +.column { + flex: 0 0 50%; } diff --git a/frontend/src/Parse/ParseResult.css.d.ts b/frontend/src/Parse/ParseResult.css.d.ts index 13942714e..653368e06 100644 --- a/frontend/src/Parse/ParseResult.css.d.ts +++ b/frontend/src/Parse/ParseResult.css.d.ts @@ -1,9 +1,8 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'description': string; - 'item': string; - 'title': string; + 'column': string; + 'container': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Parse/ParseResult.tsx b/frontend/src/Parse/ParseResult.tsx index 575b8467f..e5dafc240 100644 --- a/frontend/src/Parse/ParseResult.tsx +++ b/frontend/src/Parse/ParseResult.tsx @@ -5,6 +5,7 @@ import EpisodeFormats from 'Episode/EpisodeFormats'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import translate from 'Utilities/String/translate'; import ParseResultItem from './ParseResultItem'; +import styles from './ParseResult.css'; interface ParseResultProps { item: ParseModel; @@ -45,11 +46,11 @@ function ParseResult(props: ParseResultProps) {
- + 0 ? seriesTitleInfo.allTitles.join(', ') @@ -66,105 +67,113 @@ function ParseResult(props: ParseResultProps) { />
- {/* - - Year - Secondary titles - special episode - - */} +
+
+
+ -
- + - + - + - + +
- +
+ - + - + - - - + +
+
- +
+
+ + 1 && !quality.revision.isRepack + ? 'True' + : '-' + } + /> - 1 ? quality.revision.version : '-'} - /> + +
- +
+ 1 ? quality.revision.version : '-' + } + /> - 1 && !quality.revision.isRepack - ? 'True' - : '-' - } - /> - - + +
+
@@ -176,7 +185,7 @@ function ParseResult(props: ParseResultProps) {
@@ -218,12 +227,12 @@ function ParseResult(props: ParseResultProps) { /> } />
diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx index 66724d852..43b8b959f 100644 --- a/frontend/src/Parse/ParseToolbarButton.tsx +++ b/frontend/src/Parse/ParseToolbarButton.tsx @@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useState } from 'react'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import { icons } from 'Helpers/Props'; import ParseModal from 'Parse/ParseModal'; +import translate from 'Utilities/String/translate'; function ParseToolbarButton() { const [isParseModalOpen, setIsParseModalOpen] = useState(false); @@ -17,7 +18,7 @@ function ParseToolbarButton() { return ( diff --git a/frontend/src/Series/Details/EpisodeRow.css b/frontend/src/Series/Details/EpisodeRow.css index b27a69f1a..4a0940362 100644 --- a/frontend/src/Series/Details/EpisodeRow.css +++ b/frontend/src/Series/Details/EpisodeRow.css @@ -56,3 +56,9 @@ width: 120px; } + +.customFormatScore { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 55px; +} diff --git a/frontend/src/Series/Details/EpisodeRow.css.d.ts b/frontend/src/Series/Details/EpisodeRow.css.d.ts index 138000856..d4a5cfe93 100644 --- a/frontend/src/Series/Details/EpisodeRow.css.d.ts +++ b/frontend/src/Series/Details/EpisodeRow.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'audio': string; 'audioLanguages': string; + 'customFormatScore': string; 'episodeNumber': string; 'episodeNumberAnime': string; 'languages': string; diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index eba223b16..4b9e827f6 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -4,6 +4,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeNumber from 'Episode/EpisodeNumber'; import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; @@ -12,7 +13,9 @@ import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; +import { tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatRuntime from 'Utilities/Number/formatRuntime'; import styles from './EpisodeRow.css'; @@ -72,6 +75,7 @@ class EpisodeRow extends Component { episodeFileSize, releaseGroup, customFormats, + customFormatScore, alternateTitles, columns } = this.props; @@ -193,6 +197,24 @@ class EpisodeRow extends Component { ); } + if (name === 'customFormatScore') { + return ( + + } + position={tooltipPositions.BOTTOM} + /> + + ); + } + if (name === 'languages') { return ( - Tags + {translate('Tags')}
- Tags + {translate('Tags')} - Apply Tags + {translate('ApplyTags')} - Result + {translate('Result')}
{seriesTags.map((id) => { @@ -124,7 +125,11 @@ function TagsModalContent(props: TagsModalContentProps) { return (