From 3a405544b744947be72df419ea354e34e72fd2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=E9=87=91=E5=8F=AF=E6=98=8E?= Date: Sun, 10 Sep 2023 17:10:45 +0200 Subject: [PATCH 1/2] feat: implement library list view --- .../Library/components/PaperListItem/index.js | 46 +++- .../PaperListItem/styles.module.css | 20 +- src/components/Library/index.js | 207 +++++++++++++++--- src/components/Library/styles.module.css | 41 ++++ .../Paper/components/InfoButton/index.js | 19 +- src/components/Sortable/index.js | 33 +++ src/components/Sortable/styles.module.css | 68 ++++++ src/constants.js | 10 +- src/helpers.js | 10 +- src/index.css | 28 ++- src/index.js | 3 + src/reducers/settings/settingsSlice.js | 16 +- src/store.js | 2 +- 13 files changed, 450 insertions(+), 53 deletions(-) create mode 100644 src/components/Sortable/index.js create mode 100644 src/components/Sortable/styles.module.css diff --git a/src/components/Library/components/PaperListItem/index.js b/src/components/Library/components/PaperListItem/index.js index fb393ef..aa22903 100644 --- a/src/components/Library/components/PaperListItem/index.js +++ b/src/components/Library/components/PaperListItem/index.js @@ -8,6 +8,8 @@ import InlineEdit from './../../../InlineEdit'; import { ReactComponent as TrashcanIcon } from './../../../../assets/icons/trashcan.svg'; import { store } from './../../../../store'; import { confirm } from '@tauri-apps/api/dialog'; +import { VIEW_MODE } from '../../../../constants'; +import { formatDate } from '../../../../helpers'; function PaperListItem(props) { const onDeletePaper = () => { @@ -20,8 +22,15 @@ function PaperListItem(props) { ); }; - const onClick = (e) => { - // Do not trigger the onClick when it's not the SVG + const onClickListViewItem = (e) => { + // Do not trigger the onClick when it's not a table cell. + if (e.target.nodeName.toLowerCase() !== 'td') return false; + + props.onClick(); + }; + + const onClickGridViewItem = (e) => { + // Do not trigger the onClick when it's not the SVG. if (e.target.nodeName.toLowerCase() !== 'svg') return false; props.onClick(); @@ -38,9 +47,36 @@ function PaperListItem(props) { } }; + if (props.viewMode === VIEW_MODE.LIST) { + return ( + + {props.index + 1} + + + + {formatDate(props.paper.updatedAt)} + {formatDate(props.paper.createdAt)} + + + + + ); + } + + // Return grid view by default return ( -
- +
+ {props.viewMode === VIEW_MODE.GRID && }
@@ -56,6 +92,8 @@ function PaperListItem(props) { PaperListItem.propTypes = { paper: PropTypes.object.isRequired, + viewMode: PropTypes.number, + index: PropTypes.number, onClick: PropTypes.func, }; diff --git a/src/components/Library/components/PaperListItem/styles.module.css b/src/components/Library/components/PaperListItem/styles.module.css index 34ddf9a..20b1642 100644 --- a/src/components/Library/components/PaperListItem/styles.module.css +++ b/src/components/Library/components/PaperListItem/styles.module.css @@ -10,11 +10,21 @@ padding-bottom: 56.25%; } +.paper-list-item--view-mode--list { + height: 4rem; + padding: 1rem; +} + +.paper-list-item--view-mode--list:hover { + cursor: pointer; +} + .paper-list-item__container:hover { box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); cursor: pointer; } +.paper-list-item--view-mode--list .paper-list-item__delete-btn, .paper-list-item__container:hover .paper-list-item__delete-btn { opacity: 1; } @@ -34,7 +44,7 @@ padding: 1rem; } -.paper-list-item__delete-btn { +.paper-list-item__container .paper-list-item__delete-btn { opacity: 0; position: absolute; bottom: -1px; @@ -57,6 +67,10 @@ border: 1px solid #353535; background-color: #292929; } + + .paper-list-item--view-mode--list:hover { + background-color: #454545; + } } @media (prefers-color-scheme: light) { @@ -69,4 +83,8 @@ border: 1px solid #d1d1d1; background-color: #fff; } + + .paper-list-item--view-mode--list:hover { + background-color: #eaeaea; + } } diff --git a/src/components/Library/index.js b/src/components/Library/index.js index 896cd78..95b9725 100644 --- a/src/components/Library/index.js +++ b/src/components/Library/index.js @@ -8,8 +8,10 @@ import { to } from './../../reducers/router/routerSlice'; import FolderListItem from './components/FolderListItem'; import PaperListItem from './components/PaperListItem'; import styles from './styles.module.css'; -import { SORT_BY } from '../../constants'; -import { setSortPapersBy } from '../../reducers/settings/settingsSlice'; +import { SORT_BY, VIEW_MODE } from '../../constants'; +import { setSortPapersBy, setViewMode } from '../../reducers/settings/settingsSlice'; +import Sortable from '../Sortable'; +import { formatDate } from '../../helpers'; class Library extends React.Component { constructor(props) { @@ -18,6 +20,7 @@ class Library extends React.Component { this.state = { currentFolderId: props.activeFolderId, sortBy: props.preferredSortBy, + viewMode: props.preferredViewMode, }; } @@ -74,12 +77,114 @@ class Library extends React.Component { this.props.dispatch(newPaperInFolder(this.state.currentFolderId)); }; - onSort = (e) => { - const sortBy = parseInt(e.target.value); + onSort = (sortBy) => { this.setState({ sortBy }); this.props.dispatch(setSortPapersBy(sortBy)); }; + onChangeViewMode = (e) => { + const viewMode = parseInt(e.target.value); + + this.setState({ viewMode }); + this.props.dispatch(setViewMode(viewMode)); + + // We display less options in grid mode for the 'sort by' filter, so + // map the missing values to existing ones. + if (viewMode === VIEW_MODE.GRID) { + if (this.state.sortBy === SORT_BY.CREATED_ASC) { + this.onSort(SORT_BY.CREATED_DESC); + } else if (this.state.sortBy === SORT_BY.LAST_MODIFIED_ASC) { + this.onSort(SORT_BY.LAST_MODIFIED_DESC); + } + } + }; + + renderPaperView = (papers) => { + if (this.state.viewMode === VIEW_MODE.LIST) { + return ( + + + + + + + + + + + + {papers.map((paper, index) => ( + this.openPaper(paper.id)} + viewMode={this.state.viewMode} + /> + ))} + +
# + this.onSort(SORT_BY.NAME_AZ)} + onSortDesc={() => this.onSort(SORT_BY.NAME_ZA)} + > + Name + + + this.onSort(SORT_BY.LAST_MODIFIED_ASC)} + onSortDesc={() => this.onSort(SORT_BY.LAST_MODIFIED_DESC)} + > + Last modified + + + this.onSort(SORT_BY.CREATED_ASC)} + onSortDesc={() => this.onSort(SORT_BY.CREATED_DESC)} + > + Created + + + +
+ ); + } + + // Render by default the grid view. + return ( +
+
+
+
+ new paper +
+
+
+ + {papers.map((paper) => ( +
+ this.openPaper(paper.id)} + viewMode={this.state.viewMode} + /> +
+ ))} +
+ ); + }; + renderPapers = () => { const folder = this.props.library.folders.find( (folder) => folder.id === this.state.currentFolderId, @@ -119,7 +224,43 @@ class Library extends React.Component { }); break; - case SORT_BY.LAST_EDIT: + case SORT_BY.CREATED_ASC: + papers = papers.sort((a, b) => { + if (dayjs(a.createdAt).isBefore(dayjs(b.createdAt))) { + return -1; + } else if (dayjs(a.createdAt).isAfter(dayjs(b.createdAt))) { + return 1; + } + + return 0; + }); + break; + + case SORT_BY.CREATED_DESC: + papers = papers.sort((a, b) => { + if (dayjs(a.createdAt).isBefore(dayjs(b.createdAt))) { + return 1; + } else if (dayjs(a.createdAt).isAfter(dayjs(b.createdAt))) { + return -1; + } + + return 0; + }); + break; + + case SORT_BY.LAST_MODIFIED_ASC: + papers = papers.sort((a, b) => { + if (dayjs(a.updatedAt).isBefore(dayjs(b.updatedAt))) { + return -1; + } else if (dayjs(a.updatedAt).isAfter(dayjs(b.updatedAt))) { + return 1; + } + + return 0; + }); + break; + + case SORT_BY.LAST_MODIFIED_DESC: default: papers = papers.sort((a, b) => { if (dayjs(a.updatedAt).isBefore(dayjs(b.updatedAt))) { @@ -140,37 +281,38 @@ class Library extends React.Component { {folder.name}
- - -
-
-
-
-
-
- new paper + {this.state.viewMode === VIEW_MODE.GRID && ( +
+ +
-
-
+ )} - {papers.map((paper) => ( -
- this.openPaper(paper.id)} /> +
+ +
- ))} +
+ {this.renderPaperView(papers)}
); }; @@ -220,6 +362,7 @@ function mapStateToProps(state) { activeFolderId: state.router.current.args.activeFolderId, appVersion: state.settings.appVersion, preferredSortBy: state.settings.sortPapersBy, + preferredViewMode: state.settings.viewMode, }; } diff --git a/src/components/Library/styles.module.css b/src/components/Library/styles.module.css index 13bf4b8..ce1be16 100644 --- a/src/components/Library/styles.module.css +++ b/src/components/Library/styles.module.css @@ -95,6 +95,27 @@ flex-wrap: wrap; } +/* Number column */ +.library__paper-list-view table tr th:nth-child(1), +.library__paper-list-view table tr td:nth-child(1) { + width: 50px; + text-align: right; +} + +/* Last modified and Created column */ +.library__paper-list-view table tr th:nth-last-child(3), +.library__paper-list-view table tr td:nth-last-child(3), +.library__paper-list-view table tr th:nth-last-child(2), +.library__paper-list-view table tr td:nth-last-child(2) { + width: 160px; +} + +/* Actions column */ +.library__paper-list-view table tr th:nth-last-child(1), +.library__paper-list-view table tr td:nth-last-child(1) { + width: 120px; +} + .library__paper-list-view__title { max-width: calc(100% - 20rem); } @@ -106,6 +127,20 @@ align-items: center; } +.library__paper-list-view__filters { + display: flex; + flex-wrap: nowrap; +} + +.library__paper-list-view__filter-group { + white-space: nowrap; + overflow: hidden; +} + +.library__paper-list-view__filter-group + .library__paper-list-view__filter-group { + margin-left: 2rem; +} + .library__paper-list-view__filters label { margin-right: 1rem; } @@ -121,3 +156,9 @@ background-color: #fff; } } + +@media (max-width: 1000px) { + .library__paper-list-view__filters label { + display: none; + } +} diff --git a/src/components/Paper/components/InfoButton/index.js b/src/components/Paper/components/InfoButton/index.js index 0fe6bac..1b50eda 100644 --- a/src/components/Paper/components/InfoButton/index.js +++ b/src/components/Paper/components/InfoButton/index.js @@ -7,6 +7,7 @@ import Modal from '../../../Modal'; import { ReactComponent as InfoIcon } from './../../../../assets/icons/info.svg'; import { formatDate } from './../../../../helpers'; import styles from './styles.module.css'; +import Tooltip from 'rc-tooltip'; class InfoButton extends React.Component { state = { @@ -61,12 +62,22 @@ class InfoButton extends React.Component {
-
Last edit
-
{formatDate(this.props.paper.updatedAt)}
+
Last modified
+ + {formatDate(this.props.paper.updatedAt)} +
-
Created at
-
{formatDate(this.props.paper.createdAt)}
+
Created
+ + {formatDate(this.props.paper.createdAt)} +
diff --git a/src/components/Sortable/index.js b/src/components/Sortable/index.js new file mode 100644 index 0000000..1d695c1 --- /dev/null +++ b/src/components/Sortable/index.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './styles.module.css'; +import classNames from 'classnames'; + +export function Sortable(props) { + return ( + + {props.children} + + + + ); +} + +Sortable.propTypes = { + sortAscActive: PropTypes.bool, + sortDescActive: PropTypes.bool, + onSortAsc: PropTypes.func.isRequired, + onSortDesc: PropTypes.func.isRequired, +}; + +export default Sortable; diff --git a/src/components/Sortable/styles.module.css b/src/components/Sortable/styles.module.css new file mode 100644 index 0000000..369d1d1 --- /dev/null +++ b/src/components/Sortable/styles.module.css @@ -0,0 +1,68 @@ +.sortable-item__arrow { + position: relative; + width: 10px; + height: 10px; + display: inline-block; +} + +.sortable-item__arrow:hover { + cursor: pointer; +} + +.sortable-item__arrow:before { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + content: ''; + width: 0; + height: 0; + border-left: 5px solid rgba(0, 0, 0, 0); + border-right: 5px solid rgba(0, 0, 0, 0); +} + +.sortable-item__sort-desc { + margin-left: 1rem; +} + +.sortable-item__sort-asc { + margin-left: 4px; +} + +.sortable-item__sort-desc:before { + border-top: 8px solid; +} + +.sortable-item__sort-asc:before { + border-bottom: 8px solid; +} + +.sortable-item__sort-desc.active:before, +.sortable-item__sort-desc:hover:before { + border-top-color: #ffb417; +} + +.sortable-item__sort-asc.active:before, +.sortable-item__sort-asc:hover:before { + border-bottom-color: #ffb417; +} + +@media (prefers-color-scheme: dark) { + .sortable-item__sort-desc:before { + border-top-color: #454545; + } + + .sortable-item__sort-asc:before { + border-bottom-color: #454545; + } +} + +@media (prefers-color-scheme: light) { + .sortable-item__sort-desc:before { + border-top-color: #c8c8c8; + } + + .sortable-item__sort-asc:before { + border-bottom-color: #c8c8c8; + } +} diff --git a/src/constants.js b/src/constants.js index 938c897..e4be811 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,10 +23,18 @@ export const KEY = { BACKSPACE: 8, }; +export const VIEW_MODE = { + GRID: 1, + LIST: 2, +}; + export const SORT_BY = { NAME_AZ: 1, NAME_ZA: 2, - LAST_EDIT: 3, + LAST_MODIFIED_ASC: 3, + LAST_MODIFIED_DESC: 4, + CREATED_ASC: 5, + CREATED_DESC: 6, }; export const BASE_DIR = BaseDirectory.App; diff --git a/src/helpers.js b/src/helpers.js index 5eb1631..ae00f22 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,7 +1,13 @@ import dayjs from 'dayjs'; -export function formatDate(date, format = 'DD-MM-YYYY HH:mm') { - return dayjs(date).format(format); +export function formatDate(date, options) { + const opts = { + format: 'YYYY-MM-DD HH:mm', + relative: true, + ...options, + }; + + return opts.relative ? dayjs(date).fromNow() : dayjs(date).format(opts.format); } export function removeDuplicates(arr) { diff --git a/src/index.css b/src/index.css index a2e71f9..2a901cc 100644 --- a/src/index.css +++ b/src/index.css @@ -85,10 +85,18 @@ hr { .rc-tooltip-placement-right .rc-tooltip-arrow { border-right-color: #f9bd3f; + left: auto !important; + right: 100%; + top: 50% !important; + transform: translateY(-50%); } .rc-tooltip-placement-left .rc-tooltip-arrow { border-left-color: #f9bd3f; + right: auto !important; + left: 100%; + top: 50% !important; + transform: translateY(-50%); } .display-flex { @@ -200,7 +208,12 @@ hr { } .text-align--right { - text-align: right; + text-align: right !important; +} + +.table { + border-collapse: collapse; + width: 100%; } .table th { @@ -212,16 +225,21 @@ hr { padding: 4px 1rem; } +.table.bordered td + td, +.table.bordered th + th { + border-left: 1px solid white; +} + +.table .kbd-shortcut { + margin-left: 0; +} + .table kbd { background-color: #f8f8f8; font-size: 1.4rem; padding: 4px 0.8rem; } -.table tr { - border-top: 1px solid #d1d1d1; -} - @media (prefers-color-scheme: dark) { hr { border-top: 1px solid #454545; diff --git a/src/index.js b/src/index.js index d89ba27..ad88585 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,9 @@ import { import reportWebVitals from './reportWebVitals'; import { store } from './store'; import { getVersion } from '@tauri-apps/api/app'; +import dayjs from 'dayjs'; + +dayjs.extend(require('dayjs/plugin/relativeTime')); // Load the library folders. invoke('load_library_folders').then((folders) => { diff --git a/src/reducers/settings/settingsSlice.js b/src/reducers/settings/settingsSlice.js index d01d825..c68519f 100644 --- a/src/reducers/settings/settingsSlice.js +++ b/src/reducers/settings/settingsSlice.js @@ -1,13 +1,14 @@ import { createSlice } from '@reduxjs/toolkit'; import { LINEWIDTH } from '../../components/Paper/constants'; -import { SORT_BY } from '../../constants'; +import { SORT_BY, VIEW_MODE } from '../../constants'; import { invoke } from '@tauri-apps/api'; const initialState = { isDarkMode: window.matchMedia('(prefers-color-scheme: dark)'), platform: null, appVersion: null, - sortPapersBy: SORT_BY.NAME_AZ, + libararysortPapersBy: SORT_BY.NAME_AZ, + viewMode: VIEW_MODE.GRID, canvasPreferredLinewidth: LINEWIDTH.SMALL, }; @@ -30,12 +31,19 @@ const settingsSlice = createSlice({ setSortPapersBy: (state, action) => { state.sortPapersBy = action.payload; }, + setViewMode: (state, action) => { + state.viewMode = action.payload; + }, loadSettings: (state, action) => { if (action.payload && typeof action.payload === 'object') { if (Object.values(SORT_BY).includes(action.payload.sortPapersBy)) { state.sortPapersBy = action.payload.sortPapersBy; } + if (Object.values(VIEW_MODE).includes(action.payload.viewMode)) { + state.viewMode = action.payload.viewMode; + } + if (Object.values(LINEWIDTH).includes(action.payload.canvasPreferredLinewidth)) { state.canvasPreferredLinewidth = action.payload.canvasPreferredLinewidth; } @@ -45,9 +53,10 @@ const settingsSlice = createSlice({ }); export const saveSettings = () => async (dispatch, getState) => { - const { sortPapersBy, canvasPreferredLinewidth } = getState().settings; + const { sortPapersBy, viewMode, canvasPreferredLinewidth } = getState().settings; const settings = { sortPapersBy, + viewMode, canvasPreferredLinewidth, }; @@ -60,6 +69,7 @@ export const { setAppVersion, loadSettings, setSortPapersBy, + setViewMode, setPreferredLinewidth, } = settingsSlice.actions; diff --git a/src/store.js b/src/store.js index 7d4b7ed..84116f0 100644 --- a/src/store.js +++ b/src/store.js @@ -36,7 +36,7 @@ const saveSettingsMiddleware = (store) => (next) => (action) => { const actionName = action.type.split('/')[1]; // Auto-save the library for every library action. - const whitelistedActions = ['setSortPapersBy', 'setPreferredLinewidth']; + const whitelistedActions = ['setSortPapersBy', 'setViewMode', 'setPreferredLinewidth']; if (reducerName === 'settings' && whitelistedActions.includes(actionName)) { store.dispatch(saveSettings()); From 5379658b42b3805e26348f8001dc523f1e14d336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=E9=87=91=E5=8F=AF=E6=98=8E?= Date: Sun, 10 Sep 2023 17:16:08 +0200 Subject: [PATCH 2/2] fix(library): remove unusd import --- src/components/Library/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Library/index.js b/src/components/Library/index.js index 95b9725..e4ea827 100644 --- a/src/components/Library/index.js +++ b/src/components/Library/index.js @@ -11,7 +11,6 @@ import styles from './styles.module.css'; import { SORT_BY, VIEW_MODE } from '../../constants'; import { setSortPapersBy, setViewMode } from '../../reducers/settings/settingsSlice'; import Sortable from '../Sortable'; -import { formatDate } from '../../helpers'; class Library extends React.Component { constructor(props) {