diff --git a/package-lock.json b/package-lock.json index 2791c17990..f14494f6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1524,9 +1524,9 @@ } }, "@mdi/js": { - "version": "4.9.95", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-4.9.95.tgz", - "integrity": "sha512-6zKTCqZUCuDWJThdRcjdFTqp2BXfYwXI1UlYa68A1/kmCcy1ijpbpRbrJcUdZ+9WojencCh1DOGFqhj/x8I/eQ==" + "version": "5.1.45", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-5.1.45.tgz", + "integrity": "sha512-44He8VMteUIWE0lIZjAIH/QoxNXbGoeqA1uwzJKwRpiaU6d5n2JjvL+11uH5nQQTJtU1nQFdKpB/j5S6rM3jtQ==" }, "@mdi/react": { "version": "1.3.0", @@ -3002,7 +3002,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { diff --git a/package.json b/package.json index 63a689fc5e..93c43e69a2 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "webpack-dev-server": "^3.8.1" }, "dependencies": { - "@mdi/js": "^4.4.95", + "@mdi/js": "^5.1.45", "@mdi/react": "^1.2.1", "@svgr/cli": "^4.3.3", "@svgr/webpack": "^4.3.3", diff --git a/src/components/App.tsx b/src/components/App.tsx index e42c48381e..4a315708b7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,9 +1,9 @@ import React, { useMemo, useCallback } from 'react' -import SideNavigator from './SideNavigator' +import Navigator from './organisms/Navigator' import Router from './Router' import GlobalStyle from './GlobalStyle' import { ThemeProvider } from 'styled-components' -import { defaultTheme } from '../themes/default' +import { legacyTheme } from '../themes/legacy' import { darkTheme } from '../themes/dark' import { lightTheme } from '../themes/light' import { sepiaTheme } from '../themes/sepia' @@ -19,7 +19,6 @@ import '../lib/i18n' import '../lib/analytics' import CodeMirrorStyle from './CodeMirrorStyle' import { useGeneralStatus } from '../lib/generalStatus' -import Modal from './Modal' import ToastList from './Toast' import styled from '../lib/styled' import { useEffectOnce } from 'react-use' @@ -62,7 +61,7 @@ const App = () => { }, [toggleClosed]) useGlobalKeyDownHandler(keyboardHandler) const { generalStatus, setGeneralStatus } = useGeneralStatus() - const updateSideBarWidth = useCallback( + const updateNavWidth = useCallback( (leftWidth: number) => { setGeneralStatus({ sideBarWidth: leftWidth, @@ -80,9 +79,9 @@ const App = () => { {initialized ? ( } + left={} right={} - onResizeEnd={updateSideBarWidth} + onResizeEnd={updateNavWidth} /> ) : ( Loading Data... @@ -91,7 +90,6 @@ const App = () => { - @@ -100,16 +98,17 @@ const App = () => { } function selectTheme(theme: string) { switch (theme) { - case 'dark': - return darkTheme + case 'legacy': + return legacyTheme case 'light': return lightTheme case 'sepia': return sepiaTheme case 'solarizedDark': return solarizedDarkTheme + case 'dark': default: - return defaultTheme + return darkTheme } } diff --git a/src/components/GlobalStyle.tsx b/src/components/GlobalStyle.tsx index 88c44982a0..183d2696c3 100644 --- a/src/components/GlobalStyle.tsx +++ b/src/components/GlobalStyle.tsx @@ -7,8 +7,9 @@ export default createGlobalStyle<{ theme: BaseTheme }>` margin: 0; ${backgroundColor} ${textColor} - font-family: ${({ theme }) => theme.fontFamily}; - font-size: ${({ theme }) => theme.fontSize}px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Fira sans', Roboto, Helvetica, + Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 15px; font-weight: 400; } @@ -20,8 +21,8 @@ export default createGlobalStyle<{ theme: BaseTheme }>` outline: none; } - input { - font-size: ${({ theme }) => theme.fontSize}px; + input, button { + font-size: 15px; } h1,h2,h3,h4,h5,h6 { diff --git a/src/components/Modal/contents/DownloadOurAppModal.tsx b/src/components/Modal/contents/DownloadOurAppModal.tsx deleted file mode 100644 index 513ce78e6a..0000000000 --- a/src/components/Modal/contents/DownloadOurAppModal.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import { - ModalContainer, - ModalHeader, - ModalSubtitle, - ModalBody, - ModalFlex, -} from './styled' -import Image from '../../atoms/Image' -import AppLink from '../../atoms/AppLink' -import { openNew } from '../../../lib/platform' - -const DownloadOurAppModal = () => { - return ( - - Download our apps - - Use Boostnote on your local and focus on your work! - - - -
- - -
-
- -
- - -
-
-
-
-
- ) -} - -export default DownloadOurAppModal diff --git a/src/components/Modal/contents/styled.ts b/src/components/Modal/contents/styled.ts deleted file mode 100644 index cc83b22003..0000000000 --- a/src/components/Modal/contents/styled.ts +++ /dev/null @@ -1,80 +0,0 @@ -import styled from '../../../lib/styled' -import { textColor } from '../../../lib/styled/styleFunctions' - -export const ModalContainer = styled.div` - width: 100%; - ${textColor} - padding: 3px; -` - -export const ModalHeader = styled.h1` - text-align: center; - margin-top: 10px; - margin-bottom: 10px; -` - -export const ModalSubtitle = styled.h4` - text-align: center; - margin-bottom: 8vh; -` - -export const ModalBody = styled.div` - margin-top: 20px; - padding: 0 2%; - - .button { - display: block; - background-color: rgb(3, 197, 136); - font-size: 16px; - line-height: 1; - text-transform: uppercase; - color: rgb(255, 255, 255); - padding: 16px 32px !important; - border-width: initial; - border-style: none; - border-color: initial; - border-image: initial; - border-radius: 2px; - margin-bottom: 10px; - height: auto; - margin: 0 auto; - - &:not(:disabled):hover { - cursor: pointer; - opacity: 0.8; - } - - &.darker { - background-color: #d7d7d7; - color: #000; - } - - .subtext { - font-size: 12px; - } - } -` - -export const ModalFlex = styled.div` - width: 100%; - display: flex; - flex-wrap: no-wrap; - justify-content: space-between; - align-items: top; - - div { - flex: 1 1 0; - } - - img { - max-width: 100%; - width: auto; - height: 30vh; - display: block; - margin: auto; - } - - .center { - text-align: center; - } -` diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx deleted file mode 100644 index 902e342e20..0000000000 --- a/src/components/Modal/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import { useGlobalKeyDownHandler } from '../../lib/keyboard' -import { useModal } from '../../lib/modal' -import { - StyledModalsBackground, - StyledModalsContainer, - StyledModalsSkipButton, -} from './styled' -import { usePreferences } from '../../lib/preferences' -import DownloadOurAppModal from './contents/DownloadOurAppModal' -import { IconArrowSingleRight } from '../icons' - -interface ModalsRenderingOptions { - closable: boolean - body: JSX.Element - onSkip?: () => void -} - -export default () => { - const { modalContent, closeModal } = useModal() - const { setPreferences } = usePreferences() - - const content = useMemo((): ModalsRenderingOptions => { - const basicModal: ModalsRenderingOptions = { - closable: true, - body: <>, - } - - switch (modalContent) { - case 'download-app': - basicModal.body = - basicModal.onSkip = () => { - setPreferences({ - 'general.enableDownloadAppModal': false, - }) - closeModal() - } - break - default: - break - } - - return basicModal - }, [modalContent, setPreferences, closeModal]) - - const closeHandler = useCallback(() => { - if (content.onSkip != null) { - return content.onSkip() - } - return closeModal() - }, [closeModal, content]) - - const keydownHandler = useMemo(() => { - return (event: KeyboardEvent) => { - if (event.key === 'Escape') { - closeHandler() - } - } - }, [closeHandler]) - useGlobalKeyDownHandler(keydownHandler) - - const backgroundClickHandler = useMemo(() => { - return (event: React.MouseEvent) => { - event.preventDefault() - closeHandler() - } - }, [closeHandler]) - - if (modalContent == null) return null - - return ( - <> - - - {content.body} - - {content.closable && ( - - - Skip - - - )} - - - ) -} diff --git a/src/components/Modal/styled.ts b/src/components/Modal/styled.ts deleted file mode 100644 index ec57911afc..0000000000 --- a/src/components/Modal/styled.ts +++ /dev/null @@ -1,68 +0,0 @@ -import styled from '../../lib/styled' -import { - border, - backgroundColor, - contextMenuShadow, - iconColor, -} from '../../lib/styled/styleFunctions' - -const zIndexModalsBackground = 8001 - -export const StyledModalsBackground = styled.div` - z-index: ${zIndexModalsBackground}; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - ${backgroundColor}; - opacity: 0.8; - display: flex; - overflow: hidden; -` - -export const StyledModalsContainer = styled.div` - z-index: ${zIndexModalsBackground + 1}; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - width: 50vw; - height: 70vh; - ${border} - ${backgroundColor} - ${contextMenuShadow} - border-radius: 2px; - display: flex; - overflow: hidden; -` - -export const StyledModalsHeader = styled.h1` - margin: 0; - padding: 1em 0; -` - -export const StyledModalsSkipButton = styled.button` - position: absolute; - bottom: 0; - right: 2%; - width: auto; - height: 40px; - background-color: transparent; - border: none; - font-size: 16px; - white-space: nowrap; - cursor: pointer; - ${iconColor} - - span { - line-height: 20px; - vertical-align: middle; - } - - .icon { - vertical-align: middle; - } -` diff --git a/src/components/PreferencesModal/GeneralTab.tsx b/src/components/PreferencesModal/GeneralTab.tsx index de2657df48..182721f2b5 100644 --- a/src/components/PreferencesModal/GeneralTab.tsx +++ b/src/components/PreferencesModal/GeneralTab.tsx @@ -11,7 +11,6 @@ import { GeneralThemeOptions, GeneralLanguageOptions, GeneralNoteSortingOptions, - GeneralTutorialsOptions, } from '../../lib/preferences' import { useTranslation } from 'react-i18next' import { SelectChangeEventHandler } from '../../lib/events' @@ -55,15 +54,6 @@ const GeneralTab = () => { [setPreferences] ) - const selectTutorialsDisplay: SelectChangeEventHandler = useCallback( - (event) => { - setPreferences({ - 'general.tutorials': event.target.value as GeneralTutorialsOptions, - }) - }, - [setPreferences] - ) - const toggleEnableAutoSync: React.ChangeEventHandler = useCallback( (event) => { setPreferences({ @@ -129,10 +119,10 @@ const GeneralTab = () => { value={preferences['general.theme']} onChange={selectTheme} > - - + + @@ -152,18 +142,6 @@ const GeneralTab = () => { -
- {t('preferences.displayTutorialsLabel')} - - - - - - -
Enable auto sync diff --git a/src/components/PreferencesModal/ImportTab.tsx b/src/components/PreferencesModal/ImportTab.tsx index 9c2e773154..442a274aa5 100644 --- a/src/components/PreferencesModal/ImportTab.tsx +++ b/src/components/PreferencesModal/ImportTab.tsx @@ -1,232 +1,246 @@ -import React, { useState, useMemo, useCallback } from 'react' -import { - Section, - SectionHeader, - SectionControl, - SectionSelect, - SectionPrimaryButton, -} from './styled' -import { useDb } from '../../lib/db' -import { entries } from '../../lib/db/utils' -import FileDropZone from '../atoms/FileDropZone' -import { - convertCSONFileToNote, - ParsedNote, - ParseErrors, -} from '../../lib/legacy-import' -import styled from '../../lib/styled' -import { - secondaryBackgroundColor, - border, - textColor, -} from '../../lib/styled/styleFunctions' -import { useToast } from '../../lib/toast' -import { useTranslation } from 'react-i18next' - -interface Success { - err: false - note: ParsedNote -} - -interface Failure { - err: true - filename: string - type: ParseErrors -} - -type FileImport = Failure | Success - -const StyledDropZone = styled.div` - width: 80%; - height: 150px; - ${textColor} - &.active { - ${border} - } - ${secondaryBackgroundColor} - overflow-y: scroll; -` - -const StyledRemove = styled.span` - padding-left: 1em; - text-decoration: underline; - cursor: pointer; - color: ${({ theme }) => theme.primaryColor}; -` - -export default () => { - const { t } = useTranslation() - const { pushMessage } = useToast() - const { storageMap, createNote } = useDb() - const storageEntries = useMemo(() => entries(storageMap), [storageMap]) - const [activeStorage, setActiveStorage] = useState(() => { - if (storageEntries.length < 1) { - return - } - return storageEntries[0][1] - }) - const folderEntries = useMemo(() => { - return activeStorage != null ? entries(activeStorage.folderMap) : [] - }, [activeStorage]) - const [activeFolder, setActiveFolder] = useState(() => { - return folderEntries.length > 0 ? folderEntries[0][1] : undefined - }) - const [fileImports, setFileImports] = useState>( - new Map() - ) - const importEntries = useMemo(() => [...fileImports.entries()], [fileImports]) - const [dragInside, setDragInside] = useState(false) - - const updateActiveStorage = useCallback( - (e: React.ChangeEvent) => { - setActiveStorage(storageMap[e.target.value]) - }, - [setActiveStorage, storageMap] - ) - - const updateActiveFolder = useCallback( - (e: React.ChangeEvent) => { - if (activeStorage == null) { - return - } - setActiveFolder(activeStorage.folderMap[e.target.value]) - }, - [setActiveFolder, activeStorage] - ) - - const uploadCallback = useCallback(async () => { - if (activeFolder == null || activeStorage == null) { - return - } - - const created = await Promise.all( - importEntries - .map(([, entry]) => entry) - .filter(({ err }) => !err) - .map((entry) => - createNote(activeStorage.id, { - folderPathname: activeFolder.pathname, - ...(entry as Success).note, - }) - ) - ) - - const title = `Successfully imported ${created.length} notes` - const description = created - .map((note) => (note == null ? '' : note.title)) - .join('\n') - - pushMessage({ title, description }) - - setFileImports(new Map()) - }, [ - createNote, - activeStorage, - activeFolder, - importEntries, - setFileImports, - pushMessage, - ]) - - const draggedInCallback = useCallback(() => setDragInside(true), [ - setDragInside, - ]) - const draggedOutCallback = useCallback(() => setDragInside(false), [ - setDragInside, - ]) - - const fileDropCallback = useCallback( - (files: File[]) => { - setDragInside(false) - files.forEach(async (file) => { - const result = await convertCSONFileToNote(file) - if (!result.err) { - setFileImports( - new Map( - fileImports.set(file.name, { err: false, note: result.data }) - ) - ) - } else { - setFileImports( - new Map( - fileImports.set(file.name, { - err: true, - filename: file.name, - type: result.data, - }) - ) - ) - } - }) - }, - [setFileImports, fileImports, setDragInside] - ) - - const importRemoveCallback = (id: string) => { - fileImports.delete(id) - setFileImports(new Map(fileImports)) - } - +import React from 'react' +const ImportTab = () => { return ( -
- {t('preferences.import')} -

{t('preferences.description')}

-

{t('preferences.importFlow1')}

-

{t('preferences.importFlow2')}

-

{t('preferences.importFlow3')}

-

{t('preferences.importFlow4')}

- - - -
    - {importEntries.map(([id, entry]) => { - return ( -
  • - {entry.err - ? `Invalid File: ${entry.filename}` - : `Ready to import: ${entry.note.title}`} - importRemoveCallback(id)}> - {t('preferences.importRemove')} - -
  • - ) - })} -
-
-
-
- - - - - - - - {t('preferences.importUpload')} - -
+
+

+ Importing from the legacy app will be implemented along with File System + based storage before the end of June 2020. +

+ +

Until it is ready, please keep using the old app.

+
) } + +export default ImportTab +// import { +// Section, +// SectionHeader, +// SectionControl, +// SectionSelect, +// SectionPrimaryButton, +// } from './styled' +// import { useDb } from '../../lib/db' +// import { entries } from '../../lib/db/utils' +// import FileDropZone from '../atoms/FileDropZone' +// import { +// convertCSONFileToNote, +// ParsedNote, +// ParseErrors, +// } from '../../lib/legacy-import' +// import styled from '../../lib/styled' +// import { +// secondaryBackgroundColor, +// border, +// textColor, +// } from '../../lib/styled/styleFunctions' +// import { useToast } from '../../lib/toast' +// import { useTranslation } from 'react-i18next' + +// interface Success { +// err: false +// note: ParsedNote +// } + +// interface Failure { +// err: true +// filename: string +// type: ParseErrors +// } + +// type FileImport = Failure | Success + +// const StyledDropZone = styled.div` +// width: 80%; +// height: 150px; +// ${textColor} +// &.active { +// ${border} +// } +// ${secondaryBackgroundColor} +// overflow-y: scroll; +// ` + +// const StyledRemove = styled.span` +// padding-left: 1em; +// text-decoration: underline; +// cursor: pointer; +// color: ${({ theme }) => theme.primaryColor}; +// ` + +// export default () => { +// const { t } = useTranslation() +// const { pushMessage } = useToast() +// const { storageMap, createNote } = useDb() +// const storageEntries = useMemo(() => entries(storageMap), [storageMap]) +// const [activeStorage, setActiveStorage] = useState(() => { +// if (storageEntries.length < 1) { +// return +// } +// return storageEntries[0][1] +// }) +// const folderEntries = useMemo(() => { +// return activeStorage != null ? entries(activeStorage.folderMap) : [] +// }, [activeStorage]) +// const [activeFolder, setActiveFolder] = useState(() => { +// return folderEntries.length > 0 ? folderEntries[0][1] : undefined +// }) +// const [fileImports, setFileImports] = useState>( +// new Map() +// ) +// const importEntries = useMemo(() => [...fileImports.entries()], [fileImports]) +// const [dragInside, setDragInside] = useState(false) + +// const updateActiveStorage = useCallback( +// (e: React.ChangeEvent) => { +// setActiveStorage(storageMap[e.target.value]) +// }, +// [setActiveStorage, storageMap] +// ) + +// const updateActiveFolder = useCallback( +// (e: React.ChangeEvent) => { +// if (activeStorage == null) { +// return +// } +// setActiveFolder(activeStorage.folderMap[e.target.value]) +// }, +// [setActiveFolder, activeStorage] +// ) + +// const uploadCallback = useCallback(async () => { +// if (activeFolder == null || activeStorage == null) { +// return +// } + +// const created = await Promise.all( +// importEntries +// .map(([, entry]) => entry) +// .filter(({ err }) => !err) +// .map((entry) => +// createNote(activeStorage.id, { +// folderPathname: activeFolder.pathname, +// ...(entry as Success).note, +// }) +// ) +// ) + +// const title = `Successfully imported ${created.length} notes` +// const description = created +// .map((note) => (note == null ? '' : note.title)) +// .join('\n') + +// pushMessage({ title, description }) + +// setFileImports(new Map()) +// }, [ +// createNote, +// activeStorage, +// activeFolder, +// importEntries, +// setFileImports, +// pushMessage, +// ]) + +// const draggedInCallback = useCallback(() => setDragInside(true), [ +// setDragInside, +// ]) +// const draggedOutCallback = useCallback(() => setDragInside(false), [ +// setDragInside, +// ]) + +// const fileDropCallback = useCallback( +// (files: File[]) => { +// setDragInside(false) +// files.forEach(async (file) => { +// const result = await convertCSONFileToNote(file) +// if (!result.err) { +// setFileImports( +// new Map( +// fileImports.set(file.name, { err: false, note: result.data }) +// ) +// ) +// } else { +// setFileImports( +// new Map( +// fileImports.set(file.name, { +// err: true, +// filename: file.name, +// type: result.data, +// }) +// ) +// ) +// } +// }) +// }, +// [setFileImports, fileImports, setDragInside] +// ) + +// const importRemoveCallback = (id: string) => { +// fileImports.delete(id) +// setFileImports(new Map(fileImports)) +// } + +// return ( +//
+// {t('preferences.import')} +//

{t('preferences.description')}

+//

{t('preferences.importFlow1')}

+//

{t('preferences.importFlow2')}

+//

{t('preferences.importFlow3')}

+//

{t('preferences.importFlow4')}

+// +// +// +//
    +// {importEntries.map(([id, entry]) => { +// return ( +//
  • +// {entry.err +// ? `Invalid File: ${entry.filename}` +// : `Ready to import: ${entry.note.title}`} +// importRemoveCallback(id)}> +// {t('preferences.importRemove')} +// +//
  • +// ) +// })} +//
+//
+//
+//
+// +// +// +// +// +// +// +// {t('preferences.importUpload')} +// +//
+// ) +// } diff --git a/src/components/Router.tsx b/src/components/Router.tsx index 5b13d96a55..0ac78b45e4 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -5,7 +5,6 @@ import StorageCreatePage from './pages/StorageCreatePage' import StorageEditPage from './pages/StorageEditPage' import { useDb } from '../lib/db' import AttachmentsPage from './pages/AttachmentsPage' -import TutorialsPage from './Tutorials/TutorialsPage' import useRedirectHandler from '../lib/router/redirect' import styled from '../lib/styled' @@ -20,8 +19,6 @@ export default () => { useRedirectHandler() switch (routeParams.name) { - case 'storages.allNotes': - case 'storages.bookmarks': case 'storages.notes': case 'storages.trashCan': case 'storages.tags.show': @@ -30,9 +27,7 @@ export default () => { return case 'storages.create': return - case 'tutorials.show': - return - case 'storages.edit': + case 'storages.settings': const storage = db.storageMap[routeParams.storageId] if (storage != null) { return diff --git a/src/components/SideNavigator/ControlButton.tsx b/src/components/SideNavigator/ControlButton.tsx deleted file mode 100644 index 7da79408fb..0000000000 --- a/src/components/SideNavigator/ControlButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import { - sideBarTextColor, - sideBarSecondaryTextColor, -} from '../../lib/styled/styleFunctions' - -const StyledButton = styled.button` - position: relative; - right: 9px; - width: 30px; - padding: 0; - border: none; - background-color: transparent; - border-radius: 2px; - font-size: 20px; - line-height: 20px; - cursor: pointer; - vertical-align: middle; - ${sideBarTextColor} - ${sideBarSecondaryTextColor} - &:hover, &:active, &:focus { - box-shadow: none; - } - + button { - top: -1px; - } -` - -interface ControlButtonProps { - icon: React.ReactNode - onClick?: (event: React.MouseEvent) => void -} - -const ControlButton = ({ icon, onClick }: ControlButtonProps) => { - return {icon} -} - -export default ControlButton diff --git a/src/components/SideNavigator/SideNavigator.tsx b/src/components/SideNavigator/SideNavigator.tsx deleted file mode 100644 index b7c9dda073..0000000000 --- a/src/components/SideNavigator/SideNavigator.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' -import { useDb } from '../../lib/db' -import { entries } from '../../lib/db/utils' -import styled from '../../lib/styled' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { useContextMenu, MenuTypes } from '../../lib/contextMenu' -import { usePreferences } from '../../lib/preferences' -import { - sideBarBackgroundColor, - sideBarDefaultTextColor, - iconColor, - sideBarTextColor, -} from '../../lib/styled/styleFunctions' -import SideNavigatorItem from '../molecules/SideNavigatorItem' -import { useGeneralStatus } from '../../lib/generalStatus' -import ControlButton from './ControlButton' -import FolderListFragment from './FolderListFragment' -import TagListFragment from './TagListFragment' -import TutorialsNavigator from '../Tutorials/TutorialsNavigator' -import { useUsers } from '../../lib/accounts' -import { useToast } from '../../lib/toast' -import { useTranslation } from 'react-i18next' -import { - IconAddRound, - IconAdjustVertical, - IconArrowAgain, - IconTrash, - IconImage, - IconSetting, - IconBook, - IconStarActive, -} from '../icons' -import { getStorageItemId } from '../../lib/nav' - -const Description = styled.nav` - margin-left: 15px; - margin-bottom: 10px; - font-size: 18px; - ${sideBarDefaultTextColor} -` - -const StyledSideNavContainer = styled.nav` - display: flex; - flex-direction: column; - height: 100%; - ${sideBarBackgroundColor} - .topControl { - height: 50px; - display: flex; - -webkit-app-region: drag; - .spacer { - flex: 1; - } - .button { - width: 50px; - height: 50px; - background-color: transparent; - border: none; - cursor: pointer; - font-size: 24px; - ${iconColor} - } - } - - .storageList { - list-style: none; - padding: 0; - margin: 0; - flex: 1; - overflow: auto; - display: flex; - flex-direction: column; - } - - .empty { - padding: 4px; - padding-left: 26px; - margin-bottom: 4px; - ${sideBarTextColor} - user-select: none; - } - - .bottomControl { - height: 35px; - display: flex; - border-top: 1px solid ${({ theme }) => theme.colors.border}; - button { - height: 35px; - border: none; - background-color: transparent; - display: flex; - align-items: center; - } - .addFolderButton { - flex: 1; - border-right: 1px solid ${({ theme }) => theme.colors.border}; - } - .addFolderButtonIcon { - margin-right: 4px; - } - .moreButton { - width: 30px; - display: flex; - justify-content: center; - } - } -` - -const CreateStorageButton = styled.button` - position: absolute; - right: 8px; - border: none; - background-color: transparent; - cursor: pointer; - ${iconColor} -` - -const Spacer = styled.div` - flex: 1; -` - -export default () => { - const { - createStorage, - createFolder, - renameFolder, - renameStorage, - removeStorage, - storageMap, - syncStorage, - } = useDb() - const { popup } = useContextMenu() - const { prompt, messageBox } = useDialog() - const { push } = useRouter() - const [[user]] = useUsers() - const { pushMessage } = useToast() - - const storageEntries = useMemo(() => { - return entries(storageMap) - }, [storageMap]) - - const openSideNavContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - popup(event, [ - { - type: MenuTypes.Normal, - label: 'New Storage', - onClick: async () => { - prompt({ - title: 'Create a Storage', - message: 'Enter name of a storage to create', - iconType: DialogIconTypes.Question, - submitButtonLabel: 'Create Storage', - onClose: async (value: string | null) => { - if (value == null) return - const storage = await createStorage(value) - push(`/app/storages/${storage.id}/notes`) - }, - }) - }, - }, - ]) - }, - [popup, prompt, createStorage, push] - ) - - const { toggleClosed, preferences } = usePreferences() - const { - toggleSideNavOpenedItem, - sideNavOpenedItemSet, - openSideNavFolderItemRecursively, - } = useGeneralStatus() - - const currentPathname = usePathnameWithoutNoteId() - - const { t } = useTranslation() - - return ( - -
-
- -
- {/* - } - depth={0} - className='allnotes-sidenav' - label='All Notes' - active={currentPathname === `/app/notes`} - onClick={() => push(`/app/notes`)} - /> */} - - } - depth={0} - className='bookmark-sidenav' - label='Bookmarks' - active={currentPathname === `/app/bookmarks`} - onClick={() => push(`/app/bookmarks`)} - /> - - - Storages - push('/app/storages')}> - - - - -
- {storageEntries.map(([, storage]) => { - const itemId = getStorageItemId(storage.id) - const storageIsFolded = !sideNavOpenedItemSet.has(itemId) - const showPromptToCreateFolder = (folderPathname: string) => { - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue: folderPathname === '/' ? '/' : `${folderPathname}/`, - submitButtonLabel: 'Create Folder', - onClose: async (value: string | null) => { - if (value == null) { - return - } - if (value.endsWith('/')) { - value = value.slice(0, value.length - 1) - } - await createFolder(storage.id, value) - - push(`/app/storages/${storage.id}/notes${value}`) - - // Open folder item - openSideNavFolderItemRecursively(storage.id, value) - }, - }) - } - const showPromptToRenameFolder = (folderPathname: string) => { - prompt({ - title: t('folder.rename'), - message: t('folder.renameMessage'), - iconType: DialogIconTypes.Question, - defaultValue: folderPathname.split('/').pop(), - submitButtonLabel: t('folder.rename'), - onClose: async (value: string | null) => { - const folderPathSplit = folderPathname.split('/') - if ( - value == null || - value === '' || - value === folderPathSplit.pop() - ) { - return - } - const newPathname = folderPathSplit.join('/') + '/' + value - try { - await renameFolder(storage.id, folderPathname, newPathname) - push(`/app/storages/${storage.id}/notes${newPathname}`) - openSideNavFolderItemRecursively(storage.id, newPathname) - } catch (error) { - pushMessage({ - title: t('general.error'), - description: t('folder.renameErrorMessage'), - }) - } - }, - }) - } - - const allNotesPagePathname = `/app/storages/${storage.id}/notes` - const allNotesPageIsActive = currentPathname === allNotesPagePathname - - const trashcanPagePathname = `/app/storages/${storage.id}/trashcan` - const trashcanPageIsActive = currentPathname === trashcanPagePathname - - const attachmentsPagePathname = `/app/storages/${storage.id}/attachments` - const attachmentsPageIsActive = - currentPathname === attachmentsPagePathname - - const controlComponents = [ - showPromptToCreateFolder('/')} - icon={} - />, - ] - - if (storage.cloudStorage != null && user != null) { - const cloudSync = () => { - if (user == null) { - pushMessage({ - title: 'No User Error', - description: 'Please login first to sync the storage.', - }) - } - syncStorage(storage.id) - } - - controlComponents.unshift( - } - /> - ) - } - - controlComponents.unshift( - push(`/app/storages/${storage.id}`)} - icon={} - /> - ) - - return ( - - { - toggleSideNavOpenedItem(itemId) - }} - onClick={() => { - toggleSideNavOpenedItem(itemId) - }} - onContextMenu={(event) => { - event.preventDefault() - popup(event, [ - { - type: MenuTypes.Normal, - label: t('storage.rename'), - onClick: async () => { - prompt({ - title: `Rename "${storage.name}" storage`, - message: t('storage.renameMessage'), - iconType: DialogIconTypes.Question, - defaultValue: storage.name, - submitButtonLabel: t('storage.rename'), - onClose: async (value: string | null) => { - if (value == null) return - await renameStorage(storage.id, value) - }, - }) - }, - }, - { - type: MenuTypes.Normal, - label: t('storage.remove'), - onClick: async () => { - messageBox({ - title: `Remove "${storage.name}" storage`, - message: t('storage.removeMessage'), - iconType: DialogIconTypes.Warning, - buttons: [t('storage.remove'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(storage.id) - } - }, - }) - }, - }, - ]) - }} - controlComponents={controlComponents} - /> - {!storageIsFolded && ( - <> - } - active={allNotesPageIsActive} - onClick={() => push(allNotesPagePathname)} - /> - - - } - active={attachmentsPageIsActive} - onClick={() => push(attachmentsPagePathname)} - onContextMenu={(event) => { - event.preventDefault() - }} - /> - : } - active={trashcanPageIsActive} - onClick={() => push(trashcanPagePathname)} - onContextMenu={(event) => { - event.preventDefault() - // TODO: Implement context menu(restore all notes) - }} - /> - - )} - - ) - })} - {storageEntries.length === 0 && ( -
{t('storage.noStorage')}
- )} - {preferences['general.tutorials'] === 'display' && ( - - )} - -
- - ) -} diff --git a/src/components/SideNavigator/index.ts b/src/components/SideNavigator/index.ts deleted file mode 100644 index 8c087c9403..0000000000 --- a/src/components/SideNavigator/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import SideNavigator from './SideNavigator' - -export default SideNavigator diff --git a/src/components/Toast/styled.tsx b/src/components/Toast/styled.tsx index 6806b683dd..e8447a8b72 100644 --- a/src/components/Toast/styled.tsx +++ b/src/components/Toast/styled.tsx @@ -1,8 +1,5 @@ import styled from '../../lib/styled' -import { - secondaryBackgroundColor, - iconColor, -} from '../../lib/styled/styleFunctions' +import { secondaryBackgroundColor } from '../../lib/styled/styleFunctions' export const StyledToastContainer = styled.div` width: 350px; @@ -24,21 +21,31 @@ export const StyledToastRight = styled.div` ` export const StyledToastTitle = styled.p` font-size: 16px; - font-weight: 600: + font-weight: 600; ` + export const StyledToastTime = styled.p` font-size: 12px; margin-right: 10px; line-height: 25px; ` + export const StyledToastCloseButton = styled.button` background-color: transparent; font-size: 24px; order: none; cursor: pointer; border: none; - ${iconColor} + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } ` + export const StyledToastDescription = styled.p` font-size: 14px; ` diff --git a/src/components/Tutorials/TutorialsNavigator.tsx b/src/components/Tutorials/TutorialsNavigator.tsx deleted file mode 100644 index 1909caee6e..0000000000 --- a/src/components/Tutorials/TutorialsNavigator.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials' -import SideNavigatorItem from '../molecules/SideNavigatorItem' -import { useRouter, useCurrentTutorialPathname } from '../../lib/router' -import { useGeneralStatus } from '../../lib/generalStatus' -import { useContextMenu, MenuTypes } from '../../lib/contextMenu' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { usePreferences } from '../../lib/preferences' -import { IconFile, IconFileOpen, IconHelpOutline } from '../icons' - -interface NavigatorNode { - id: string - icon: React.ReactNode - depth: number - opened: boolean - name: string - href: string - children: NavigatorNode[] - active?: boolean -} - -const TutorialsNavigator = ({}) => { - const { push } = useRouter() - const currentHref = useCurrentTutorialPathname() - const { popup } = useContextMenu() - const { messageBox } = useDialog() - const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() - const { setPreferences } = usePreferences() - - const tutorials = tutorialsTree - - const getNavigatorNodesFromTreeItem = useCallback( - ( - tree: TutorialsNavigatorTreeItem, - currentDepth: number, - parentNode?: NavigatorNode, - parentComponentPathname?: string - ): NavigatorNode | undefined => { - if (tree.type === 'note') { - return - } - - const componentPathname = `${ - parentComponentPathname != null && parentComponentPathname - }/${tree.absolutePath}` - const nodeHref = `${parentNode != null ? parentNode.href : '/app'}/${ - tree.slug - }` - - const folderIsActive = currentHref.split('/notes/note:')[0] === nodeHref - - const notesUnderCurrentNode = tree.children.filter( - (child) => child.type === 'note' - ) - - const nodeId = `TF-${nodeHref.split('/app')[1]}` - const currentNode = { - id: nodeId, - name: `${tree.label} ${ - notesUnderCurrentNode.length > 0 - ? `(${notesUnderCurrentNode.length})` - : '' - }`, - icon: - tree.type === 'folder' ? ( - folderIsActive ? ( - - ) : ( - - ) - ) : ( - - ), - href: nodeHref, - active: folderIsActive, - depth: currentDepth, - opened: sideNavOpenedItemSet.has(nodeId), - children: [], - } - - const childrenNodes = - tree.children.length === 0 - ? [] - : (tree.children - .map((childrenTree) => - getNavigatorNodesFromTreeItem( - childrenTree, - currentDepth + 1, - currentNode, - componentPathname - ) - ) - .filter((node) => node != null) as NavigatorNode[]) - return { - ...currentNode, - children: childrenNodes, - } - }, - [currentHref, sideNavOpenedItemSet] - ) - - const createOnContextMenuHandler = (depth: number) => { - return (event: React.MouseEvent) => { - event.preventDefault() - popup(event, [ - { - type: MenuTypes.Normal, - label: 'Remove folder', - enabled: depth === 0, - onClick: () => { - messageBox({ - title: `Hide tutorials`, - message: - 'You can choose to display them again in your preferences.', - iconType: DialogIconTypes.Warning, - buttons: ['Hide', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: () => { - setPreferences({ 'general.tutorials': 'hide' }) - }, - }) - }, - }, - ]) - } - } - - const nodes = useMemo(() => { - return tutorials - .map((tutorial) => getNavigatorNodesFromTreeItem(tutorial, 0)) - .filter((node) => node != null) as NavigatorNode[] - }, [tutorials, getNavigatorNodesFromTreeItem]) - - const redirectToTutorialNode = (node: NavigatorNode) => { - push(node.href) - } - - const renderNode = ( - node: NavigatorNode, - parentNodeIsOpened: boolean - ): JSX.Element | null => { - if (node.depth > 0 && !parentNodeIsOpened) { - return null - } - return ( - - redirectToTutorialNode(node)} - onFoldButtonClick={() => toggleSideNavOpenedItem(node.id)} - folded={node.children.length === 0 ? undefined : !node.opened} - onContextMenu={createOnContextMenuHandler(node.depth)} - /> - {node.children.map((child) => renderNode(child, node.opened))} - - ) - } - - return <>{nodes.map((node) => renderNode(node, true))} -} - -export default TutorialsNavigator diff --git a/src/components/Tutorials/TutorialsNoteDetail.tsx b/src/components/Tutorials/TutorialsNoteDetail.tsx deleted file mode 100644 index e972582886..0000000000 --- a/src/components/Tutorials/TutorialsNoteDetail.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react' -import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' -import CustomizedMarkdownPreviewer from '../atoms/CustomizedMarkdownPreviewer' -import ToolbarIconButton from '../atoms/ToolbarIconButton' -import Toolbar from '../atoms/Toolbar' -import ToolbarSeparator from '../atoms/ToolbarSeparator' -import { TutorialsNavigatorTreeItem } from '../../lib/tutorials' -import { StyledNoteDetailContainer } from '../organisms/NoteDetail/NoteDetail' -import { ViewModeType } from '../../lib/generalStatus' -import { IconEye, IconSplit, IconEdit } from '../icons' - -type TutorialsNoteDetailProps = { - note: TutorialsNavigatorTreeItem - viewMode: ViewModeType - toggleViewMode: (mode: ViewModeType) => void -} - -type TutorialsNoteDetailState = { - noteComponent: string - noteContent: string -} - -export default class TutorialsNoteDetail extends React.Component< - TutorialsNoteDetailProps -> { - state: TutorialsNoteDetailState = { - noteComponent: this.props.note.slug, - noteContent: '', - } - - async componentDidMount() { - this.setState({ noteContent: await this.fetchNoteContentFromTreeItem() }) - } - - async fetchNoteContentFromTreeItem() { - try { - const doc = await import( - `../../lib/tutorials/files${this.props.note.absolutePath}` - ) - return doc.default - } catch (error) { - console.log(error) - return `Could not load the file` - } - } - - async componentDidUpdate( - _prevProps: TutorialsNoteDetailProps, - prevState: TutorialsNoteDetailState - ) { - const { note } = this.props - if (note.absolutePath !== prevState.noteComponent) { - this.setState({ - noteComponent: note.absolutePath, - noteContent: await this.fetchNoteContentFromTreeItem(), - }) - } - } - - codeMirror?: CodeMirror.EditorFromTextArea - codeMirrorRef = (codeMirror: CodeMirror.EditorFromTextArea) => { - this.codeMirror = codeMirror - } - - render() { - const { note, viewMode, toggleViewMode } = this.props - - const codeEditor = ( - - ) - const markdownPreviewer = ( - - ) - - return ( - -
- -
-
- {viewMode === 'preview' ? ( - markdownPreviewer - ) : viewMode === 'split' ? ( - <> -
{codeEditor}
-
{markdownPreviewer}
- - ) : ( - codeEditor - )} -
- - - toggleViewMode('edit')} - icon={} - /> - toggleViewMode('split')} - icon={} - /> - toggleViewMode('preview')} - icon={} - /> - -
- ) - } -} diff --git a/src/components/Tutorials/TutorialsNoteItem.tsx b/src/components/Tutorials/TutorialsNoteItem.tsx deleted file mode 100644 index f58e405f9e..0000000000 --- a/src/components/Tutorials/TutorialsNoteItem.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import Link from '../atoms/Link' -import cc from 'classcat' -import { TutorialsNavigatorTreeItem } from '../../lib/tutorials' -import { StyledNoteListItem } from '../organisms/NoteList/NoteItem' - -type TutorialsNoteItemProps = { - note: TutorialsNavigatorTreeItem - active: boolean - basePathname: string - focusList: () => void -} - -export default ({ - note, - active, - basePathname, - focusList, -}: TutorialsNoteItemProps) => { - const href = `${basePathname}/notes/note:${note.slug}` - - return ( - - -
-
{note.label}
-
- -
- ) -} diff --git a/src/components/Tutorials/TutorialsNoteList.tsx b/src/components/Tutorials/TutorialsNoteList.tsx deleted file mode 100644 index adcae3808f..0000000000 --- a/src/components/Tutorials/TutorialsNoteList.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useCallback, useRef } from 'react' -import { TutorialsNavigatorTreeItem } from '../../lib/tutorials' -import TutorialsNoteItem from './TutorialsNoteItem' -import { StyledNoteListContainer } from '../organisms/NoteList/NoteList' -import { useTranslation } from 'react-i18next' -import { - isWithGeneralCtrlKey, - useGlobalKeyDownHandler, -} from '../../lib/keyboard' - -type TutorialsNoteListProps = { - currentTree: TutorialsNavigatorTreeItem - basePathname: string - parentTree?: TutorialsNavigatorTreeItem - selectedNote?: TutorialsNavigatorTreeItem - navigateUp: () => void - navigateDown: () => void -} - -const TutorialsNoteList = ({ - currentTree, - parentTree, - navigateUp, - navigateDown, - basePathname, - selectedNote, -}: TutorialsNoteListProps) => { - useGlobalKeyDownHandler((e) => { - switch (e.key) { - case 'j': - if (isWithGeneralCtrlKey(e)) { - e.preventDefault() - e.stopPropagation() - navigateDown() - } - break - case 'k': - if (isWithGeneralCtrlKey(e)) { - e.preventDefault() - e.stopPropagation() - navigateUp() - } - break - default: - break - } - }) - - const listRef = useRef(null) - - const focusList = useCallback(() => { - listRef.current!.focus() - }, []) - - const notes = - currentTree.type !== 'note' - ? currentTree.children.filter((child) => child.type === 'note') - : parentTree == null - ? [] - : parentTree.children.filter((child) => child.type === 'note') - - const { t } = useTranslation() - - return ( - -
    - {notes.map((note) => { - const noteIsCurrentNote = - selectedNote != null && - note.absolutePath === selectedNote.absolutePath - return ( -
  • - -
  • - ) - })} - {notes.length === 0 &&
  • {t('note.nothing')}
  • } -
-
- ) -} - -export default TutorialsNoteList diff --git a/src/components/Tutorials/TutorialsPage.tsx b/src/components/Tutorials/TutorialsPage.tsx deleted file mode 100644 index bb7bc2a97a..0000000000 --- a/src/components/Tutorials/TutorialsPage.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import React, { useCallback, useMemo } from 'react' -import { tutorialsTree, TutorialsNavigatorTreeItem } from '../../lib/tutorials' -import TwoPaneLayout from '../atoms/TwoPaneLayout' -import { useGeneralStatus, ViewModeType } from '../../lib/generalStatus' -import { useRouter } from '../../lib/router' -import TutorialsNoteList from './TutorialsNoteList' -import TutorialsNoteDetail from './TutorialsNoteDetail' -import { useTranslation } from 'react-i18next' - -interface TutorialsPageProps { - pathname: string -} - -type TutoriasPagePicker = { - parentTree?: TutorialsNavigatorTreeItem - currentTree: TutorialsNavigatorTreeItem -} - -export default ({ pathname }: TutorialsPageProps) => { - const { generalStatus, setGeneralStatus } = useGeneralStatus() - const router = useRouter() - const { t } = useTranslation() - const searchThroughTreeForIdenticalNode = useCallback( - ( - pathToSearch: string, - parentDepthPath: string, - parentAbsolutePath: string, - tree: TutorialsNavigatorTreeItem, - parentTree?: TutorialsNavigatorTreeItem - ): TutoriasPagePicker | null => { - let match = null - const currentDepthPath = `${parentDepthPath}/${ - tree.type === 'note' ? 'notes/note:' : '' - }${tree.slug}` - - const currentAbsolutePath = parentAbsolutePath + '/' + tree.absolutePath - if (currentDepthPath === pathToSearch) { - const currentTreeWithDepthAbsolutePath = { - ...tree, - absolutePath: currentAbsolutePath, - children: Object.entries(tree.children).map((obj) => { - return { - ...obj[1], - absolutePath: currentAbsolutePath + '/' + obj[1].absolutePath, - } - }) as TutorialsNavigatorTreeItem[], - } - - const parentTreeWithDepthAbsolutePath = - parentTree != null - ? { - ...parentTree, - absolutePath: parentAbsolutePath, - children: Object.entries(parentTree.children).map((obj) => { - return { - ...obj[1], - absolutePath: - parentAbsolutePath + '/' + obj[1].absolutePath, - } - }), - } - : undefined - - return { - currentTree: currentTreeWithDepthAbsolutePath, - parentTree: parentTreeWithDepthAbsolutePath, - } - } - - for (let i = 0; i < tree.children.length; i++) { - match = searchThroughTreeForIdenticalNode( - pathToSearch, - currentDepthPath, - currentAbsolutePath, - tree.children[i], - tree - ) - if (match != null) { - break - } - } - - return match - }, - [] - ) - - const currentTutorialBranch = useMemo(() => { - let match = null - for (let i = 0; i < tutorialsTree.length; i++) { - match = searchThroughTreeForIdenticalNode( - pathname, - '/app', - '', - tutorialsTree[i] - ) - if (match != null) { - break - } - } - return match - }, [pathname, searchThroughTreeForIdenticalNode]) - - const updateNoteListWidth = useCallback( - (leftWidth: number) => { - setGeneralStatus({ - noteListWidth: leftWidth, - }) - }, - [setGeneralStatus] - ) - - const toggleViewMode = useCallback( - (newMode: ViewModeType) => { - setGeneralStatus({ - noteViewMode: newMode, - }) - }, - [setGeneralStatus] - ) - - const selectedNote = useMemo((): TutorialsNavigatorTreeItem | undefined => { - if (currentTutorialBranch == null) { - return undefined - } - - if (currentTutorialBranch.currentTree.type === 'note') { - return currentTutorialBranch.currentTree - } - - const notesChildren = currentTutorialBranch.currentTree.children.filter( - (node) => node.type === 'note' - ) - if (notesChildren.length > 0) { - return notesChildren[0] - } - return undefined - }, [currentTutorialBranch]) - - const currentFolderPathname = useMemo(() => { - return pathname.split('/notes')[0] - }, [pathname]) - - const navigateUp = useCallback(() => { - if (currentTutorialBranch == null) { - return - } - - if (selectedNote == null) { - return - } - - const notes = - currentTutorialBranch.currentTree.type !== 'note' - ? currentTutorialBranch.currentTree.children.filter( - (node) => node.type === 'note' - ) - : currentTutorialBranch.parentTree != null - ? currentTutorialBranch.parentTree.children.filter( - (node) => node.type === 'note' - ) - : [] - - if (notes.length < 1) { - return - } - - let currentNoteIndex = 0 - for (let i = 0; i < notes.length; i++) { - if (selectedNote.absolutePath === notes[i].absolutePath) { - currentNoteIndex = i - break - } - } - - if (currentNoteIndex - 1 >= 0) { - router.push( - currentFolderPathname + - '/notes/note:' + - notes[currentNoteIndex - 1].slug - ) - } - return - }, [selectedNote, currentTutorialBranch, router, currentFolderPathname]) - - const navigateDown = useCallback(() => { - if (currentTutorialBranch == null) { - return - } - - if (selectedNote == null) { - return - } - - const notes = - currentTutorialBranch.currentTree.type !== 'note' - ? currentTutorialBranch.currentTree.children.filter( - (node) => node.type === 'note' - ) - : currentTutorialBranch.parentTree != null - ? currentTutorialBranch.parentTree.children.filter( - (node) => node.type === 'note' - ) - : [] - - if (notes.length < 1) { - return - } - - let currentNoteIndex = 0 - for (let i = 0; i < notes.length; i++) { - if (selectedNote.absolutePath === notes[i].absolutePath) { - currentNoteIndex = i - break - } - } - - if (currentNoteIndex + 1 >= 0 && currentNoteIndex + 1 < notes.length) { - router.push( - currentFolderPathname + - '/notes/note:' + - notes[currentNoteIndex + 1].slug - ) - } - return - }, [selectedNote, currentFolderPathname, currentTutorialBranch, router]) - - return ( - - ) - } - right={ - selectedNote == null ? ( -
{t('note.unselect')}
- ) : ( - - ) - } - onResizeEnd={updateNoteListWidth} - /> - ) -} diff --git a/src/components/atoms/Icon.tsx b/src/components/atoms/Icon.tsx new file mode 100644 index 0000000000..9b5d94b4ea --- /dev/null +++ b/src/components/atoms/Icon.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { Icon as MdiIcon } from '@mdi/react' + +interface IconProps { + path: string + color?: string + size?: number + style?: React.CSSProperties + className?: string + spin?: boolean +} + +const Icon = ({ + path, + color = 'currentColor', + size, + style, + className, + spin, +}: IconProps) => ( + +) + +export default Icon diff --git a/src/components/atoms/NavigatorButton.tsx b/src/components/atoms/NavigatorButton.tsx new file mode 100644 index 0000000000..078e845dcb --- /dev/null +++ b/src/components/atoms/NavigatorButton.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import styled from '../../lib/styled' +import Icon from './Icon' + +const ButtonContainer = styled.button` + width: 24px; + height: 24px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + + background-color: transparent; + border-radius: 50%; + border: none; + cursor: pointer; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + &.active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } +` + +interface NavigatorButtonProps { + active?: boolean + onClick?: React.MouseEventHandler + onContextMenu?: React.MouseEventHandler + iconPath: string + spin?: boolean + title?: string +} + +const NavigatorButton = ({ + active, + onClick, + onContextMenu, + iconPath, + title, + spin, +}: NavigatorButtonProps) => { + return ( + + + + ) +} + +export default NavigatorButton diff --git a/src/components/atoms/NavigatorHeader.tsx b/src/components/atoms/NavigatorHeader.tsx new file mode 100644 index 0000000000..a5f3daca8a --- /dev/null +++ b/src/components/atoms/NavigatorHeader.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import styled from '../../lib/styled' + +const HeaderContainer = styled.header` + position: relative; + user-select: none; + height: 24px; + display: flex; + justify-content: space-between; + font-weight: bold; + align-items: center; +` + +const Label = styled.div` + padding: 0 0 0 0.5em; + color: ${({ theme }) => theme.sideNavLabelColor}; +` + +const Control = styled.div` + display: flex; +` + +interface NavigatorHeaderProps { + label: string + control?: React.ReactNode + onContextMenu: React.MouseEventHandler +} + +const NavigatorHeader = ({ + label, + onContextMenu, + control, +}: NavigatorHeaderProps) => { + return ( + + + {control && {control}} + + ) +} + +export default NavigatorHeader diff --git a/src/components/atoms/NavigatorItem.tsx b/src/components/atoms/NavigatorItem.tsx new file mode 100644 index 0000000000..3c9552af5a --- /dev/null +++ b/src/components/atoms/NavigatorItem.tsx @@ -0,0 +1,164 @@ +import React from 'react' +import cc from 'classcat' +import styled from '../../lib/styled' +import Icon from './Icon' +import { mdiChevronDown, mdiChevronRight } from '@mdi/js' + +const Container = styled.div` + position: relative; + user-select: none; + height: 34px; + display: flex; + justify-content: space-between; + width: 100%; + + font-size: 1em; + transition: 200ms background-color; + &:hover { + .control { + opacity: 1; + } + } +` + +const FoldButton = styled.button` + position: absolute; + width: 24px; + height: 24px; + border: none; + background-color: transparent; + border-radius: 50%; + top: 5px; + display: flex; + align-items: center; + justify-content: center; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } +` + +const ClickableContainer = styled.button` + background-color: transparent; + border: none; + display: flex; + align-items: center; + flex: 1 1 auto; + cursor: pointer; + + color: ${({ theme }) => theme.sideNavItemColor}; + background-color: ${({ theme }) => theme.sideNavItemBackgroundColor}; + &:hover { + background-color: ${({ theme }) => theme.sideNavItemHoverBackgroundColor}; + } + &:active, + &.active { + color: ${({ theme }) => theme.sideNavItemActiveColor}; + background-color: ${({ theme }) => theme.sideNavItemActiveBackgroundColor}; + } + &:hover:active, + &:hover.active { + background-color: ${({ theme }) => + theme.sideNavItemHoverActiveBackgroundColor}; + } +` + +const Label = styled.div` + overflow: ellipsis; + white-space: nowrap; +` + +const Control = styled.div` + position: absolute; + right: 0; + top: 5px; + opacity: 0; + transition: opacity 200ms ease-in-out; +` + +const IconContainer = styled.div` + width: 22px; + height: 24px; + display: flex; + align-items: center; + font-size: 18px; +` + +interface NavigatorItemProps { + label: string + iconPath?: string + depth: number + control?: React.ReactNode + className?: string + folded?: boolean + active?: boolean + onFoldButtonClick?: (event: React.MouseEvent) => void + onClick?: (event: React.MouseEvent) => void + onContextMenu?: (event: React.MouseEvent) => void + onDrop?: (event: React.DragEvent) => void + onDragOver?: (event: React.DragEvent) => void + onDragEnd?: (event: React.DragEvent) => void + onDoubleClick?: (event: React.MouseEvent) => void +} + +const NavigatorItem = ({ + label, + iconPath, + depth, + control, + className, + folded, + active, + onFoldButtonClick, + onClick, + onDoubleClick, + onContextMenu, + onDrop, + onDragOver, + onDragEnd, +}: NavigatorItemProps) => { + return ( + + {folded != null && ( + + + + )} + + {iconPath != null && ( + + + + )} + + + {control && {control}} + + ) +} + +export default NavigatorItem diff --git a/src/components/atoms/Spacer.tsx b/src/components/atoms/Spacer.tsx new file mode 100644 index 0000000000..b0a0ba8f9e --- /dev/null +++ b/src/components/atoms/Spacer.tsx @@ -0,0 +1,7 @@ +import styled from '../../lib/styled' + +const Spacer = styled.div` + flex: 1; +` + +export default Spacer diff --git a/src/components/atoms/ToolbarExportButton.tsx b/src/components/atoms/ToolbarExportButton.tsx index 6d7295c817..2542684fbc 100644 --- a/src/components/atoms/ToolbarExportButton.tsx +++ b/src/components/atoms/ToolbarExportButton.tsx @@ -1,6 +1,4 @@ import React, { useCallback } from 'react' -import styled from '../../lib/styled' -import { noteListIconColor } from '../../lib/styled/styleFunctions' import { useContextMenu, MenuTypes } from '../../lib/contextMenu' import { NoteDoc } from '../../lib/db/types' import { @@ -9,30 +7,15 @@ import { } from '../../lib/exports' import { usePreferences } from '../../lib/preferences' import { usePreviewStyle } from '../../lib/preview' -import { IconInfo } from '../icons' - -const StyledButton = styled.button<{ active: boolean }>` - background: transparent; - height: 32px; - box-sizing: border-box; - outline: none; - font-size: 16px; - border: none; - ${noteListIconColor} - &:first-child { - margin-left: 0; - } - &:last-child { - margin-right: 0; - } -` +import ToolbarButton from './ToolbarIconButton' +import { mdiExportVariant } from '@mdi/js' interface ToolbarExportButtonProps { note: NoteDoc className?: string } -const ToolbarExportButton = ({ className, note }: ToolbarExportButtonProps) => { +const ToolbarExportButton = ({ note }: ToolbarExportButtonProps) => { const { popup } = useContextMenu() const { preferences } = usePreferences() const { previewStyle } = usePreviewStyle() @@ -58,13 +41,11 @@ const ToolbarExportButton = ({ className, note }: ToolbarExportButtonProps) => { ) return ( - - - + iconPath={mdiExportVariant} + /> ) } diff --git a/src/components/atoms/ToolbarIconButton.tsx b/src/components/atoms/ToolbarIconButton.tsx index c1c600d666..c3e0f885bb 100644 --- a/src/components/atoms/ToolbarIconButton.tsx +++ b/src/components/atoms/ToolbarIconButton.tsx @@ -1,39 +1,43 @@ import React from 'react' import styled from '../../lib/styled' -import { noteListIconColor } from '../../lib/styled/styleFunctions' +import Icon from './Icon' -const StyledButton = styled.button<{ active: boolean }>` - background: transparent; +const ToolbarButtonContainer = styled.button` height: 32px; box-sizing: border-box; - font-size: 16px; + font-size: 18px; outline: none; + + background-color: transparent; border: none; - ${noteListIconColor} - &:first-child { - margin-left: 0; + cursor: pointer; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; } - &:last-child { - margin-right: 0; + + &:active, + &.active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; } ` interface ToolbarButtonProps { - icon: React.ReactNode + iconPath: string active?: boolean - className?: string onClick: React.MouseEventHandler } const ToolbarButton = ({ - icon, + iconPath, onClick, active = false, - className, }: ToolbarButtonProps) => ( - - {icon} - + + + ) export default ToolbarButton diff --git a/src/components/icons/ArrowSingleDown.tsx b/src/components/icons/ArrowSingleDown.tsx deleted file mode 100644 index 98af130a5c..0000000000 --- a/src/components/icons/ArrowSingleDown.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - BoostnoteIconProps, - BoostnoteIconStyledContainer, -} from '../../lib/icons' -import React from 'react' - -export const IconArrowSingleDown = (props: BoostnoteIconProps) => ( - - - - - -) diff --git a/src/components/icons/ArrowSingleRight.tsx b/src/components/icons/ArrowSingleRight.tsx deleted file mode 100644 index e3651de4c3..0000000000 --- a/src/components/icons/ArrowSingleRight.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - BoostnoteIconProps, - BoostnoteIconStyledContainer, -} from '../../lib/icons' -import React from 'react' - -export const IconArrowSingleRight = (props: BoostnoteIconProps) => ( - - - - - -) diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 20bc399f7e..6c65278f4f 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -5,8 +5,6 @@ export * from './Alphabet' export * from './ArrowAgain' export * from './ArrowOutput' export * from './ArrowRotate' -export * from './ArrowSingleDown' -export * from './ArrowSingleRight' export * from './Bold' export * from './Book' export * from './Check' diff --git a/src/components/SideNavigator/FolderListFragment.tsx b/src/components/molecules/FolderListFragment.tsx similarity index 87% rename from src/components/SideNavigator/FolderListFragment.tsx rename to src/components/molecules/FolderListFragment.tsx index bfe897e5d0..317a2f84a4 100644 --- a/src/components/SideNavigator/FolderListFragment.tsx +++ b/src/components/molecules/FolderListFragment.tsx @@ -2,15 +2,16 @@ import React, { useMemo } from 'react' import { useDb } from '../../lib/db' import { useDialog, DialogIconTypes } from '../../lib/dialog' import { useContextMenu, MenuTypes } from '../../lib/contextMenu' -import SideNavigatorItem from '../molecules/SideNavigatorItem' +import NavigatorItem from '../atoms/NavigatorItem' import { NoteStorage } from '../../lib/db/types' import { usePathnameWithoutNoteId, useRouter } from '../../lib/router' import { useGeneralStatus } from '../../lib/generalStatus' -import ControlButton from './ControlButton' import { getFolderItemId } from '../../lib/nav' import { getTransferrableNoteData } from '../../lib/dnd' -import { IconAddRound, IconFile, IconFileOpen } from '../icons' import { useTranslation } from 'react-i18next' +import { mdiFolderOpen, mdiFolder, mdiDotsVertical } from '@mdi/js' +import NavigatorButton from '../atoms/NavigatorButton' +import { dispatchNoteDetailFocusTitleInputEvent } from '../../lib/events' interface FolderListFragmentProps { storage: NoteStorage @@ -61,6 +62,22 @@ const FolderListFragment = ({ return (event: React.MouseEvent) => { event.preventDefault() popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Note', + onClick: async () => { + const note = await createNote(storage.id, { + folderPathname, + }) + push( + `/app/storages/${storage.id}/notes${folderPathname}/${note!._id}` + ) + dispatchNoteDetailFocusTitleInputEvent() + }, + }, + { + type: MenuTypes.Separator, + }, { type: MenuTypes.Normal, label: t('folder.create'), @@ -178,7 +195,7 @@ const FolderListFragment = ({ const nameElements = folderPathname.split('/').slice(1) const folderName = nameElements[nameElements.length - 1] const itemId = getFolderItemId(storageId, folderPathname) - const depth = nameElements.length + const depth = nameElements.length - 1 const folded = folderSetWithSubFolders.has(folderPathname) ? !sideNavOpenedItemSet.has(itemId) : undefined @@ -187,12 +204,12 @@ const FolderListFragment = ({ currentPathnameWithoutNoteId === `/app/storages/${storageId}/notes${folderPathname}` return ( - : } + iconPath={folderIsActive ? mdiFolderOpen : mdiFolder} label={folderName} onClick={createOnFolderItemClickHandler(folderPathname)} onDoubleClick={() => showPromptToRenameFolder(folderPathname)} @@ -201,13 +218,12 @@ const FolderListFragment = ({ folderPathname )} onFoldButtonClick={() => toggleSideNavOpenedItem(itemId)} - controlComponents={[ - showPromptToCreateFolder(folderPathname)} - icon={} - />, - ]} + control={ + + } onDragOver={(event) => { event.preventDefault() }} diff --git a/src/components/organisms/NoteList/NoteItem.tsx b/src/components/molecules/NoteItem.tsx similarity index 92% rename from src/components/organisms/NoteList/NoteItem.tsx rename to src/components/molecules/NoteItem.tsx index d6c5f87b7f..7af7d01717 100644 --- a/src/components/organisms/NoteList/NoteItem.tsx +++ b/src/components/molecules/NoteItem.tsx @@ -1,21 +1,21 @@ import React, { useMemo, useCallback } from 'react' -import Link from '../../atoms/Link' -import styled from '../../../lib/styled/styled' +import Link from '../atoms/Link' +import styled from '../../lib/styled/styled' import { borderBottom, uiTextColor, secondaryBackgroundColor, inputStyle, -} from '../../../lib/styled/styleFunctions' +} from '../../lib/styled/styleFunctions' import cc from 'classcat' -import { setTransferrableNoteData } from '../../../lib/dnd' -import HighlightText from '../../atoms/HighlightText' +import { setTransferrableNoteData } from '../../lib/dnd' +import HighlightText from '../atoms/HighlightText' import { formatDistanceToNow } from 'date-fns' -import { scaleAndTransformFromLeft } from '../../../lib/styled' -import { PopulatedNoteDoc } from '../../../lib/db/types' -import { useContextMenu, MenuTypes, MenuItem } from '../../../lib/contextMenu' -import { useDb } from '../../../lib/db' -import { useDialog, DialogIconTypes } from '../../../lib/dialog' +import { scaleAndTransformFromLeft } from '../../lib/styled' +import { PopulatedNoteDoc } from '../../lib/db/types' +import { useContextMenu, MenuTypes, MenuItem } from '../../lib/contextMenu' +import { useDb } from '../../lib/db' +import { useDialog, DialogIconTypes } from '../../lib/dialog' import { useTranslation } from 'react-i18next' export const StyledNoteListItem = styled.div` diff --git a/src/components/molecules/SideNavigatorItem.tsx b/src/components/molecules/SideNavigatorItem.tsx deleted file mode 100644 index 5043bf21a3..0000000000 --- a/src/components/molecules/SideNavigatorItem.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React from 'react' -import cc from 'classcat' -import styled from '../../lib/styled' -import { - sideBarTextColor, - sideBarSecondaryTextColor, - activeBackgroundColor, - iconColor, -} from '../../lib/styled/styleFunctions' -import { IconArrowSingleRight, IconArrowSingleDown } from '../icons' - -const Container = styled.div` - position: relative; - user-select: none; - height: 34px; - display: flex; - justify-content: space-between; - - .sideNavWrapper { - min-width: 0; - flex: 1 1 auto; - - button > span { - width: 30px; - ${iconColor} - } - } - - transition: 200ms background-color; - &:hover, - &:focus, - &:active, - &.active { - ${activeBackgroundColor} - - button > span { - ${sideBarTextColor} - } - } - &:hover { - cursor: pointer; - } - .control { - opacity: 0; - } - &:hover .control { - opacity: 1; - } - - &.allnotes-sidenav { - padding-left: 4px !important; - button { - padding-left: 6px !important; - } - } - &.bookmark-sidenav { - padding-left: 4px !important; - margin-bottom: 25px; - button { - padding-left: 6px !important; - } - } -` - -const FoldButton = styled.button` - position: absolute; - width: 26px; - height: 26px; - padding-left: 10px; - border: none; - background-color: transparent; - margin-right: 3px; - border-radius: 2px; - flex: 0 0 26px; - top: 5px; - ${sideBarSecondaryTextColor} - &:focus { - box-shadow: none; - } -` - -const ClickableContainer = styled.button` - background-color: transparent; - border: none; - height: 35px; - display: flex; - align-items: center; - min-width: 0; - width: 100%; - flex: 1 1 auto; - - ${sideBarTextColor} - - .icon { - flex: 0 0 auto; - margin-right: 4px; - ${sideBarSecondaryTextColor} - } -` - -const Label = styled.div` - min-width: 0; - overflow: hidden; - white-space: nowrap; - flex: 1 1 auto; - text-align: left; -` - -const ControlContainer = styled.div` - position: absolute; - right: 0; - top: 4px; - padding-left: 10px; - display: flex; - flex: 2 0 auto; - justify-content: flex-end; - button { - ${iconColor} - } -` - -const SideNavigatorItemIconContainer = styled.span` - padding-right: 6px; -` - -interface SideNaviagtorItemProps { - label: string - icon?: React.ReactNode - depth: number - controlComponents?: any[] - className?: string - folded?: boolean - active?: boolean - onFoldButtonClick?: (event: React.MouseEvent) => void - onClick?: (event: React.MouseEvent) => void - onContextMenu?: (event: React.MouseEvent) => void - onDrop?: (event: React.DragEvent) => void - onDragOver?: (event: React.DragEvent) => void - onDragEnd?: (event: React.DragEvent) => void - onDoubleClick?: (event: React.MouseEvent) => void -} - -const SideNaviagtorItem = ({ - label, - icon, - depth, - controlComponents, - className, - folded, - active, - onFoldButtonClick, - onClick, - onDoubleClick, - onContextMenu, - onDrop, - onDragOver, - onDragEnd, -}: SideNaviagtorItemProps) => { - return ( - -
- {folded != null && ( - - {folded ? ( - - ) : ( - - )} - - )} - - {icon != null && ( - - {icon} - - )} - - -
- {controlComponents && ( - - {controlComponents} - - )} -
- ) -} - -export default SideNaviagtorItem diff --git a/src/components/molecules/StorageNavigatorFragment.tsx b/src/components/molecules/StorageNavigatorFragment.tsx new file mode 100644 index 0000000000..72096f51d5 --- /dev/null +++ b/src/components/molecules/StorageNavigatorFragment.tsx @@ -0,0 +1,298 @@ +import React, { useMemo } from 'react' +import { useGeneralStatus } from '../../lib/generalStatus' +import { useDialog, DialogIconTypes } from '../../lib/dialog' +import { useDb } from '../../lib/db' +import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' +import { useTranslation } from 'react-i18next' +import { useToast } from '../../lib/toast' +import { useFirstUser } from '../../lib/preferences' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import NavigatorItem from '../atoms/NavigatorItem' +import { NoteStorage } from '../../lib/db/types' +import { + mdiTrashCanOutline, + mdiPaperclip, + mdiBookOpen, + mdiTuneVertical, + mdiSync, +} from '@mdi/js' +import FolderListFragment from './FolderListFragment' +import TagListFragment from './TagListFragment' +import NavigatorHeader from '../atoms/NavigatorHeader' +import NavigatorButton from '../atoms/NavigatorButton' +import styled from '../../lib/styled' +import { dispatchNoteDetailFocusTitleInputEvent } from '../../lib/events' + +const Spacer = styled.div` + height: 1em; +` + +interface StorageNavigatorFragmentProps { + storage: NoteStorage +} + +const StorageNavigatorFragment = ({ + storage, +}: StorageNavigatorFragmentProps) => { + const { openSideNavFolderItemRecursively } = useGeneralStatus() + const { prompt, messageBox } = useDialog() + const { + createNote, + createFolder, + renameFolder, + renameStorage, + removeStorage, + syncStorage, + } = useDb() + const { push } = useRouter() + const { t } = useTranslation() + const { pushMessage } = useToast() + const currentPathname = usePathnameWithoutNoteId() + const user = useFirstUser() + const { popup } = useContextMenu() + + const showPromptToCreateFolder = (folderPathname: string) => { + prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue: folderPathname === '/' ? '/' : `${folderPathname}/`, + submitButtonLabel: 'Create Folder', + onClose: async (value: string | null) => { + if (value == null) { + return + } + if (value.endsWith('/')) { + value = value.slice(0, value.length - 1) + } + await createFolder(storage.id, value) + + push(`/app/storages/${storage.id}/notes${value}`) + + // Open folder item + openSideNavFolderItemRecursively(storage.id, value) + }, + }) + } + const showPromptToRenameFolder = (folderPathname: string) => { + prompt({ + title: t('folder.rename'), + message: t('folder.renameMessage'), + iconType: DialogIconTypes.Question, + defaultValue: folderPathname.split('/').pop(), + submitButtonLabel: t('folder.rename'), + onClose: async (value: string | null) => { + const folderPathSplit = folderPathname.split('/') + if (value == null || value === '' || value === folderPathSplit.pop()) { + return + } + const newPathname = folderPathSplit.join('/') + '/' + value + try { + await renameFolder(storage.id, folderPathname, newPathname) + push(`/app/storages/${storage.id}/notes${newPathname}`) + openSideNavFolderItemRecursively(storage.id, newPathname) + } catch (error) { + pushMessage({ + title: t('general.error'), + description: t('folder.renameErrorMessage'), + }) + } + }, + }) + } + + const syncThisStorage = () => { + if (user == null) { + pushMessage({ + title: 'No User Error', + description: 'Please login first to sync the storage.', + }) + return + } + syncStorage(storage.id) + } + + const allNotesPagePathname = `/app/storages/${storage.id}/notes` + const allNotesPageIsActive = currentPathname === allNotesPagePathname + + const trashcanPagePathname = `/app/storages/${storage.id}/trashcan` + const trashcanPageIsActive = currentPathname === trashcanPagePathname + + const attachmentsPagePathname = `/app/storages/${storage.id}/attachments` + const attachmentsPageIsActive = currentPathname === attachmentsPagePathname + + const openContextMenu: React.MouseEventHandler = (event) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Note', + onClick: async () => { + const note = await createNote(storage.id, { + folderPathname: '/', + }) + push(`/app/storages/${storage.id}/notes/${note!._id}`) + dispatchNoteDetailFocusTitleInputEvent() + }, + }, + { + type: MenuTypes.Normal, + label: t('folder.create'), + onClick: async () => { + showPromptToCreateFolder('/') + }, + }, + { + type: MenuTypes.Separator, + }, + { + type: MenuTypes.Normal, + label: t('storage.rename'), + onClick: syncThisStorage, + }, + { + type: MenuTypes.Normal, + label: t('storage.rename'), + onClick: async () => { + prompt({ + title: `Rename "${storage.name}" storage`, + message: t('storage.renameMessage'), + iconType: DialogIconTypes.Question, + defaultValue: storage.name, + submitButtonLabel: t('storage.rename'), + onClose: async (value: string | null) => { + if (value == null) return + await renameStorage(storage.id, value) + }, + }) + }, + }, + { + type: MenuTypes.Normal, + label: t('storage.remove'), + onClick: async () => { + messageBox({ + title: `Remove "${storage.name}" storage`, + message: t('storage.removeMessage'), + iconType: DialogIconTypes.Warning, + buttons: [t('storage.remove'), t('general.cancel')], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeStorage(storage.id) + } + }, + }) + }, + }, + { + type: MenuTypes.Normal, + label: 'Configure Storage', + onClick: () => push(`/app/storages/${storage.id}/settings`), + }, + ]) + } + + const openAllNotesContextMenu: React.MouseEventHandler = (event) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Note', + onClick: async () => { + const note = await createNote(storage.id, { + folderPathname: '/', + }) + push(`/app/storages/${storage.id}/notes/${note!._id}`) + dispatchNoteDetailFocusTitleInputEvent() + }, + }, + { + type: MenuTypes.Separator, + }, + { + type: MenuTypes.Normal, + label: t('folder.create'), + onClick: async () => { + showPromptToCreateFolder('/') + }, + }, + ]) + } + + const attachments = useMemo(() => Object.values(storage.attachmentMap), [ + storage.attachmentMap, + ]) + const trashed = useMemo( + () => Object.values(storage.noteMap).filter((note) => note!.trashed), + [storage.noteMap] + ) + + const syncing = storage.sync != null + + return ( + <> + + + push(`/app/storages/${storage.id}/settings`)} + iconPath={mdiTuneVertical} + /> + + } + /> + push(allNotesPagePathname)} + onContextMenu={openAllNotesContextMenu} + /> + + + {attachments.length > 0 && ( + push(attachmentsPagePathname)} + onContextMenu={(event) => { + event.preventDefault() + }} + /> + )} + {trashed.length > 0 && ( + push(trashcanPagePathname)} + onContextMenu={(event) => { + event.preventDefault() + // TODO: Implement context menu(restore all notes) + }} + /> + )} + + + ) +} + +export default StorageNavigatorFragment diff --git a/src/components/SideNavigator/TagListFragment.tsx b/src/components/molecules/TagListFragment.tsx similarity index 91% rename from src/components/SideNavigator/TagListFragment.tsx rename to src/components/molecules/TagListFragment.tsx index 2a121c371c..a0aef4698a 100644 --- a/src/components/SideNavigator/TagListFragment.tsx +++ b/src/components/molecules/TagListFragment.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import SideNavigatorItem from '../molecules/SideNavigatorItem' +import SideNavigatorItem from '../atoms/NavigatorItem' import { NoteStorage } from '../../lib/db/types' import { useGeneralStatus } from '../../lib/generalStatus' import { getTagListItemId } from '../../lib/nav' @@ -7,8 +7,8 @@ import { useRouter, usePathnameWithoutNoteId } from '../../lib/router' import { useContextMenu, MenuTypes } from '../../lib/contextMenu' import { useDialog, DialogIconTypes } from '../../lib/dialog' import { useDb } from '../../lib/db' -import { IconTag, IconTags, IconTagFill } from '../icons' import { useTranslation } from 'react-i18next' +import { mdiPound, mdiTagMultiple } from '@mdi/js' interface TagListFragmentProps { storage: NoteStorage @@ -34,8 +34,8 @@ const TagListFragment = ({ storage }: TagListFragmentProps) => { return ( : } + depth={1} + iconPath={mdiPound} label={tagName} onClick={() => { push(tagPathname) @@ -79,11 +79,15 @@ const TagListFragment = ({ storage }: TagListFragmentProps) => { t, ]) + if (tagList.length === 0) { + return null + } + return ( <> } + depth={0} + iconPath={mdiTagMultiple} label={t('tag.tag')} folded={tagList.length > 0 ? tagListIsFolded : undefined} onFoldButtonClick={() => { diff --git a/src/components/organisms/ContextMenu.tsx b/src/components/organisms/ContextMenu.tsx index 9a1ee98123..311574bc6e 100644 --- a/src/components/organisms/ContextMenu.tsx +++ b/src/components/organisms/ContextMenu.tsx @@ -3,6 +3,7 @@ import { useContextMenu, MenuTypes, ContextMenuContext, + menuSeparatorHeight, } from '../../lib/contextMenu' import styled from '../../lib/styled' import { @@ -27,15 +28,15 @@ const StyledContextMenu = styled.div` border-style: solid; border-width: 1px; padding: ${menuVerticalPadding}px 0; - font-size: 14px; box-sizing: border-box; border-radius: 2px; ${contextMenuShadow} outline: none; ` -const StyledContextMenuItem = styled.button` +const NormalContextMenuItem = styled.button` height: ${menuHeight}px; + font-size: 13px; padding: 0 20px; box-sizing: border-box; background-color: transparent; @@ -55,6 +56,30 @@ const StyledContextMenuItem = styled.button` } ` +const SeparatorContextMenuItemContainer = styled.div` + height: ${menuSeparatorHeight}px; + box-sizing: border-box; + background-color: transparent; + border: none; + display: flex; + align-items: center; + width: 100%; +` + +const SeparatorContextMenuItemBorder = styled.div` + height: 1px; + width: 100%; + background-color: ${({ theme }) => theme.borderColor}; +` + +const SeparatorContextMenuItem = () => { + return ( + + + + ) +} + interface ContextMenuProps { contextMenu: ContextMenuContext } @@ -110,7 +135,7 @@ class ContextMenu extends React.Component { switch (menu.type) { case MenuTypes.Normal: return ( - { this.closeContextMenu() @@ -119,13 +144,15 @@ class ContextMenu extends React.Component { disabled={menu.enabled == null ? false : !menu.enabled} > {menu.label} - + ) + case MenuTypes.Separator: + return default: return ( - + Not implemented yet - + ) } })} diff --git a/src/components/organisms/Navigator.tsx b/src/components/organisms/Navigator.tsx new file mode 100644 index 0000000000..455c56983b --- /dev/null +++ b/src/components/organisms/Navigator.tsx @@ -0,0 +1,124 @@ +import React, { useMemo, useCallback } from 'react' +import { useRouter } from '../../lib/router' +import { useDb } from '../../lib/db' +import { entries } from '../../lib/db/utils' +import styled from '../../lib/styled' +import { useDialog, DialogIconTypes } from '../../lib/dialog' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import { usePreferences } from '../../lib/preferences' +import StorageNavigatorFragment from '../molecules/StorageNavigatorFragment' +import { mdiPlus, mdiHammerWrench } from '@mdi/js' +import NavigatorButton from '../atoms/NavigatorButton' +import Spacer from '../atoms/Spacer' +import { usePathnameWithoutNoteId } from '../../lib/router' + +const NavigatorContainer = styled.nav` + display: flex; + flex-direction: column; + height: 100%; + background-color: ${({ theme }) => theme.sideNavBackgroundColor}; +` + +const TopControl = styled.div` + display: flex; + margin: 1em 0; +` + +const Empty = styled.button` + width: 100%; + border: none; + text-decoration: underline; + padding: 0.25em; + text-align: center; + background-color: transparent; + cursor: pointer; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } +` + +const ScrollableContainer = styled.div` + flex: 1; + overflow: auto; +` + +const Navigator = () => { + const { createStorage, storageMap } = useDb() + const { popup } = useContextMenu() + const { prompt } = useDialog() + const { push } = useRouter() + const storageEntries = useMemo(() => { + return entries(storageMap) + }, [storageMap]) + + const openSideNavContextMenu = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Storage', + onClick: async () => { + prompt({ + title: 'Create a Storage', + message: 'Enter name of a storage to create', + iconType: DialogIconTypes.Question, + submitButtonLabel: 'Create Storage', + onClose: async (value: string | null) => { + if (value == null) return + const storage = await createStorage(value) + push(`/app/storages/${storage.id}/notes`) + }, + }) + }, + }, + ]) + }, + [popup, prompt, createStorage, push] + ) + const pathname = usePathnameWithoutNoteId() + const { toggleClosed } = usePreferences() + + return ( + + + + push('/app/storages')} + /> + + + + + {storageEntries.map(([, storage]) => ( + + ))} + {storageEntries.length === 0 && ( + push('/app/storages')}> + There are no storages. +
+ Click here to create one. +
+ )} + +
+
+ ) +} + +export default Navigator diff --git a/src/components/organisms/NoteDetail/NoteDetail.tsx b/src/components/organisms/NoteDetail/NoteDetail.tsx index d07ba1c810..5634c726e4 100644 --- a/src/components/organisms/NoteDetail/NoteDetail.tsx +++ b/src/components/organisms/NoteDetail/NoteDetail.tsx @@ -16,32 +16,32 @@ import ToolbarIconButton from '../../atoms/ToolbarIconButton' import Toolbar from '../../atoms/Toolbar' import ToolbarSeparator from '../../atoms/ToolbarSeparator' import { - secondaryBackgroundColor, textColor, borderBottom, borderRight, uiTextColor, PrimaryTextColor, + backgroundColor, } from '../../../lib/styled/styleFunctions' import ToolbarExportButton from '../../atoms/ToolbarExportButton' import { getFileList } from '../../../lib/dnd' import { ViewModeType } from '../../../lib/generalStatus' import { BreadCrumbs } from '../../pages/NotePage' import cc from 'classcat' -import { - IconTrash, - IconArrowAgain, - IconPreview, - IconSplitView, - IconEditView, -} from '../../icons' import { listenNoteDetailFocusTitleInputEvent, unlistenNoteDetailFocusTitleInputEvent, } from '../../../lib/events' +import { + mdiTrashCan, + mdiRestore, + mdiViewSplitVertical, + mdiCodeTags, + mdiTextSubject, +} from '@mdi/js' export const StyledNoteDetailContainer = styled.div` - ${secondaryBackgroundColor} + ${backgroundColor}; display: flex; flex-direction: column; height: 100%; @@ -580,35 +580,35 @@ export default class NoteDetail extends React.Component<
toggleViewMode('edit')} - icon={} + iconPath={mdiCodeTags} /> toggleViewMode('split')} - icon={} + iconPath={mdiViewSplitVertical} /> toggleViewMode('preview')} - icon={} + iconPath={mdiTextSubject} /> {note.trashed ? ( <> } + iconPath={mdiRestore} /> } + iconPath={mdiTrashCan} /> ) : ( } + iconPath={mdiTrashCan} /> )} diff --git a/src/components/organisms/NoteDetail/TagList.tsx b/src/components/organisms/NoteDetail/TagList.tsx index 90af19e1c5..e2cda5c834 100644 --- a/src/components/organisms/NoteDetail/TagList.tsx +++ b/src/components/organisms/NoteDetail/TagList.tsx @@ -1,12 +1,8 @@ import React, { useCallback } from 'react' -import ButtonIcon from '../../atoms/ButtonIcon' import styled from '../../../lib/styled' -import { - iconColor, - noteListIconColor, - inputStyle, -} from '../../../lib/styled/styleFunctions' -import { IconTag, IconClose } from '../../icons' +import { inputStyle } from '../../../lib/styled/styleFunctions' +import Icon from '../../atoms/Icon' +import { mdiTagMultiple, mdiClose } from '@mdi/js' interface TagListItemProps { tagName: string @@ -22,27 +18,28 @@ const TagListItem = ({ tagName, removeTagByName }: TagListItemProps) => {
{tagName}
) } -const StyledContainer = styled.div` +const TagListContainer = styled.div` display: flex; .listItem { display: flex; - margin: 0 2px; - ${inputStyle} margin-right: 5px; + padding: 0 0 0 0.5em; + ${inputStyle} + border-radius: 13px; } .icon { - ${noteListIconColor} + color: ${({ theme }) => theme.sideNavButtonColor}; } .listItem-label { - padding: 0 4px; + padding-right: 0.25em; line-height: 20px; } @@ -54,9 +51,18 @@ const StyledContainer = styled.div` display: flex; align-items: center; justify-content: center; - ${iconColor}; background-color: transparent; + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } svg { vertical-align: top; } @@ -70,8 +76,8 @@ interface TagListProps { const TagList = ({ tags, removeTagByName }: TagListProps) => { return ( - - } /> + + {tags.map((tag) => ( { removeTagByName={removeTagByName} /> ))} - + ) } diff --git a/src/components/organisms/NoteList/NoteList.tsx b/src/components/organisms/NoteList.tsx similarity index 69% rename from src/components/organisms/NoteList/NoteList.tsx rename to src/components/organisms/NoteList.tsx index b0899b5f04..f5d45d3eea 100644 --- a/src/components/organisms/NoteList/NoteList.tsx +++ b/src/components/organisms/NoteList.tsx @@ -1,24 +1,25 @@ import React, { useCallback, useRef, ChangeEventHandler } from 'react' -import NoteItem from './NoteItem' -import { PopulatedNoteDoc } from '../../../lib/db/types' -import styled from '../../../lib/styled' +import NoteItem from '../molecules/NoteItem' +import { PopulatedNoteDoc } from '../../lib/db/types' +import styled from '../../lib/styled' import { borderBottom, - inputStyle, - iconColor, noteListIconColor, selectTabStyle, -} from '../../../lib/styled/styleFunctions' -import { IconEdit, IconLoupe, IconArrowSingleDown } from '../../icons' + disabledUiTextColor, + inputStyle, +} from '../../lib/styled/styleFunctions' import { useTranslation } from 'react-i18next' import { useGlobalKeyDownHandler, isWithGeneralCtrlKey, -} from '../../../lib/keyboard' -import { NoteListSortOptions } from '../../pages/NotePage' -import { osName } from '../../../lib/platform' +} from '../../lib/keyboard' +import { NoteListSortOptions } from '../pages/NotePage' +import { osName } from '../../lib/platform' +import Icon from '../atoms/Icon' +import { mdiChevronDown, mdiPlus, mdiMagnify } from '@mdi/js' -export const StyledNoteListContainer = styled.div` +const NoteListContainer = styled.div` display: flex; flex-direction: column; overflow: hidden; @@ -32,71 +33,112 @@ export const StyledNoteListContainer = styled.div` overflow-y: auto; } - .control { - height: 50px; + .filterTab { + height: 25px; display: flex; - padding: 8px; align-items: center; - -webkit-app-region: drag; - ${borderBottom} + padding-left: 1em; + .filterIcon { + font-size: 10px; + margin-right: 5px; + z-index: 0; + pointer-events: none; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } + } + .input { + ${selectTabStyle} + } + select { + -webkit-appearance: none; + -moz-appearance: none; + border: none; + background: transparent; + } + ${borderBottom}; + ${noteListIconColor}; + } + .empty { + user-select: none; + padding: 10px; + ${disabledUiTextColor}; + } +` + +const Control = styled.div` + height: 50px; + display: flex; + padding: 0 0 0 8px; + align-items: center; + -webkit-app-region: drag; + ${borderBottom} + .newNoteButton { + width: 36px; + height: 36px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + + background-color: transparent; + border-radius: 50%; + border: none; + cursor: pointer; + + transition: color 200ms ease-in-out; + color: ${({ theme }) => theme.sideNavButtonColor}; + &:hover { + color: ${({ theme }) => theme.sideNavButtonHoverColor}; + } + + &:active, + .active { + color: ${({ theme }) => theme.sideNavButtonActiveColor}; + } } .searchInput { + ${inputStyle} flex: 1; position: relative; height: 32px; + color: ${({ theme }) => theme.sideNavButtonColor}; + border-radius: 4px; + overflow: hidden; + &:focus { + box-shadow: 0 0 0 2px ${({ theme }) => theme.primaryColor}; + } .icon { position: absolute; - top: 8px; + top: 6px; left: 10px; - font-size: 20px; + font-size: 18px; z-index: 0; pointer-events: none; ${noteListIconColor} } .input { + background-color: transparent; position: relative; width: 100%; - height: 32px; + height: 30px; padding-left: 35px; box-sizing: border-box; - ${inputStyle} - } - select { - appearance: none; - } - } - - .filterTab { - height: 25px; - display: flex; - align-items: center; - padding-left: 13px; - .filterIcon { - font-size: 10px; - margin-right: 5px; - z-index: 0; - pointer-events: none; - ${iconColor} - } - .input { - ${selectTabStyle} + border: none; } select { - -webkit-appearance: none; - -moz-appearance: none; appearance: none; } } - - .newNoteButton { - width: 35px; - height: 30px; - font-size: 24px; - background: transparent; - border: none; - ${noteListIconColor} - } ` type NoteListProps = { @@ -188,8 +230,8 @@ const NoteList = ({ ) return ( - -
+ +
- +
{currentStorageId != null && createNote != null && ( )} -
+
- +