feat: add desktop repo (#1071)
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||
import { FileBackupsDevice } from '@web/Application/Device/DesktopSnjsExports'
|
||||
|
||||
export interface CrossProcessBridge extends FileBackupsDevice {
|
||||
get extServerHost(): string
|
||||
|
||||
get useNativeKeychain(): boolean
|
||||
|
||||
get rendererPath(): string
|
||||
|
||||
get isMacOS(): boolean
|
||||
|
||||
get appVersion(): string
|
||||
|
||||
get useSystemMenuBar(): boolean
|
||||
|
||||
closeWindow(): void
|
||||
|
||||
minimizeWindow(): void
|
||||
|
||||
maximizeWindow(): void
|
||||
|
||||
unmaximizeWindow(): void
|
||||
|
||||
isWindowMaximized(): boolean
|
||||
|
||||
getKeychainValue(): Promise<unknown>
|
||||
|
||||
setKeychainValue: (value: unknown) => Promise<void>
|
||||
|
||||
clearKeychainValue(): Promise<boolean>
|
||||
|
||||
localBackupsCount(): Promise<number>
|
||||
|
||||
viewlocalBackups(): void
|
||||
|
||||
deleteLocalBackups(): Promise<void>
|
||||
|
||||
saveDataBackup(data: unknown): void
|
||||
|
||||
displayAppMenu(): void
|
||||
|
||||
syncComponents(components: Component[]): void
|
||||
|
||||
onMajorDataChange(): void
|
||||
|
||||
onSearch(text: string): void
|
||||
|
||||
onInitialDataLoad(): void
|
||||
|
||||
destroyAllData(): void
|
||||
}
|
||||
154
packages/desktop/app/javascripts/Renderer/DesktopDevice.ts
Normal file
154
packages/desktop/app/javascripts/Renderer/DesktopDevice.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||
import {
|
||||
RawKeychainValue,
|
||||
Environment,
|
||||
DesktopDeviceInterface,
|
||||
FileBackupsMapping,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { CrossProcessBridge } from './CrossProcessBridge'
|
||||
|
||||
const FallbackLocalStorageKey = 'keychain'
|
||||
|
||||
export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceInterface {
|
||||
public environment: Environment.Desktop = Environment.Desktop
|
||||
|
||||
constructor(
|
||||
private remoteBridge: CrossProcessBridge,
|
||||
private useNativeKeychain: boolean,
|
||||
public extensionsServerHost: string,
|
||||
appVersion: string,
|
||||
) {
|
||||
super(appVersion)
|
||||
}
|
||||
|
||||
async getKeychainValue() {
|
||||
if (this.useNativeKeychain) {
|
||||
const keychainValue = await this.remoteBridge.getKeychainValue()
|
||||
return keychainValue
|
||||
} else {
|
||||
const value = window.localStorage.getItem(FallbackLocalStorageKey)
|
||||
if (value) {
|
||||
return JSON.parse(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setKeychainValue(value: RawKeychainValue) {
|
||||
if (this.useNativeKeychain) {
|
||||
await this.remoteBridge.setKeychainValue(value)
|
||||
} else {
|
||||
window.localStorage.setItem(FallbackLocalStorageKey, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
async clearRawKeychainValue() {
|
||||
if (this.useNativeKeychain) {
|
||||
await this.remoteBridge.clearKeychainValue()
|
||||
} else {
|
||||
window.localStorage.removeItem(FallbackLocalStorageKey)
|
||||
}
|
||||
}
|
||||
|
||||
syncComponents(components: Component[]) {
|
||||
this.remoteBridge.syncComponents(components)
|
||||
}
|
||||
|
||||
onMajorDataChange() {
|
||||
this.remoteBridge.onMajorDataChange()
|
||||
}
|
||||
|
||||
onSearch(text: string) {
|
||||
this.remoteBridge.onSearch(text)
|
||||
}
|
||||
|
||||
onInitialDataLoad() {
|
||||
this.remoteBridge.onInitialDataLoad()
|
||||
}
|
||||
|
||||
async clearAllDataFromDevice(workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
|
||||
await super.clearAllDataFromDevice(workspaceIdentifiers)
|
||||
|
||||
this.remoteBridge.destroyAllData()
|
||||
|
||||
return { killsApplication: true }
|
||||
}
|
||||
|
||||
async downloadBackup() {
|
||||
const receiver = window.webClient
|
||||
|
||||
receiver.didBeginBackup()
|
||||
|
||||
try {
|
||||
const data = await receiver.requestBackupFile()
|
||||
if (data) {
|
||||
this.remoteBridge.saveDataBackup(data)
|
||||
} else {
|
||||
receiver.didFinishBackup(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
receiver.didFinishBackup(false)
|
||||
}
|
||||
}
|
||||
|
||||
async localBackupsCount() {
|
||||
return this.remoteBridge.localBackupsCount()
|
||||
}
|
||||
|
||||
viewlocalBackups() {
|
||||
this.remoteBridge.viewlocalBackups()
|
||||
}
|
||||
|
||||
async deleteLocalBackups() {
|
||||
return this.remoteBridge.deleteLocalBackups()
|
||||
}
|
||||
|
||||
public isFilesBackupsEnabled(): Promise<boolean> {
|
||||
return this.remoteBridge.isFilesBackupsEnabled()
|
||||
}
|
||||
|
||||
public enableFilesBackups(): Promise<void> {
|
||||
return this.remoteBridge.enableFilesBackups()
|
||||
}
|
||||
|
||||
public disableFilesBackups(): Promise<void> {
|
||||
return this.remoteBridge.disableFilesBackups()
|
||||
}
|
||||
|
||||
public changeFilesBackupsLocation(): Promise<string | undefined> {
|
||||
return this.remoteBridge.changeFilesBackupsLocation()
|
||||
}
|
||||
|
||||
public getFilesBackupsLocation(): Promise<string> {
|
||||
return this.remoteBridge.getFilesBackupsLocation()
|
||||
}
|
||||
|
||||
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
|
||||
return this.remoteBridge.getFilesBackupsMappingFile()
|
||||
}
|
||||
|
||||
async openFilesBackupsLocation(): Promise<void> {
|
||||
return this.remoteBridge.openFilesBackupsLocation()
|
||||
}
|
||||
|
||||
async saveFilesBackupsFile(
|
||||
uuid: string,
|
||||
metaFile: string,
|
||||
downloadRequest: {
|
||||
chunkSizes: number[]
|
||||
valetToken: string
|
||||
url: string
|
||||
},
|
||||
): Promise<'success' | 'failed'> {
|
||||
return this.remoteBridge.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
|
||||
}
|
||||
|
||||
async performHardReset(): Promise<void> {
|
||||
console.error('performHardReset is not yet implemented')
|
||||
}
|
||||
|
||||
isDeviceDestroyed(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
42
packages/desktop/app/javascripts/Renderer/Preload.ts
Normal file
42
packages/desktop/app/javascripts/Renderer/Preload.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||
const { ipcRenderer } = require('electron')
|
||||
const path = require('path')
|
||||
const rendererPath = path.join('file://', __dirname, '/renderer.js')
|
||||
const RemoteBridge = require('@electron/remote').getGlobal('RemoteBridge')
|
||||
const { contextBridge } = require('electron')
|
||||
|
||||
process.once('loaded', function () {
|
||||
contextBridge.exposeInMainWorld('electronRemoteBridge', RemoteBridge.exposableValue)
|
||||
|
||||
listenForIpcEventsFromMainProcess()
|
||||
})
|
||||
|
||||
function listenForIpcEventsFromMainProcess() {
|
||||
const sendMessageToRenderProcess = (message: string, payload = {}) => {
|
||||
window.postMessage(JSON.stringify({ message, data: payload }), rendererPath)
|
||||
}
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.UpdateAvailable, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.UpdateAvailable, data)
|
||||
})
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.PerformAutomatedBackup, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.PerformAutomatedBackup, data)
|
||||
})
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.FinishedSavingBackup, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.FinishedSavingBackup, data)
|
||||
})
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.WindowBlurred, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.WindowBlurred, data)
|
||||
})
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.WindowFocused, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.WindowFocused, data)
|
||||
})
|
||||
|
||||
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, function (_event, data) {
|
||||
sendMessageToRenderProcess(MessageToWebApp.InstallComponentComplete, data)
|
||||
})
|
||||
}
|
||||
164
packages/desktop/app/javascripts/Renderer/Renderer.ts
Normal file
164
packages/desktop/app/javascripts/Renderer/Renderer.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { DesktopDevice } from './DesktopDevice'
|
||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||
import { DesktopClientRequiresWebMethods } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { StartApplication } from '@web/Application/Device/StartApplication'
|
||||
import { CrossProcessBridge } from './CrossProcessBridge'
|
||||
|
||||
declare const DEFAULT_SYNC_SERVER: string
|
||||
declare const WEBSOCKET_URL: string
|
||||
declare const ENABLE_UNFINISHED_FEATURES: string
|
||||
declare const PURCHASE_URL: string
|
||||
declare const PLANS_URL: string
|
||||
declare const DASHBOARD_URL: string
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
device: DesktopDevice
|
||||
electronRemoteBridge: CrossProcessBridge
|
||||
dashboardUrl: string
|
||||
webClient: DesktopClientRequiresWebMethods
|
||||
electronAppVersion: string
|
||||
enableUnfinishedFeatures: boolean
|
||||
plansUrl: string
|
||||
purchaseUrl: string
|
||||
startApplication: StartApplication
|
||||
zip: any
|
||||
}
|
||||
}
|
||||
|
||||
const loadWindowVarsRequiredByWebApp = () => {
|
||||
window.dashboardUrl = DASHBOARD_URL
|
||||
window.enableUnfinishedFeatures = ENABLE_UNFINISHED_FEATURES === 'true'
|
||||
window.plansUrl = PLANS_URL
|
||||
window.purchaseUrl = PURCHASE_URL
|
||||
}
|
||||
|
||||
const loadAndStartApplication = async () => {
|
||||
const remoteBridge: CrossProcessBridge = window.electronRemoteBridge
|
||||
|
||||
await configureWindow(remoteBridge)
|
||||
|
||||
window.device = await createDesktopDevice(remoteBridge)
|
||||
|
||||
window.startApplication(DEFAULT_SYNC_SERVER, window.device, window.enableUnfinishedFeatures, WEBSOCKET_URL)
|
||||
|
||||
listenForMessagesSentFromMainToPreloadToUs(window.device)
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
loadWindowVarsRequiredByWebApp()
|
||||
|
||||
void loadAndStartApplication()
|
||||
}
|
||||
|
||||
/** @returns whether the keychain structure is up to date or not */
|
||||
async function migrateKeychain(remoteBridge: CrossProcessBridge): Promise<boolean> {
|
||||
if (!remoteBridge.useNativeKeychain) {
|
||||
/** User chose not to use keychain, do not migrate. */
|
||||
return false
|
||||
}
|
||||
|
||||
const key = 'keychain'
|
||||
const localStorageValue = window.localStorage.getItem(key)
|
||||
|
||||
if (localStorageValue) {
|
||||
/** Migrate to native keychain */
|
||||
console.warn('Migrating keychain from localStorage to native keychain.')
|
||||
window.localStorage.removeItem(key)
|
||||
await remoteBridge.setKeychainValue(JSON.parse(localStorageValue))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function createDesktopDevice(remoteBridge: CrossProcessBridge): Promise<DesktopDevice> {
|
||||
const useNativeKeychain = await migrateKeychain(remoteBridge)
|
||||
const extensionsServerHost = remoteBridge.extServerHost
|
||||
const appVersion = remoteBridge.appVersion
|
||||
|
||||
return new DesktopDevice(remoteBridge, useNativeKeychain, extensionsServerHost, appVersion)
|
||||
}
|
||||
|
||||
async function configureWindow(remoteBridge: CrossProcessBridge) {
|
||||
const isMacOS = remoteBridge.isMacOS
|
||||
const useSystemMenuBar = remoteBridge.useSystemMenuBar
|
||||
const appVersion = remoteBridge.appVersion
|
||||
|
||||
window.electronAppVersion = appVersion
|
||||
|
||||
/*
|
||||
Title bar events
|
||||
*/
|
||||
document.getElementById('menu-btn')!.addEventListener('click', () => {
|
||||
remoteBridge.displayAppMenu()
|
||||
})
|
||||
|
||||
document.getElementById('min-btn')!.addEventListener('click', () => {
|
||||
remoteBridge.minimizeWindow()
|
||||
})
|
||||
|
||||
document.getElementById('max-btn')!.addEventListener('click', async () => {
|
||||
if (remoteBridge.isWindowMaximized()) {
|
||||
remoteBridge.unmaximizeWindow()
|
||||
} else {
|
||||
remoteBridge.maximizeWindow()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('close-btn')!.addEventListener('click', () => {
|
||||
remoteBridge.closeWindow()
|
||||
})
|
||||
|
||||
// For Mac inset window
|
||||
const sheet = document.styleSheets[0]
|
||||
if (isMacOS) {
|
||||
sheet.insertRule('#navigation { padding-top: 25px !important; }', sheet.cssRules.length)
|
||||
}
|
||||
|
||||
if (isMacOS || useSystemMenuBar) {
|
||||
// !important is important here because #desktop-title-bar has display: flex.
|
||||
sheet.insertRule('#desktop-title-bar { display: none !important; }', sheet.cssRules.length)
|
||||
} else {
|
||||
/* Use custom title bar. Take the sn-titlebar-height off of
|
||||
the app content height so its not overflowing */
|
||||
sheet.insertRule('body { padding-top: var(--sn-desktop-titlebar-height); }', sheet.cssRules.length)
|
||||
sheet.insertRule(
|
||||
`.main-ui-view { height: calc(100vh - var(--sn-desktop-titlebar-height)) !important;
|
||||
min-height: calc(100vh - var(--sn-desktop-titlebar-height)) !important; }`,
|
||||
sheet.cssRules.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function listenForMessagesSentFromMainToPreloadToUs(device: DesktopDevice) {
|
||||
window.addEventListener('message', async (event) => {
|
||||
// We don't have access to the full file path.
|
||||
if (event.origin !== 'file://') {
|
||||
return
|
||||
}
|
||||
let payload
|
||||
try {
|
||||
payload = JSON.parse(event.data)
|
||||
} catch (e) {
|
||||
// message doesn't belong to us
|
||||
return
|
||||
}
|
||||
const receiver = window.webClient
|
||||
const message = payload.message
|
||||
const data = payload.data
|
||||
|
||||
if (message === MessageToWebApp.WindowBlurred) {
|
||||
receiver.windowLostFocus()
|
||||
} else if (message === MessageToWebApp.WindowFocused) {
|
||||
receiver.windowGainedFocus()
|
||||
} else if (message === MessageToWebApp.InstallComponentComplete) {
|
||||
receiver.onComponentInstallationComplete(data.component, data.error)
|
||||
} else if (message === MessageToWebApp.UpdateAvailable) {
|
||||
receiver.updateAvailable()
|
||||
} else if (message === MessageToWebApp.PerformAutomatedBackup) {
|
||||
void device.downloadBackup()
|
||||
} else if (message === MessageToWebApp.FinishedSavingBackup) {
|
||||
receiver.didFinishBackup(data.success)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable no-undef */
|
||||
const { ipcRenderer } = require('electron')
|
||||
import { MessageToMainProcess } from '../Shared/IpcMessages'
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('use-storage-button').addEventListener('click', () => {
|
||||
ipcRenderer.send(MessageToMainProcess.UseLocalstorageForKeychain)
|
||||
})
|
||||
|
||||
document.getElementById('quit-button').addEventListener('click', () => {
|
||||
ipcRenderer.send(MessageToMainProcess.Quit)
|
||||
})
|
||||
|
||||
const learnMoreButton = document.getElementById('learn-more')
|
||||
learnMoreButton.addEventListener('click', (event) => {
|
||||
ipcRenderer.send(MessageToMainProcess.LearnMoreAboutKeychainAccess)
|
||||
event.preventDefault()
|
||||
const moreInfo = document.getElementById('more-info')
|
||||
moreInfo.style.display = 'block'
|
||||
learnMoreButton.style.display = 'none'
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user