feat: Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. (#2322)
This commit is contained in:
@@ -17,12 +17,12 @@ import {
|
||||
WebApplicationInterface,
|
||||
MobileDeviceInterface,
|
||||
MobileUnlockTiming,
|
||||
InternalEventBus,
|
||||
DecryptedItem,
|
||||
EditorIdentifier,
|
||||
FeatureIdentifier,
|
||||
Environment,
|
||||
ApplicationOptionsDefaults,
|
||||
BackupServiceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||
@@ -93,27 +93,26 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
})
|
||||
|
||||
deviceInterface.setApplication(this)
|
||||
const internalEventBus = new InternalEventBus()
|
||||
|
||||
this.itemControllerGroup = new ItemGroupController(this)
|
||||
this.routeService = new RouteService(this, internalEventBus)
|
||||
this.routeService = new RouteService(this, this.internalEventBus)
|
||||
|
||||
this.webServices = {} as WebServices
|
||||
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
|
||||
this.webServices.archiveService = new ArchiveManager(this)
|
||||
this.webServices.themeService = new ThemeManager(this, internalEventBus)
|
||||
this.webServices.themeService = new ThemeManager(this, this.internalEventBus)
|
||||
this.webServices.autolockService = this.isNativeMobileWeb()
|
||||
? undefined
|
||||
: new AutolockService(this, internalEventBus)
|
||||
: new AutolockService(this, this.internalEventBus)
|
||||
this.webServices.desktopService = isDesktopDevice(deviceInterface)
|
||||
? new DesktopManager(this, deviceInterface)
|
||||
? new DesktopManager(this, deviceInterface, this.fileBackups as BackupServiceInterface)
|
||||
: undefined
|
||||
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
|
||||
this.webServices.changelogService = new ChangelogService(this.environment, this.storage)
|
||||
this.webServices.momentsService = new MomentsService(
|
||||
this,
|
||||
this.webServices.viewControllerManager.filesController,
|
||||
internalEventBus,
|
||||
this.internalEventBus,
|
||||
)
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
@@ -181,6 +180,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
for (const observer of this.webEventObservers) {
|
||||
observer(event, data)
|
||||
}
|
||||
|
||||
this.internalEventBus.publish({ type: event, payload: data })
|
||||
}
|
||||
|
||||
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
|
||||
@@ -268,16 +269,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return this.protocolUpgradeAvailable()
|
||||
}
|
||||
|
||||
downloadBackup(): void | Promise<void> {
|
||||
if (isDesktopDevice(this.deviceInterface)) {
|
||||
return this.deviceInterface.downloadBackup()
|
||||
}
|
||||
}
|
||||
|
||||
async signOutAndDeleteLocalBackups(): Promise<void> {
|
||||
isDesktopDevice(this.deviceInterface) && (await this.deviceInterface.deleteLocalBackups())
|
||||
|
||||
return this.user.signOut()
|
||||
performDesktopTextBackup(): void | Promise<void> {
|
||||
return this.getDesktopService()?.saveDesktopBackup()
|
||||
}
|
||||
|
||||
isGlobalSpellcheckEnabled(): boolean {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
DesktopDeviceInterface,
|
||||
WebApplicationInterface,
|
||||
WebAppEvent,
|
||||
BackupServiceInterface,
|
||||
DesktopWatchedDirectoriesChanges,
|
||||
} from '@standardnotes/snjs'
|
||||
|
||||
export class DesktopManager
|
||||
@@ -27,10 +29,34 @@ export class DesktopManager
|
||||
dataLoaded = false
|
||||
lastSearchedText?: string
|
||||
|
||||
constructor(application: WebApplicationInterface, private device: DesktopDeviceInterface) {
|
||||
private textBackupsInterval: ReturnType<typeof setInterval> | undefined
|
||||
private needsInitialTextBackup = false
|
||||
|
||||
constructor(
|
||||
application: WebApplicationInterface,
|
||||
private device: DesktopDeviceInterface,
|
||||
private backups: BackupServiceInterface,
|
||||
) {
|
||||
super(application, new InternalEventBus())
|
||||
}
|
||||
|
||||
async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
|
||||
void this.backups.importWatchedDirectoryChanges(changes)
|
||||
}
|
||||
|
||||
beginTextBackupsTimer() {
|
||||
if (this.textBackupsInterval) {
|
||||
clearInterval(this.textBackupsInterval)
|
||||
}
|
||||
|
||||
this.needsInitialTextBackup = true
|
||||
|
||||
const hoursInterval = 12
|
||||
const seconds = hoursInterval * 60 * 60
|
||||
const milliseconds = seconds * 1000
|
||||
this.textBackupsInterval = setInterval(this.saveDesktopBackup, milliseconds)
|
||||
}
|
||||
|
||||
get webApplication() {
|
||||
return this.application as WebApplicationInterface
|
||||
}
|
||||
@@ -44,14 +70,35 @@ export class DesktopManager
|
||||
super.onAppEvent(eventName).catch(console.error)
|
||||
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||
this.dataLoaded = true
|
||||
this.device.onInitialDataLoad()
|
||||
if (this.backups.isTextBackupsEnabled()) {
|
||||
this.beginTextBackupsTimer()
|
||||
}
|
||||
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
||||
this.device.onMajorDataChange()
|
||||
void this.saveDesktopBackup()
|
||||
}
|
||||
}
|
||||
|
||||
saveBackup() {
|
||||
this.device.onMajorDataChange()
|
||||
async saveDesktopBackup() {
|
||||
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
|
||||
|
||||
const data = await this.getBackupFile()
|
||||
if (data) {
|
||||
await this.webApplication.fileBackups?.saveTextBackupData(data)
|
||||
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success: true })
|
||||
}
|
||||
}
|
||||
|
||||
private async getBackupFile(): Promise<string | undefined> {
|
||||
const encrypted = this.application.hasProtectionSources()
|
||||
const data = encrypted
|
||||
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
: await this.application.createDecryptedBackupFile()
|
||||
|
||||
if (data) {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
getExtServerHost(): string {
|
||||
@@ -111,6 +158,11 @@ export class DesktopManager
|
||||
|
||||
windowLostFocus(): void {
|
||||
this.webApplication.notifyWebEvent(WebAppEvent.WindowDidBlur)
|
||||
|
||||
if (this.needsInitialTextBackup) {
|
||||
this.needsInitialTextBackup = false
|
||||
void this.saveDesktopBackup()
|
||||
}
|
||||
}
|
||||
|
||||
async onComponentInstallationComplete(componentData: DecryptedTransferPayload<ComponentContent>) {
|
||||
@@ -136,25 +188,4 @@ export class DesktopManager
|
||||
observer.callback(updatedComponent as SNComponent)
|
||||
}
|
||||
}
|
||||
|
||||
async requestBackupFile(): Promise<string | undefined> {
|
||||
const encrypted = this.application.hasProtectionSources()
|
||||
const data = encrypted
|
||||
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
: await this.application.createDecryptedBackupFile()
|
||||
|
||||
if (data) {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
didBeginBackup() {
|
||||
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
|
||||
}
|
||||
|
||||
didFinishBackup(success: boolean) {
|
||||
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,7 @@ export {
|
||||
FileBackupReadToken,
|
||||
FileBackupReadChunkResponse,
|
||||
FileDownloadProgress,
|
||||
PlaintextBackupsMapping,
|
||||
DesktopWatchedDirectoriesChanges,
|
||||
DesktopWatchedDirectoriesChange,
|
||||
} from '@standardnotes/snjs'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { STRING_SIGN_OUT_CONFIRMATION } from '@/Constants/Strings'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
@@ -8,6 +8,7 @@ import { isDesktopApplication } from '@/Utils'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import AlertDialog from '../AlertDialog/AlertDialog'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -16,30 +17,24 @@ type Props = {
|
||||
}
|
||||
|
||||
const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewControllerManager, applicationGroup }) => {
|
||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false)
|
||||
const hasAnyBackupsEnabled =
|
||||
application.fileBackups?.isFilesBackupsEnabled() ||
|
||||
application.fileBackups?.isPlaintextBackupsEnabled() ||
|
||||
application.fileBackups?.isTextBackupsEnabled()
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||
const closeDialog = useCallback(() => {
|
||||
viewControllerManager.accountMenuController.setSigningOut(false)
|
||||
}, [viewControllerManager.accountMenuController])
|
||||
|
||||
const [localBackupsCount, setLocalBackupsCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
application.desktopDevice?.localBackupsCount().then(setLocalBackupsCount).catch(console.error)
|
||||
}, [viewControllerManager.accountMenuController.signingOut, application.desktopDevice])
|
||||
|
||||
const workspaces = applicationGroup.getDescriptors()
|
||||
const showWorkspaceWarning = workspaces.length > 1 && isDesktopApplication()
|
||||
|
||||
const confirm = useCallback(() => {
|
||||
if (deleteLocalBackups) {
|
||||
application.signOutAndDeleteLocalBackups().catch(console.error)
|
||||
} else {
|
||||
application.user.signOut().catch(console.error)
|
||||
}
|
||||
application.user.signOut().catch(console.error)
|
||||
|
||||
closeDialog()
|
||||
}, [application, closeDialog, deleteLocalBackups])
|
||||
}, [application, closeDialog])
|
||||
|
||||
return (
|
||||
<AlertDialog closeDialog={closeDialog}>
|
||||
@@ -66,31 +61,26 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewContro
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{localBackupsCount > 0 && (
|
||||
<div className="flex">
|
||||
<div className="sk-panel-row"></div>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={deleteLocalBackups}
|
||||
onChange={(event) => {
|
||||
setDeleteLocalBackups((event.target as HTMLInputElement).checked)
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
Delete {localBackupsCount} local backup file
|
||||
{localBackupsCount > 1 ? 's' : ''}
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
className="sk-a ml-1.5 cursor-pointer rounded p-0 capitalize"
|
||||
onClick={() => {
|
||||
application.desktopDevice?.viewlocalBackups()
|
||||
}}
|
||||
>
|
||||
View backup files
|
||||
</button>
|
||||
</div>
|
||||
{hasAnyBackupsEnabled && (
|
||||
<>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="flex">
|
||||
<div className="sk-panel-row"></div>
|
||||
<div>
|
||||
<p className="text-base text-foreground lg:text-sm">
|
||||
Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.
|
||||
</p>
|
||||
<button
|
||||
className="sk-a mt-2 cursor-pointer rounded p-0 capitalize lg:text-sm"
|
||||
onClick={() => {
|
||||
void application.fileBackups?.openAllDirectoriesContainingBackupFiles()
|
||||
}}
|
||||
>
|
||||
View backup files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
|
||||
@@ -35,7 +35,7 @@ export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }>
|
||||
>
|
||||
<div className="ml-2">
|
||||
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
||||
<div className="text-xs text-neutral">{backupInfo.absolutePath}</div>
|
||||
<div className="text-xs text-neutral">{application.fileBackups?.getFileBackupAbsolutePath(backupInfo)}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -153,7 +153,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
}
|
||||
|
||||
async processPasswordChange() {
|
||||
await this.application.downloadBackup()
|
||||
await this.application.performDesktopTextBackup()
|
||||
|
||||
this.setState({
|
||||
lockContinue: true,
|
||||
|
||||
@@ -58,7 +58,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
}
|
||||
|
||||
const processEmailChange = useCallback(async () => {
|
||||
await application.downloadBackup()
|
||||
await application.performDesktopTextBackup()
|
||||
|
||||
setLockContinue(true)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import DataBackups from './DataBackups'
|
||||
import EmailBackups from './EmailBackups'
|
||||
import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import TextBackupsCrossPlatform from './TextBackups/TextBackupsCrossPlatform'
|
||||
import PlaintextBackupsCrossPlatform from './PlaintextBackups/PlaintextBackupsCrossPlatform'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -16,6 +18,8 @@ const Backups: FunctionComponent<Props> = ({ application, viewControllerManager
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<DataBackups application={application} viewControllerManager={viewControllerManager} />
|
||||
<TextBackupsCrossPlatform application={application} />
|
||||
<PlaintextBackupsCrossPlatform />
|
||||
<FileBackupsCrossPlatform application={application} />
|
||||
<EmailBackups application={application} />
|
||||
</PreferencesPane>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
|
||||
import {
|
||||
STRING_IMPORT_SUCCESS,
|
||||
@@ -15,7 +14,7 @@ import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef,
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
@@ -177,14 +176,7 @@ const DataBackups = ({ application, viewControllerManager }: Props) => {
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Data Backups</Title>
|
||||
|
||||
{isDesktopApplication() && (
|
||||
<Text className="mb-3">
|
||||
Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Subtitle>Download a backup of all your data</Subtitle>
|
||||
<Subtitle>Download a backup of all your text-based data</Subtitle>
|
||||
|
||||
{isEncryptionEnabled && (
|
||||
<form className="sk-panel-form sk-panel-row">
|
||||
|
||||
@@ -118,7 +118,7 @@ const EmailBackups = ({ application }: Props) => {
|
||||
)}
|
||||
|
||||
<div className={`${!hasAccount ? 'pointer-events-none cursor-default opacity-50' : ''}`}>
|
||||
<Subtitle>Email frequency</Subtitle>
|
||||
<Subtitle>Frequency</Subtitle>
|
||||
<Text>How often to receive backups.</Text>
|
||||
<div className="mt-2">
|
||||
{isLoading ? (
|
||||
|
||||
@@ -15,13 +15,13 @@ const FileBackupsCrossPlatform = ({ application }: Props) => {
|
||||
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||
|
||||
return fileBackupsService ? (
|
||||
<FileBackupsDesktop application={application} backupsService={fileBackupsService} />
|
||||
<FileBackupsDesktop backupsService={fileBackupsService} />
|
||||
) : (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>File Backups</Title>
|
||||
<Subtitle>Automatically save encrypted backups of files uploaded on any device to this computer.</Subtitle>
|
||||
<Title>Automatic File Backups</Title>
|
||||
<Subtitle>Automatically save encrypted backups of your files.</Subtitle>
|
||||
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
|
||||
</PreferencesSegment>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
@@ -10,30 +9,21 @@ import BackupsDropZone from './BackupsDropZone'
|
||||
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
backupsService: NonNullable<WebApplication['fileBackups']>
|
||||
backupsService: BackupServiceInterface
|
||||
}
|
||||
|
||||
const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
||||
const [backupsEnabled, setBackupsEnabled] = useState(false)
|
||||
const [backupsLocation, setBackupsLocation] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
void backupsService.isFilesBackupsEnabled().then(setBackupsEnabled)
|
||||
}, [backupsService])
|
||||
|
||||
useEffect(() => {
|
||||
if (backupsEnabled) {
|
||||
void backupsService.getFilesBackupsLocation().then(setBackupsLocation)
|
||||
}
|
||||
}, [backupsService, backupsEnabled])
|
||||
const FileBackupsDesktop = ({ backupsService }: Props) => {
|
||||
const application = useApplication()
|
||||
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isFilesBackupsEnabled())
|
||||
const [backupsLocation, setBackupsLocation] = useState(backupsService.getFilesBackupsLocation())
|
||||
|
||||
const changeBackupsLocation = useCallback(async () => {
|
||||
await backupsService.changeFilesBackupsLocation()
|
||||
|
||||
setBackupsLocation(await backupsService.getFilesBackupsLocation())
|
||||
const newLocation = await backupsService.changeFilesBackupsLocation()
|
||||
setBackupsLocation(newLocation)
|
||||
}, [backupsService])
|
||||
|
||||
const openBackupsLocation = useCallback(async () => {
|
||||
@@ -42,25 +32,24 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
||||
|
||||
const toggleBackups = useCallback(async () => {
|
||||
if (backupsEnabled) {
|
||||
await backupsService.disableFilesBackups()
|
||||
backupsService.disableFilesBackups()
|
||||
} else {
|
||||
await backupsService.enableFilesBackups()
|
||||
}
|
||||
|
||||
setBackupsEnabled(await backupsService.isFilesBackupsEnabled())
|
||||
setBackupsEnabled(backupsService.isFilesBackupsEnabled())
|
||||
setBackupsLocation(backupsService.getFilesBackupsLocation())
|
||||
}, [backupsService, backupsEnabled])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>File Backups</Title>
|
||||
<Title>Automatic File Backups</Title>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-10 flex flex-col">
|
||||
<Subtitle>
|
||||
Automatically save encrypted backups of files uploaded on any device to this computer.
|
||||
</Subtitle>
|
||||
<Subtitle>Automatically save encrypted backups of your uploaded files to this computer.</Subtitle>
|
||||
</div>
|
||||
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||
</div>
|
||||
@@ -85,14 +74,14 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
||||
</Text>
|
||||
|
||||
<EncryptionStatusItem
|
||||
status={backupsLocation}
|
||||
status={backupsLocation || 'Not Set'}
|
||||
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||
checkmark={false}
|
||||
/>
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Open Backups Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||
<Button label="Change Backups Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||
</div>
|
||||
</>
|
||||
</PreferencesSegment>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import PlaintextBackupsDesktop from './PlaintextBackupsDesktop'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
const PlaintextBackupsCrossPlatform = () => {
|
||||
const application = useApplication()
|
||||
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||
|
||||
return fileBackupsService ? (
|
||||
<PlaintextBackupsDesktop backupsService={fileBackupsService} />
|
||||
) : (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Automatic Plaintext Backups</Title>
|
||||
<Subtitle>Automatically save backups of all your notes into plaintext, non-encrypted folders.</Subtitle>
|
||||
<Text className="mt-3">To enable plaintext backups, use the Standard Notes desktop application.</Text>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlaintextBackupsCrossPlatform
|
||||
@@ -0,0 +1,89 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
backupsService: BackupServiceInterface
|
||||
}
|
||||
|
||||
const PlaintextBackupsDesktop = ({ backupsService }: Props) => {
|
||||
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isPlaintextBackupsEnabled())
|
||||
const [backupsLocation, setBackupsLocation] = useState(backupsService.getPlaintextBackupsLocation())
|
||||
|
||||
const changeBackupsLocation = useCallback(async () => {
|
||||
const newLocation = await backupsService.changePlaintextBackupsLocation()
|
||||
setBackupsLocation(newLocation)
|
||||
}, [backupsService])
|
||||
|
||||
const openBackupsLocation = useCallback(async () => {
|
||||
await backupsService.openPlaintextBackupsLocation()
|
||||
}, [backupsService])
|
||||
|
||||
const toggleBackups = useCallback(async () => {
|
||||
if (backupsEnabled) {
|
||||
backupsService.disablePlaintextBackups()
|
||||
} else {
|
||||
await backupsService.enablePlaintextBackups()
|
||||
}
|
||||
|
||||
setBackupsEnabled(backupsService.isPlaintextBackupsEnabled())
|
||||
setBackupsLocation(backupsService.getPlaintextBackupsLocation())
|
||||
}, [backupsEnabled, backupsService])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Automatic Plaintext Backups</Title>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-10 flex flex-col">
|
||||
<Subtitle>
|
||||
Automatically save backups of all your notes to this computer into plaintext, non-encrypted folders.
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||
</div>
|
||||
|
||||
{!backupsEnabled && (
|
||||
<>
|
||||
<HorizontalSeparator classes="mt-2.5 mb-4" />
|
||||
<Text>Plaintext backups are not enabled. Enable to choose where your data is backed up.</Text>
|
||||
</>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
|
||||
{backupsEnabled && (
|
||||
<>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<PreferencesSegment>
|
||||
<>
|
||||
<Text className="mb-3">Plaintext backups are enabled and saved to:</Text>
|
||||
<EncryptionStatusItem
|
||||
status={backupsLocation || 'Not Set'}
|
||||
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||
checkmark={false}
|
||||
/>
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||
</div>
|
||||
</>
|
||||
</PreferencesSegment>
|
||||
</>
|
||||
)}
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(PlaintextBackupsDesktop)
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { useMemo } from 'react'
|
||||
import TextBackupsDesktop from './TextBackupsDesktop'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const TextBackupsCrossPlatform = ({ application }: Props) => {
|
||||
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||
|
||||
return fileBackupsService ? (
|
||||
<TextBackupsDesktop backupsService={fileBackupsService} />
|
||||
) : (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Automatic Text Backups</Title>
|
||||
<Subtitle>Automatically save encrypted and decrypted backups of your note and tag data.</Subtitle>
|
||||
<Text className="mt-3">To enable text backups, use the Standard Notes desktop application.</Text>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextBackupsCrossPlatform
|
||||
@@ -0,0 +1,106 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
backupsService: BackupServiceInterface
|
||||
}
|
||||
|
||||
const TextBackupsDesktop = ({ backupsService }: Props) => {
|
||||
const application = useApplication()
|
||||
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isTextBackupsEnabled())
|
||||
const [backupsLocation, setBackupsLocation] = useState(backupsService.getTextBackupsLocation())
|
||||
|
||||
const changeBackupsLocation = useCallback(async () => {
|
||||
const newLocation = await backupsService.changeTextBackupsLocation()
|
||||
setBackupsLocation(newLocation)
|
||||
}, [backupsService])
|
||||
|
||||
const openBackupsLocation = useCallback(async () => {
|
||||
await backupsService.openTextBackupsLocation()
|
||||
}, [backupsService])
|
||||
|
||||
const toggleBackups = useCallback(async () => {
|
||||
if (backupsEnabled) {
|
||||
backupsService.disableTextBackups()
|
||||
} else {
|
||||
await backupsService.enableTextBackups()
|
||||
}
|
||||
|
||||
setBackupsEnabled(backupsService.isTextBackupsEnabled())
|
||||
setBackupsLocation(backupsService.getTextBackupsLocation())
|
||||
}, [backupsEnabled, backupsService])
|
||||
|
||||
const performBackup = useCallback(async () => {
|
||||
void application.getDesktopService()?.saveDesktopBackup()
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Automatic Encrypted Text Backups</Title>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-10 flex flex-col">
|
||||
<Subtitle>
|
||||
Automatically save encrypted text backups of all your note and tag data to this computer.
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||
</div>
|
||||
|
||||
{!backupsEnabled && (
|
||||
<>
|
||||
<HorizontalSeparator classes="mt-2.5 mb-4" />
|
||||
<Text>Text backups are not enabled. Enable to choose where your data is backed up.</Text>
|
||||
</>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
|
||||
{backupsEnabled && (
|
||||
<>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
|
||||
<PreferencesSegment>
|
||||
<>
|
||||
<Text className="mb-3">Text backups are enabled and saved to:</Text>
|
||||
|
||||
<EncryptionStatusItem
|
||||
status={backupsLocation || 'Not Set'}
|
||||
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||
checkmark={false}
|
||||
/>
|
||||
|
||||
<div className="mt-2.5 flex flex-row">
|
||||
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
|
||||
<Text className="mb-3">
|
||||
Backups are saved automatically throughout the day. You can perform a one-time backup now below.
|
||||
</Text>
|
||||
<div className="flex flex-row">
|
||||
<Button label="Perform Backup" className={'mr-3 text-xs'} onClick={performBackup} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</>
|
||||
)}
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TextBackupsDesktop)
|
||||
@@ -12,7 +12,7 @@ const TwoFactorTitle: FunctionComponent<Props> = ({ auth }) => {
|
||||
return <Title>Two-factor authentication not available</Title>
|
||||
}
|
||||
|
||||
return <Title>Two-factor authentication</Title>
|
||||
return <Title>Two-Factor Authentication</Title>
|
||||
}
|
||||
|
||||
export default observer(TwoFactorTitle)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||
import { InternalEventBus, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
|
||||
import { InternalEventBusInterface, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '../../Application/Application'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
|
||||
@@ -10,7 +10,7 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
|
||||
protected disposers: Disposer[] = []
|
||||
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
|
||||
|
||||
constructor(public application: WebApplication, protected eventBus: InternalEventBus) {}
|
||||
constructor(public application: WebApplication, protected eventBus: InternalEventBusInterface) {}
|
||||
|
||||
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
|
||||
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { ApplicationEvent, InternalEventBus, StorageKey } from '@standardnotes/services'
|
||||
import { ApplicationEvent, InternalEventBusInterface, StorageKey } from '@standardnotes/services'
|
||||
import { isDev } from '@/Utils'
|
||||
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
||||
import { FilesController } from '../FilesController'
|
||||
@@ -19,7 +19,11 @@ export class MomentsService extends AbstractViewController {
|
||||
isEnabled = false
|
||||
private intervalReference: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
constructor(application: WebApplication, private filesController: FilesController, eventBus: InternalEventBus) {
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
private filesController: FilesController,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(application, eventBus)
|
||||
|
||||
this.disposers.push(
|
||||
|
||||
@@ -4,10 +4,10 @@ import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services'
|
||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
const DEFAULT_PANE: PreferenceId = 'account'
|
||||
const DEFAULT_PANE: PreferenceId = 'backups'
|
||||
|
||||
export class PreferencesController extends AbstractViewController {
|
||||
private _open = false
|
||||
private _open = true
|
||||
currentPane: PreferenceId = DEFAULT_PANE
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||
|
||||
Reference in New Issue
Block a user