diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.css b/frontend/src/Search/Mobile/SearchIndexOverview.css
new file mode 100644
index 000000000..9d28a55c2
--- /dev/null
+++ b/frontend/src/Search/Mobile/SearchIndexOverview.css
@@ -0,0 +1,47 @@
+$hoverScale: 1.05;
+
+.content {
+ display: flex;
+ flex-grow: 0;
+ margin-left: 5px;
+}
+
+.container {
+ border-radius: 4px;
+ background-color: var(--cardBackgroundColor);
+}
+
+.info {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: hidden;
+}
+
+.titleRow {
+ display: flex;
+ justify-content: space-between;
+ flex: 0 0 auto;
+ margin-bottom: 10px;
+ height: 38px;
+}
+
+.indexerRow {
+ color: var(--disabledColor);
+}
+
+.infoRow {
+ margin-bottom: 5px;
+}
+
+.title {
+ width: 85%;
+ font-weight: 500;
+ font-size: 12px;
+}
+
+.actions {
+ position: absolute;
+ right: 0;
+ white-space: nowrap;
+}
diff --git a/frontend/src/Search/Mobile/SearchIndexOverview.js b/frontend/src/Search/Mobile/SearchIndexOverview.js
new file mode 100644
index 000000000..9de331e78
--- /dev/null
+++ b/frontend/src/Search/Mobile/SearchIndexOverview.js
@@ -0,0 +1,189 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import TextTruncate from 'react-text-truncate';
+import Label from 'Components/Label';
+import IconButton from 'Components/Link/IconButton';
+import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
+import { icons, kinds } from 'Helpers/Props';
+import CategoryLabel from 'Search/Table/CategoryLabel';
+import Peers from 'Search/Table/Peers';
+import ProtocolLabel from 'Search/Table/ProtocolLabel';
+import dimensions from 'Styles/Variables/dimensions';
+import formatAge from 'Utilities/Number/formatAge';
+import formatBytes from 'Utilities/Number/formatBytes';
+import translate from 'Utilities/String/translate';
+import styles from './SearchIndexOverview.css';
+
+const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
+const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
+
+function getContentHeight(rowHeight, isSmallScreen) {
+ const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
+
+ return rowHeight - padding;
+}
+
+function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
+ if (isGrabbing) {
+ return icons.SPINNER;
+ } else if (isGrabbed) {
+ return icons.DOWNLOADING;
+ } else if (grabError) {
+ return icons.DOWNLOADING;
+ }
+
+ return icons.DOWNLOAD;
+}
+
+function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
+ if (isGrabbing) {
+ return '';
+ } else if (isGrabbed) {
+ return translate('AddedToDownloadClient');
+ } else if (grabError) {
+ return grabError;
+ }
+
+ return translate('AddToDownloadClient');
+}
+
+class SearchIndexOverview extends Component {
+
+ //
+ // Listeners
+
+ onEditSeriesPress = () => {
+ this.setState({ isEditSeriesModalOpen: true });
+ };
+
+ onEditSeriesModalClose = () => {
+ this.setState({ isEditSeriesModalOpen: false });
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ title,
+ protocol,
+ downloadUrl,
+ categories,
+ seeders,
+ leechers,
+ size,
+ age,
+ ageHours,
+ ageMinutes,
+ indexer,
+ rowHeight,
+ isSmallScreen,
+ isGrabbed,
+ isGrabbing,
+ grabError
+ } = this.props;
+
+ const contentHeight = getContentHeight(rowHeight, isSmallScreen);
+
+ return (
+
+
+
+
+
+ {indexer}
+
+
+
+
+ {
+ protocol === 'torrent' &&
+
+ }
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SearchIndexOverview.propTypes = {
+ guid: PropTypes.string.isRequired,
+ categories: PropTypes.arrayOf(PropTypes.object).isRequired,
+ protocol: PropTypes.string.isRequired,
+ age: PropTypes.number.isRequired,
+ ageHours: PropTypes.number.isRequired,
+ ageMinutes: PropTypes.number.isRequired,
+ publishDate: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ infoUrl: PropTypes.string.isRequired,
+ downloadUrl: PropTypes.string.isRequired,
+ indexerId: PropTypes.number.isRequired,
+ indexer: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired,
+ files: PropTypes.number,
+ grabs: PropTypes.number,
+ seeders: PropTypes.number,
+ leechers: PropTypes.number,
+ indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
+ rowHeight: PropTypes.number.isRequired,
+ showRelativeDates: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ isSmallScreen: PropTypes.bool.isRequired,
+ onGrabPress: PropTypes.func.isRequired,
+ isGrabbing: PropTypes.bool.isRequired,
+ isGrabbed: PropTypes.bool.isRequired,
+ grabError: PropTypes.string
+};
+
+SearchIndexOverview.defaultProps = {
+ isGrabbing: false,
+ isGrabbed: false
+};
+
+export default SearchIndexOverview;
diff --git a/frontend/src/Search/Mobile/SearchIndexOverviews.css b/frontend/src/Search/Mobile/SearchIndexOverviews.css
new file mode 100644
index 000000000..f393dcbb0
--- /dev/null
+++ b/frontend/src/Search/Mobile/SearchIndexOverviews.css
@@ -0,0 +1,11 @@
+.grid {
+ flex: 1 0 auto;
+}
+
+.container {
+ &:hover {
+ .content {
+ background-color: var(--tableRowHoverBackgroundColor);
+ }
+ }
+}
diff --git a/frontend/src/Search/Mobile/SearchIndexOverviews.js b/frontend/src/Search/Mobile/SearchIndexOverviews.js
new file mode 100644
index 000000000..c8e5d1007
--- /dev/null
+++ b/frontend/src/Search/Mobile/SearchIndexOverviews.js
@@ -0,0 +1,211 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { Grid, WindowScroller } from 'react-virtualized';
+import Measure from 'Components/Measure';
+import SearchIndexItemConnector from 'Search/Table/SearchIndexItemConnector';
+import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
+import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
+import SearchIndexOverview from './SearchIndexOverview';
+import styles from './SearchIndexOverviews.css';
+
+class SearchIndexOverviews extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ width: 0,
+ columnCount: 1,
+ rowHeight: 100,
+ scrollRestored: false
+ };
+
+ this._grid = null;
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const {
+ items,
+ sortKey,
+ jumpToCharacter,
+ scrollTop,
+ isSmallScreen
+ } = this.props;
+
+ const {
+ width,
+ rowHeight,
+ scrollRestored
+ } = this.state;
+
+ if (prevProps.sortKey !== sortKey) {
+ this.calculateGrid(this.state.width, isSmallScreen);
+ }
+
+ if (
+ this._grid &&
+ (prevState.width !== width ||
+ prevState.rowHeight !== rowHeight ||
+ hasDifferentItemsOrOrder(prevProps.items, items)
+ )
+ ) {
+ // recomputeGridSize also forces Grid to discard its cache of rendered cells
+ this._grid.recomputeGridSize();
+ }
+
+ if (this._grid && scrollTop !== 0 && !scrollRestored) {
+ this.setState({ scrollRestored: true });
+ this._grid.scrollToPosition({ scrollTop });
+ }
+
+ if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
+ const index = getIndexOfFirstCharacter(items, jumpToCharacter);
+
+ if (this._grid && index != null) {
+
+ this._grid.scrollToCell({
+ rowIndex: index,
+ columnIndex: 0
+ });
+ }
+ }
+ }
+
+ //
+ // Control
+
+ setGridRef = (ref) => {
+ this._grid = ref;
+ };
+
+ calculateGrid = (width = this.state.width, isSmallScreen) => {
+ const rowHeight = 100;
+
+ this.setState({
+ width,
+ rowHeight
+ });
+ };
+
+ cellRenderer = ({ key, rowIndex, style }) => {
+ const {
+ items,
+ sortKey,
+ showRelativeDates,
+ shortDateFormat,
+ longDateFormat,
+ timeFormat,
+ isSmallScreen,
+ onGrabPress
+ } = this.props;
+
+ const {
+ rowHeight
+ } = this.state;
+
+ const release = items[rowIndex];
+
+ return (
+
+
+
+ );
+ };
+
+ //
+ // Listeners
+
+ onMeasure = ({ width }) => {
+ this.calculateGrid(width, this.props.isSmallScreen);
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ items
+ } = this.props;
+
+ const {
+ width,
+ rowHeight
+ } = this.state;
+
+ return (
+
+
+ {({ height, registerChild, onChildScroll, scrollTop }) => {
+ if (!height) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+ }
+ }
+
+
+ );
+ }
+}
+
+SearchIndexOverviews.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ sortKey: PropTypes.string,
+ scrollTop: PropTypes.number.isRequired,
+ jumpToCharacter: PropTypes.string,
+ scroller: PropTypes.instanceOf(Element).isRequired,
+ showRelativeDates: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ isSmallScreen: PropTypes.bool.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ onGrabPress: PropTypes.func.isRequired
+};
+
+export default SearchIndexOverviews;
diff --git a/frontend/src/Search/Mobile/SearchIndexOverviewsConnector.js b/frontend/src/Search/Mobile/SearchIndexOverviewsConnector.js
new file mode 100644
index 000000000..39f376e55
--- /dev/null
+++ b/frontend/src/Search/Mobile/SearchIndexOverviewsConnector.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { grabRelease } from 'Store/Actions/releaseActions';
+import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import SearchIndexOverviews from './SearchIndexOverviews';
+
+function createMapStateToProps() {
+ return createSelector(
+ createUISettingsSelector(),
+ createDimensionsSelector(),
+ (uiSettings, dimensions) => {
+ return {
+ showRelativeDates: uiSettings.showRelativeDates,
+ shortDateFormat: uiSettings.shortDateFormat,
+ longDateFormat: uiSettings.longDateFormat,
+ timeFormat: uiSettings.timeFormat,
+ isSmallScreen: dimensions.isSmallScreen
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onGrabPress(payload) {
+ dispatch(grabRelease(payload));
+ }
+ };
+}
+
+export default connect(createMapStateToProps, createMapDispatchToProps)(SearchIndexOverviews);
diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js
index 5a7d9d484..884e279d2 100644
--- a/frontend/src/Search/SearchIndex.js
+++ b/frontend/src/Search/SearchIndex.js
@@ -11,8 +11,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
-import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
-import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -23,12 +21,17 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import SearchIndexFilterMenu from './Menus/SearchIndexFilterMenu';
import SearchIndexSortMenu from './Menus/SearchIndexSortMenu';
+import SearchIndexOverviewsConnector from './Mobile/SearchIndexOverviewsConnector';
import NoSearchResults from './NoSearchResults';
import SearchFooterConnector from './SearchFooterConnector';
import SearchIndexTableConnector from './Table/SearchIndexTableConnector';
import styles from './SearchIndex.css';
-function getViewComponent() {
+function getViewComponent(isSmallScreen) {
+ if (isSmallScreen) {
+ return SearchIndexOverviewsConnector;
+ }
+
return SearchIndexTableConnector;
}
@@ -44,8 +47,6 @@ class SearchIndex extends Component {
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
- isAddIndexerModalOpen: false,
- isEditIndexerModalOpen: false,
searchType: null,
lastToggled: null,
allSelected: false,
@@ -177,21 +178,6 @@ class SearchIndex extends Component {
//
// Listeners
- onAddIndexerPress = () => {
- this.setState({ isAddIndexerModalOpen: true });
- };
-
- onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
- this.setState({
- isAddIndexerModalOpen: false,
- isEditIndexerModalOpen: indexerSelected
- });
- };
-
- onEditIndexerModalClose = () => {
- this.setState({ isEditIndexerModalOpen: false });
- };
-
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
};
@@ -253,6 +239,7 @@ class SearchIndex extends Component {
onScroll,
onSortSelect,
onFilterSelect,
+ isSmallScreen,
hasIndexers,
...otherProps
} = this.props;
@@ -260,8 +247,6 @@ class SearchIndex extends Component {
const {
scroller,
jumpBarItems,
- isAddIndexerModalOpen,
- isEditIndexerModalOpen,
jumpToCharacter,
selectedState,
allSelected,
@@ -270,7 +255,7 @@ class SearchIndex extends Component {
const selectedIndexerIds = this.getSelectedIds();
- const ViewComponent = getViewComponent();
+ const ViewComponent = getViewComponent(isSmallScreen);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoIndexer = !totalItems;
@@ -384,16 +369,6 @@ class SearchIndex extends Component {
onSearchPress={this.onSearchPress}
onBulkGrabPress={this.onBulkGrabPress}
/>
-
-
-
-
);
}
diff --git a/frontend/src/Search/Table/SearchIndexItemConnector.js b/frontend/src/Search/Table/SearchIndexItemConnector.js
index 04db22740..da1133b6d 100644
--- a/frontend/src/Search/Table/SearchIndexItemConnector.js
+++ b/frontend/src/Search/Table/SearchIndexItemConnector.js
@@ -18,20 +18,20 @@ function createMapStateToProps() {
return createSelector(
createReleaseSelector(),
(
- movie
+ release
) => {
- // If a movie is deleted this selector may fire before the parent
- // selecors, which will result in an undefined movie, if that happens
+ // If a release is deleted this selector may fire before the parent
+ // selecors, which will result in an undefined release, if that happens
// we want to return early here and again in the render function to avoid
- // trying to show a movie that has no information available.
+ // trying to show a release that has no information available.
- if (!movie) {
+ if (!release) {
return {};
}
return {
- ...movie
+ ...release
};
}
);
diff --git a/package.json b/package.json
index 3ed9997aa..57c4e0e24 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"react-redux": "8.0.5",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
+ "react-text-truncate": "0.19.0",
"react-virtualized": "9.21.1",
"redux": "4.2.0",
"redux-actions": "2.6.5",
diff --git a/yarn.lock b/yarn.lock
index 5162372bf..573ee0d51 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4714,7 +4714,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-prop-types@15.8.1, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@15.8.1, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -4996,6 +4996,13 @@ react-side-effect@^1.0.2:
dependencies:
shallowequal "^1.0.1"
+react-text-truncate@0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.19.0.tgz#60bc5ecf29a03ebc256f31f90a2d8402176aac91"
+ integrity sha512-QxHpZABfGG0Z3WEYbRTZ+rXdZn50Zvp+sWZXgVAd7FCKAMzv/kcwctTpNmWgXDTpAoHhMjOVwmgRtX3x5yeF4w==
+ dependencies:
+ prop-types "^15.5.7"
+
react-themeable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e"