mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
[CP-2490] Backup (#1742)
This commit is contained in:
committed by
GitHub
parent
5706e171d3
commit
cc3d76d72e
@@ -2,5 +2,6 @@ module.exports = {
|
||||
extends: "@mudita/stylelint-config",
|
||||
rules: {
|
||||
"no-descending-specificity": null,
|
||||
}
|
||||
"selector-type-no-unknown": [true, { ignoreTypes: ["$dummyValue"] }],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ import installExtension, {
|
||||
REDUX_DEVTOOLS,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer"
|
||||
import { AppEvents, callRenderer } from "shared/utils"
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
@@ -190,6 +191,10 @@ const createWindow = async () => {
|
||||
app.exit()
|
||||
})
|
||||
|
||||
win.on("focus", () => {
|
||||
callRenderer(AppEvents.WindowFocused)
|
||||
})
|
||||
|
||||
new MetadataInitializer(metadataStore).init()
|
||||
|
||||
const registerDownloadListener = createDownloadListenerRegistrar(win)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,593 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import "reflect-metadata"
|
||||
import { check as checkPort } from "tcp-port-used"
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
shell,
|
||||
} from "electron"
|
||||
import { ipcMain } from "electron-better-ipc"
|
||||
import * as path from "path"
|
||||
import * as url from "url"
|
||||
import packageInfo from "Root/apps/mudita-center/package.json"
|
||||
import registerPureOsDownloadListener from "Core/__deprecated__/main/functions/register-pure-os-download-listener"
|
||||
import registerNewsListener from "Core/__deprecated__/main/functions/register-news-listener/register-news-listener"
|
||||
import registerAppLogsListeners from "Core/__deprecated__/main/functions/register-app-logs-listener"
|
||||
import registerContactsExportListener from "Core/contacts/backend/export-contacts"
|
||||
import registerWriteFileListener from "Core/__deprecated__/main/functions/register-write-file-listener"
|
||||
import registerCopyFileListener from "Core/__deprecated__/main/functions/register-copy-file-listener"
|
||||
import registerWriteGzipListener from "Core/__deprecated__/main/functions/register-write-gzip-listener"
|
||||
import registerRmdirListener from "Core/__deprecated__/main/functions/register-rmdir-listener"
|
||||
import registerArchiveFilesListener from "Core/__deprecated__/main/functions/register-archive-files-listener"
|
||||
import createDownloadListenerRegistrar from "Core/__deprecated__/main/functions/create-download-listener-registrar"
|
||||
import {
|
||||
registerDownloadHelpHandler,
|
||||
removeDownloadHelpHandler,
|
||||
} from "Core/__deprecated__/main/functions/download-help-handler"
|
||||
import {
|
||||
registerSetHelpStoreHandler,
|
||||
removeSetHelpStoreHandler,
|
||||
} from "Core/__deprecated__/main/functions/set-help-store-handler"
|
||||
import {
|
||||
registerGetHelpStoreHandler,
|
||||
removeGetHelpStoreHandler,
|
||||
} from "Core/__deprecated__/main/functions/get-help-store-handler"
|
||||
import { GoogleAuthActions } from "Core/__deprecated__/common/enums/google-auth-actions.enum"
|
||||
import {
|
||||
authServerPort,
|
||||
createAuthServer,
|
||||
killAuthServer,
|
||||
} from "Core/__deprecated__/main/auth-server"
|
||||
import logger from "Core/__deprecated__/main/utils/logger"
|
||||
import { Scope } from "Core/__deprecated__/renderer/models/external-providers/google/google.interface"
|
||||
import { OutlookAuthActions } from "Core/__deprecated__/common/enums/outlook-auth-actions.enum"
|
||||
import {
|
||||
clientId,
|
||||
redirectUrl,
|
||||
} from "Core/__deprecated__/renderer/models/external-providers/outlook/outlook.constants"
|
||||
import { TokenRequester } from "Core/__deprecated__/renderer/models/external-providers/outlook/token-requester"
|
||||
import {
|
||||
GOOGLE_AUTH_WINDOW_SIZE,
|
||||
WINDOW_SIZE,
|
||||
DEFAULT_WINDOWS_SIZE,
|
||||
} from "./config"
|
||||
import autoupdate, { mockAutoupdate } from "./autoupdate"
|
||||
import {
|
||||
URL_MAIN,
|
||||
URL_OVERVIEW,
|
||||
} from "Core/__deprecated__/renderer/constants/urls"
|
||||
import { Mode } from "Core/__deprecated__/common/enums/mode.enum"
|
||||
import { HelpActions } from "Core/__deprecated__/common/enums/help-actions.enum"
|
||||
import { AboutActions } from "Core/__deprecated__/common/enums/about-actions.enum"
|
||||
import { PureSystemActions } from "Core/__deprecated__/common/enums/pure-system-actions.enum"
|
||||
import { BrowserActions } from "Core/__deprecated__/common/enums/browser-actions.enum"
|
||||
import {
|
||||
createMetadataStore,
|
||||
MetadataStore,
|
||||
MetadataInitializer,
|
||||
registerMetadataAllGetValueListener,
|
||||
registerMetadataGetValueListener,
|
||||
registerMetadataSetValueListener,
|
||||
} from "Core/metadata"
|
||||
import { registerOsUpdateAlreadyDownloadedCheck } from "Core/update/requests"
|
||||
import { createSettingsService } from "Core/settings/containers/settings.container"
|
||||
import { ApplicationModule } from "Core/core/application.module"
|
||||
import registerExternalUsageDevice from "Core/device/listeners/register-external-usage-device.listner"
|
||||
import installExtension, {
|
||||
REDUX_DEVTOOLS,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer"
|
||||
import isPrereleaseSet from "Core/utils/is-prerelease-set"
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
require("dotenv").config()
|
||||
|
||||
// FIXME: electron v12 added changes to the remote module. This module has many subtle pitfalls.
|
||||
// There is almost always a better way to accomplish your task than using this module.
|
||||
// You can read more in https://github.com/electron/remote#migrating-from-remote
|
||||
require("@electron/remote/main").initialize()
|
||||
|
||||
logger.info("Starting the app")
|
||||
|
||||
let win: BrowserWindow | null
|
||||
let helpWindow: BrowserWindow | null = null
|
||||
let googleAuthWindow: BrowserWindow | null = null
|
||||
let outlookAuthWindow: BrowserWindow | null = null
|
||||
const licenseWindow: BrowserWindow | null = null
|
||||
const termsWindow: BrowserWindow | null = null
|
||||
const policyWindow: BrowserWindow | null = null
|
||||
const metadataStore: MetadataStore = createMetadataStore()
|
||||
|
||||
// Disabling browser security features
|
||||
// to address CORS issue between local and remote servers.
|
||||
// To be handled as part of ticket https://appnroll.atlassian.net/browse/CP-2242
|
||||
app.commandLine.appendSwitch(
|
||||
"disable-features",
|
||||
"BlockInsecurePrivateNetworkRequests,PrivateNetworkAccessSendPreflights"
|
||||
)
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
// Fetch and log all errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(error)
|
||||
// TODO: Add contact support modal
|
||||
})
|
||||
|
||||
const productionEnvironment = process.env.NODE_ENV === "production"
|
||||
const commonWindowOptions: BrowserWindowConstructorOptions = {
|
||||
resizable: true,
|
||||
fullscreen: false,
|
||||
fullscreenable: true,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webSecurity: false,
|
||||
// FIXME: electron v12 throw error: 'Require' is not defined. `contextIsolation` default value is changed to `true`.
|
||||
// You can read more in https://www.electronjs.org/blog/electron-12-0#breaking-changes
|
||||
contextIsolation: false,
|
||||
},
|
||||
}
|
||||
const getWindowOptions = (
|
||||
extendedWindowOptions?: BrowserWindowConstructorOptions
|
||||
) => ({
|
||||
...commonWindowOptions,
|
||||
...extendedWindowOptions,
|
||||
})
|
||||
|
||||
const installElectronDevToolExtensions = async () => {
|
||||
try {
|
||||
await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS], {
|
||||
loadExtensionOptions: {
|
||||
allowFileAccess: true,
|
||||
},
|
||||
})
|
||||
console.info(`[INFO] Successfully added devtools extensions`)
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[WARN] An error occurred while trying to add devtools extensions:\n",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const createWindow = async () => {
|
||||
const version = packageInfo.version
|
||||
const prereleaseSet = isPrereleaseSet(version)
|
||||
const title = prereleaseSet ? `Mudita Center - ${version}` : "Mudita Center"
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call
|
||||
;(global as any).__static = require("path")
|
||||
.join(__dirname, "/static")
|
||||
.replace(/\\/g, "\\\\")
|
||||
|
||||
win = new BrowserWindow(
|
||||
getWindowOptions({
|
||||
minWidth: WINDOW_SIZE.minWidth,
|
||||
width: WINDOW_SIZE.width,
|
||||
minHeight: WINDOW_SIZE.minHeight,
|
||||
height: WINDOW_SIZE.height,
|
||||
title,
|
||||
})
|
||||
)
|
||||
// FIXME: electron v12 added changes to the remote module. This module has many subtle pitfalls.
|
||||
// There is almost always a better way to accomplish your task than using this module.
|
||||
// You can read more in https://github.com/electron/remote#migrating-from-remote
|
||||
require("@electron/remote/main").enable(win.webContents)
|
||||
win.removeMenu()
|
||||
|
||||
win.webContents.on("before-input-event", (event, input) => {
|
||||
if ((input.control || input.meta) && input.key.toLowerCase() === "r") {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
win.on("closed", () => {
|
||||
win = null
|
||||
app.exit()
|
||||
})
|
||||
|
||||
new MetadataInitializer(metadataStore).init()
|
||||
|
||||
const registerDownloadListener = createDownloadListenerRegistrar(win)
|
||||
|
||||
const settingsService = createSettingsService()
|
||||
settingsService.init()
|
||||
|
||||
const appModules = new ApplicationModule(ipcMain, win)
|
||||
|
||||
registerPureOsDownloadListener(registerDownloadListener)
|
||||
registerOsUpdateAlreadyDownloadedCheck()
|
||||
registerNewsListener()
|
||||
registerAppLogsListeners()
|
||||
registerContactsExportListener()
|
||||
registerWriteFileListener()
|
||||
registerCopyFileListener()
|
||||
registerRmdirListener()
|
||||
registerWriteGzipListener()
|
||||
registerArchiveFilesListener()
|
||||
registerMetadataAllGetValueListener()
|
||||
registerMetadataGetValueListener()
|
||||
registerMetadataSetValueListener()
|
||||
registerExternalUsageDevice()
|
||||
|
||||
if (productionEnvironment) {
|
||||
win.setMenuBarVisibility(false)
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
})
|
||||
)
|
||||
autoupdate(win)
|
||||
} else {
|
||||
await installElectronDevToolExtensions()
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "1"
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
win.loadURL(`http://localhost:2003`)
|
||||
mockAutoupdate(win)
|
||||
}
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return {
|
||||
action: "deny",
|
||||
overrideBrowserWindowOptions: {},
|
||||
}
|
||||
})
|
||||
|
||||
if (productionEnvironment) {
|
||||
win.webContents.once("dom-ready", () => {
|
||||
appModules.lateInitialization()
|
||||
})
|
||||
} else {
|
||||
// Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready
|
||||
win.webContents.once("dom-ready", () => {
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
win!.webContents.openDevTools()
|
||||
appModules.lateInitialization()
|
||||
})
|
||||
|
||||
win.webContents.once("dom-ready", () => {
|
||||
win!.webContents.once("devtools-opened", () => {
|
||||
win!.focus()
|
||||
})
|
||||
win!.webContents.openDevTools()
|
||||
})
|
||||
}
|
||||
|
||||
logger.updateMetadata()
|
||||
}
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
app.on("ready", createWindow)
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
app.on("activate", () => {
|
||||
if (win === null) {
|
||||
void createWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ipcMain.answerRenderer(HelpActions.OpenWindow, () => {
|
||||
const title = "Mudita Center - Help"
|
||||
if (helpWindow === null) {
|
||||
helpWindow = new BrowserWindow(
|
||||
getWindowOptions({
|
||||
width: DEFAULT_WINDOWS_SIZE.width,
|
||||
height: DEFAULT_WINDOWS_SIZE.height,
|
||||
title,
|
||||
})
|
||||
)
|
||||
// FIXME: electron v12 added changes to the remote module. This module has many subtle pitfalls.
|
||||
// There is almost always a better way to accomplish your task than using this module.
|
||||
// You can read more in https://github.com/electron/remote#migrating-from-remote
|
||||
require("@electron/remote/main").enable(helpWindow.webContents)
|
||||
helpWindow.removeMenu()
|
||||
|
||||
helpWindow.on("closed", () => {
|
||||
removeDownloadHelpHandler()
|
||||
removeSetHelpStoreHandler()
|
||||
removeGetHelpStoreHandler()
|
||||
helpWindow = null
|
||||
})
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
helpWindow.loadURL(
|
||||
!productionEnvironment
|
||||
? `http://localhost:2003/?mode=${Mode.Help}#${URL_MAIN.help}`
|
||||
: url.format({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
hash: URL_MAIN.help,
|
||||
search: `?mode=${Mode.Help}`,
|
||||
})
|
||||
)
|
||||
registerDownloadHelpHandler()
|
||||
registerSetHelpStoreHandler()
|
||||
registerGetHelpStoreHandler()
|
||||
} else {
|
||||
helpWindow.show()
|
||||
}
|
||||
})
|
||||
|
||||
const createOpenWindowListener = (
|
||||
channel: string,
|
||||
mode: string,
|
||||
urlMain: string,
|
||||
title: string,
|
||||
newWindow: BrowserWindow | null = null
|
||||
) => {
|
||||
ipcMain.answerRenderer(channel, async () => {
|
||||
if (newWindow === null) {
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
newWindow = new BrowserWindow(
|
||||
getWindowOptions({
|
||||
width: DEFAULT_WINDOWS_SIZE.width,
|
||||
height: DEFAULT_WINDOWS_SIZE.height,
|
||||
title,
|
||||
})
|
||||
)
|
||||
// FIXME: electron v12 added changes to the remote module. This module has many subtle pitfalls.
|
||||
// There is almost always a better way to accomplish your task than using this module.
|
||||
// You can read more in https://github.com/electron/remote#migrating-from-remote
|
||||
require("@electron/remote/main").enable(newWindow.webContents)
|
||||
newWindow.removeMenu()
|
||||
|
||||
newWindow.on("closed", () => {
|
||||
newWindow = null
|
||||
})
|
||||
|
||||
await newWindow.loadURL(
|
||||
!productionEnvironment
|
||||
? `http://localhost:2003/?mode=${mode}#${urlMain}`
|
||||
: url.format({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
hash: urlMain,
|
||||
search: `?mode=${mode}`,
|
||||
})
|
||||
)
|
||||
|
||||
newWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return {
|
||||
action: "allow",
|
||||
overrideBrowserWindowOptions: {},
|
||||
}
|
||||
})
|
||||
} else {
|
||||
newWindow.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ipcMain.answerRenderer(BrowserActions.PolicyOpenBrowser, () =>
|
||||
shell.openExternal(
|
||||
`${process.env.MUDITA_CENTER_SERVER_URL ?? ""}/privacy-policy-url`
|
||||
)
|
||||
)
|
||||
ipcMain.answerRenderer(BrowserActions.UpdateOpenBrowser, () =>
|
||||
shell.openExternal("https://mudita.com")
|
||||
)
|
||||
|
||||
ipcMain.answerRenderer(BrowserActions.AppleOpenBrowser, () =>
|
||||
shell.openExternal(
|
||||
"https://support.apple.com/en-al/guide/contacts/adrbdcfd32e6/mac#:~:text=In%20the%20Contacts%20app%20on,vcf)%20only."
|
||||
)
|
||||
)
|
||||
|
||||
createOpenWindowListener(
|
||||
AboutActions.LicenseOpenWindow,
|
||||
Mode.License,
|
||||
URL_MAIN.license,
|
||||
"Mudita Center - License",
|
||||
licenseWindow
|
||||
)
|
||||
|
||||
createOpenWindowListener(
|
||||
AboutActions.TermsOpenWindow,
|
||||
Mode.TermsOfService,
|
||||
URL_MAIN.termsOfService,
|
||||
"Mudita Center - Terms of service",
|
||||
termsWindow
|
||||
)
|
||||
|
||||
createOpenWindowListener(
|
||||
AboutActions.PolicyOpenWindow,
|
||||
Mode.PrivacyPolicy,
|
||||
URL_MAIN.privacyPolicy,
|
||||
"Mudita Center - Privacy policy",
|
||||
policyWindow
|
||||
)
|
||||
|
||||
createOpenWindowListener(
|
||||
PureSystemActions.SarOpenWindow,
|
||||
Mode.Sar,
|
||||
URL_OVERVIEW.sar,
|
||||
"Mudita Center - SAR information",
|
||||
policyWindow
|
||||
)
|
||||
|
||||
const createErrorWindow = async (googleAuthWindow: BrowserWindow) => {
|
||||
return await googleAuthWindow.loadURL(
|
||||
!productionEnvironment
|
||||
? `http://localhost:2003/?mode=${Mode.ServerError}#${URL_MAIN.error}`
|
||||
: url.format({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
hash: URL_MAIN.error,
|
||||
search: `?mode=${Mode.ServerError}`,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
ipcMain.answerRenderer(GoogleAuthActions.OpenWindow, async (scope: Scope) => {
|
||||
const title = "Mudita Center - Google Auth"
|
||||
if (process.env.MUDITA_CENTER_SERVER_URL) {
|
||||
const cb = (data: string) => {
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ipcMain.callRenderer(
|
||||
win as BrowserWindow,
|
||||
GoogleAuthActions.GotCredentials,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
if (googleAuthWindow === null) {
|
||||
googleAuthWindow = new BrowserWindow(
|
||||
getWindowOptions({
|
||||
width: GOOGLE_AUTH_WINDOW_SIZE.width,
|
||||
height: GOOGLE_AUTH_WINDOW_SIZE.height,
|
||||
title,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
googleAuthWindow.removeMenu()
|
||||
|
||||
googleAuthWindow.on("close", () => {
|
||||
void ipcMain.callRenderer(
|
||||
win as BrowserWindow,
|
||||
GoogleAuthActions.CloseWindow
|
||||
)
|
||||
googleAuthWindow = null
|
||||
killAuthServer()
|
||||
})
|
||||
|
||||
if (await checkPort(authServerPort)) {
|
||||
await createErrorWindow(googleAuthWindow)
|
||||
return
|
||||
}
|
||||
|
||||
createAuthServer(cb)
|
||||
|
||||
let scopeUrl: string
|
||||
|
||||
switch (scope) {
|
||||
case Scope.Calendar:
|
||||
scopeUrl = "https://www.googleapis.com/auth/calendar"
|
||||
break
|
||||
case Scope.Contacts:
|
||||
scopeUrl = "https://www.googleapis.com/auth/contacts"
|
||||
break
|
||||
}
|
||||
const url = `${process.env.MUDITA_CENTER_SERVER_URL}/google-auth-init`
|
||||
void (await googleAuthWindow.loadURL(`${url}?scope=${scopeUrl}`))
|
||||
} else {
|
||||
googleAuthWindow.show()
|
||||
}
|
||||
} else {
|
||||
console.log("No Google Auth URL defined!")
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.answerRenderer(GoogleAuthActions.CloseWindow, () => {
|
||||
killAuthServer()
|
||||
googleAuthWindow?.close()
|
||||
})
|
||||
|
||||
ipcMain.answerRenderer(
|
||||
OutlookAuthActions.OpenWindow,
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async (data: { authorizationUrl: string; scope: string }) => {
|
||||
const title = "Mudita Center - Outlook Auth"
|
||||
const { authorizationUrl, scope } = data
|
||||
if (clientId) {
|
||||
if (outlookAuthWindow === null) {
|
||||
outlookAuthWindow = new BrowserWindow(
|
||||
getWindowOptions({
|
||||
width: 600,
|
||||
height: 600,
|
||||
title,
|
||||
})
|
||||
)
|
||||
outlookAuthWindow.removeMenu()
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
outlookAuthWindow.loadURL(authorizationUrl)
|
||||
|
||||
outlookAuthWindow.on("close", () => {
|
||||
void ipcMain.callRenderer(
|
||||
win as BrowserWindow,
|
||||
OutlookAuthActions.CloseWindow
|
||||
)
|
||||
outlookAuthWindow = null
|
||||
})
|
||||
|
||||
const {
|
||||
session: { webRequest },
|
||||
} = outlookAuthWindow.webContents
|
||||
webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [`${redirectUrl}*`],
|
||||
}, // * character is used to "catch all" url params
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
async ({ url }) => {
|
||||
const code = new URL(url).searchParams.get("code") || ""
|
||||
try {
|
||||
const tokenRequester = new TokenRequester()
|
||||
const tokens = await tokenRequester.requestTokens(code, scope)
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ipcMain.callRenderer(
|
||||
win as BrowserWindow,
|
||||
OutlookAuthActions.GotCredentials,
|
||||
tokens
|
||||
)
|
||||
} catch (error) {
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ipcMain.callRenderer(
|
||||
win as BrowserWindow,
|
||||
OutlookAuthActions.GotCredentials,
|
||||
{ error }
|
||||
)
|
||||
}
|
||||
|
||||
outlookAuthWindow?.close()
|
||||
outlookAuthWindow = null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
outlookAuthWindow.show()
|
||||
}
|
||||
} else {
|
||||
logger.info("No Outlook Auth URL defined!")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.answerRenderer(OutlookAuthActions.CloseWindow, () => {
|
||||
outlookAuthWindow?.close()
|
||||
})
|
||||
@@ -16,6 +16,10 @@ import store from "Core/__deprecated__/renderer/store"
|
||||
|
||||
jest.mock("Core/feature-flags")
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
test("matches snapshot without tabs", () => {
|
||||
const currentLocation = "/overview"
|
||||
const { container } = renderWithThemeAndIntl(
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
} from "Core/device-initialization/reducers/device-initialization.interface"
|
||||
import { DeviceManagerState } from "Core/device-manager/reducers/device-manager.interface"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/feature-flags")
|
||||
|
||||
type Props = MenuProps
|
||||
|
||||
@@ -862,5 +862,32 @@
|
||||
"module.genericBackup.createButtonLabel": "Create backup",
|
||||
"module.genericBackup.restoreButtonLabel": "Restore from backup",
|
||||
"module.genericViews.update.tag": "Update is available ({version}).",
|
||||
"module.genericViews.update.actionLabel": "You can update it on your device."
|
||||
"module.genericViews.update.actionLabel": "You can update it on your device.",
|
||||
"module.genericViews.backup.features.title": "Create backup",
|
||||
"module.genericViews.backup.features.description": "All backup data stays on your computer.",
|
||||
"module.genericViews.backup.features.cancelButtonLabel": "Cancel",
|
||||
"module.genericViews.backup.features.createButtonLabel": "Create backup",
|
||||
"module.genericViews.backup.password.title": "Create password for backup",
|
||||
"module.genericViews.backup.password.subtitle": "(optional)",
|
||||
"module.genericViews.backup.password.description": "You can protect backup with a new password.",
|
||||
"module.genericViews.backup.password.description2": "* You can't change/recover the password later.",
|
||||
"module.genericViews.backup.password.passwordPlaceholder": "Password",
|
||||
"module.genericViews.backup.password.passwordRepeatPlaceholder": "Repeat password",
|
||||
"module.genericViews.backup.password.passwordRepeatNotMatchingError": "Passwords do not match",
|
||||
"module.genericViews.backup.password.confirmButtonLabel": "Confirm password",
|
||||
"module.genericViews.backup.password.skipButtonLabel": "Skip password",
|
||||
"module.genericViews.backup.progress.title": "Creating backup",
|
||||
"module.genericViews.backup.progress.description": "Please wait and do not unplug your device from computer.",
|
||||
"module.genericViews.backup.progress.progressDetails": "This might take a few minutes",
|
||||
"module.genericViews.backup.progress.progressDetailsForFeature": "Backing up {featureLabel}",
|
||||
"module.genericViews.backup.success.title": "Backup complete",
|
||||
"module.genericViews.backup.success.description": "Your data was successfully secured.\nOpen the backup folder to see your backup data or close this window.",
|
||||
"module.genericViews.backup.success.openBackupButtonLabel": "Open backup folder",
|
||||
"module.genericViews.backup.success.closeButtonLabel": "Close",
|
||||
"module.genericViews.backup.failure.title": "Backup failed",
|
||||
"module.genericViews.backup.failure.defaultErrorMessage": "The backup process was interrupted.",
|
||||
"module.genericViews.backup.failure.closeButtonLabel": "Close",
|
||||
"module.genericViews.backup.directoryOpenFailure.title": "Directory open failed",
|
||||
"module.genericViews.backup.directoryOpenFailure.defaultErrorMessage": "The directory could not be opened.\n\nYou can still try to find the backup in the default location:",
|
||||
"module.genericViews.backup.directoryOpenFailure.closeButtonLabel": "Close"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { appInitializationReducer } from "Core/app-initialization/reducers/app-i
|
||||
import { deviceManagerReducer } from "Core/device-manager/reducers/device-manager.reducer"
|
||||
import {
|
||||
genericBackupsReducer,
|
||||
genericFileTransferReducer,
|
||||
genericModalsReducer,
|
||||
genericViewsReducer,
|
||||
} from "generic-view/store"
|
||||
@@ -51,6 +52,7 @@ export const reducers = {
|
||||
genericViews: genericViewsReducer,
|
||||
genericModals: genericModalsReducer,
|
||||
genericBackups: genericBackupsReducer,
|
||||
genericFileTransfer: genericFileTransferReducer,
|
||||
appState: appStateReducer,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ import { AppUpdateStepModalTestIds } from "Core/__deprecated__/renderer/wrappers
|
||||
import { Provider } from "react-redux"
|
||||
import store from "../../store"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
const onCloseMock = jest.fn()
|
||||
const openExternalMock = jest.fn()
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import ModalsManager from "Core/modals-manager/components/modals-manager.contain
|
||||
import ContactSupportFlow from "Core/contact-support/containers/contact-support-flow.container"
|
||||
import { ContactSupportFlowTestIds } from "Core/contact-support/components/contact-support-flow-test-ids.component"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
type Props = ComponentProps<typeof ModalsManager>
|
||||
|
||||
const defaultProps: Props = {}
|
||||
|
||||
@@ -38,6 +38,10 @@ window.IntersectionObserver = jest
|
||||
|
||||
type Props = ComponentProps<typeof Contacts>
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
Menu: () => ({
|
||||
popup: jest.fn(),
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from "Core/device-manager/services"
|
||||
import { APIModule } from "device/feature"
|
||||
import { FileSystemDialogModule } from "shared/app-state"
|
||||
import { SystemUtilsModule } from "system-utils/feature"
|
||||
|
||||
export class ApplicationModule {
|
||||
public modules: Module[] = [
|
||||
@@ -89,6 +90,7 @@ export class ApplicationModule {
|
||||
new DeviceResolverService(),
|
||||
this.eventEmitter
|
||||
)
|
||||
private systemUtilsModule = new SystemUtilsModule()
|
||||
|
||||
constructor(
|
||||
private ipc: MainProcessIpc,
|
||||
@@ -105,11 +107,12 @@ export class ApplicationModule {
|
||||
this.initializeInitializer = new InitializeInitializer()
|
||||
|
||||
this.modules.forEach(this.initModule)
|
||||
this.apiModule = new APIModule(this.deviceManager)
|
||||
this.apiModule = new APIModule(this.deviceManager, this.systemUtilsModule)
|
||||
this.controllerInitializer.initialize(this.apiModule.getAPIServices())
|
||||
this.controllerInitializer.initialize(
|
||||
FileSystemDialogModule.getControllers()
|
||||
)
|
||||
this.controllerInitializer.initialize(this.systemUtilsModule.getServices())
|
||||
}
|
||||
|
||||
lateInitialization(): void {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { AppError } from "Core/core/errors"
|
||||
import { AppError, AppErrorType } from "Core/core/errors"
|
||||
|
||||
export type ResultObject<
|
||||
Data,
|
||||
ErrorType extends string = string,
|
||||
ErrorType extends AppErrorType = AppErrorType,
|
||||
ErrorData = unknown
|
||||
> = SuccessResult<Data> | FailedResult<ErrorData, ErrorType>
|
||||
|
||||
@@ -18,7 +18,7 @@ export class SuccessResult<Data> {
|
||||
constructor(public data: Data) {}
|
||||
}
|
||||
|
||||
export class FailedResult<Data, ErrorType extends string = string> {
|
||||
export class FailedResult<Data, ErrorType extends AppErrorType = AppErrorType> {
|
||||
public ok: false = false
|
||||
|
||||
constructor(
|
||||
@@ -32,7 +32,7 @@ export class Result {
|
||||
return new SuccessResult<Data>(data)
|
||||
}
|
||||
|
||||
static failed<Data, ErrorType extends string = string>(
|
||||
static failed<Data, ErrorType extends AppErrorType = AppErrorType>(
|
||||
error: AppError<ErrorType>,
|
||||
data?: Data
|
||||
): FailedResult<Data, ErrorType> {
|
||||
|
||||
@@ -35,99 +35,101 @@ import ConfiguredDevicesDiscovery from "Core/discovery-device/components/configu
|
||||
import DevicesInitialization from "Core/device-initialization/components/devices-initialization.component"
|
||||
import AvailableDeviceListContainer from "Core/discovery-device/components/available-device-list.container"
|
||||
import DeviceConnecting from "Core/discovery-device/components/device-connecting.component"
|
||||
import { GenericView } from "generic-view/feature"
|
||||
import { ApiDeviceModals, GenericView } from "generic-view/feature"
|
||||
import { APIConnectionDemo } from "generic-view/ui"
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export default () => (
|
||||
<Switch>
|
||||
<Redirect exact from={URL_MAIN.root} to={URL_ONBOARDING.root} />
|
||||
<Redirect from={URL_ONBOARDING.root} to={URL_ONBOARDING.welcome} exact />
|
||||
<>
|
||||
<Switch>
|
||||
<Redirect exact from={URL_MAIN.root} to={URL_ONBOARDING.root} />
|
||||
<Redirect from={URL_ONBOARDING.root} to={URL_ONBOARDING.welcome} exact />
|
||||
|
||||
<Route exact path={[...Object.values(URL_ONBOARDING)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route path={URL_ONBOARDING.welcome} component={Onboarding} />
|
||||
<Route
|
||||
path={URL_ONBOARDING.troubleshooting}
|
||||
component={OnboardingTroubleshooting}
|
||||
/>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Route exact path={URL_OVERVIEW.pureSystem}>
|
||||
<LayoutDesktopWrapperWithoutHeader>
|
||||
<Route path={URL_OVERVIEW.pureSystem} component={PureSystem} />
|
||||
</LayoutDesktopWrapperWithoutHeader>
|
||||
</Route>
|
||||
|
||||
<Route exact path={[...Object.values(URL_DISCOVERY_DEVICE)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route
|
||||
path={URL_DISCOVERY_DEVICE.root}
|
||||
component={ConfiguredDevicesDiscovery}
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path={URL_DISCOVERY_DEVICE.deviceConnecting}
|
||||
component={DeviceConnecting}
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path={URL_DISCOVERY_DEVICE.availableDeviceListModal}
|
||||
component={AvailableDeviceListContainer}
|
||||
exact
|
||||
/>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Route exact path={[...Object.values(URL_DEVICE_INITIALIZATION)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route
|
||||
path={URL_DEVICE_INITIALIZATION.root}
|
||||
component={DevicesInitialization}
|
||||
/>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Route>
|
||||
<LayoutDesktopWrapper>
|
||||
<Switch>
|
||||
<Route exact path={[...Object.values(URL_ONBOARDING)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route path={URL_ONBOARDING.welcome} component={Onboarding} />
|
||||
<Route
|
||||
path={"/generic/api-connection-demo"}
|
||||
component={APIConnectionDemo}
|
||||
path={URL_ONBOARDING.troubleshooting}
|
||||
component={OnboardingTroubleshooting}
|
||||
/>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Route exact path={URL_OVERVIEW.pureSystem}>
|
||||
<LayoutDesktopWrapperWithoutHeader>
|
||||
<Route path={URL_OVERVIEW.pureSystem} component={PureSystem} />
|
||||
</LayoutDesktopWrapperWithoutHeader>
|
||||
</Route>
|
||||
|
||||
<Route exact path={[...Object.values(URL_DISCOVERY_DEVICE)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route
|
||||
path={URL_DISCOVERY_DEVICE.root}
|
||||
component={ConfiguredDevicesDiscovery}
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path={"/generic/:viewKey/:subviewKey"}
|
||||
component={GenericView}
|
||||
/>
|
||||
<Route path={"/generic/:viewKey"} component={GenericView} />
|
||||
<Route path={URL_MAIN.filesManager} component={FilesManager} />
|
||||
<Route path={URL_MAIN.messages} component={Messages} exact />
|
||||
<Route
|
||||
path={`${URL_MAIN.messages}${URL_TABS.templates}`}
|
||||
component={TemplatesContainer}
|
||||
/>
|
||||
<Route path={URL_MAIN.news} component={News} />
|
||||
<Route path={URL_OVERVIEW.root} component={Overview} exact />
|
||||
<Route path={URL_MAIN.contacts} component={Contacts} exact />
|
||||
<Route path={URL_MAIN.settings} component={BackupContainer} exact />
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.notifications}`}
|
||||
component={NotificationsContainer}
|
||||
path={URL_DISCOVERY_DEVICE.deviceConnecting}
|
||||
component={DeviceConnecting}
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.audioConversion}`}
|
||||
component={AudioConversionContainer}
|
||||
path={URL_DISCOVERY_DEVICE.availableDeviceListModal}
|
||||
component={AvailableDeviceListContainer}
|
||||
exact
|
||||
/>
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.about}`}
|
||||
component={AboutContainer}
|
||||
/>
|
||||
</Switch>
|
||||
</LayoutDesktopWrapper>
|
||||
</Route>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Redirect to={URL_OVERVIEW.root} />
|
||||
</Switch>
|
||||
<Route exact path={[...Object.values(URL_DEVICE_INITIALIZATION)]}>
|
||||
<LayoutBlankWrapper>
|
||||
<Route
|
||||
path={URL_DEVICE_INITIALIZATION.root}
|
||||
component={DevicesInitialization}
|
||||
/>
|
||||
</LayoutBlankWrapper>
|
||||
</Route>
|
||||
|
||||
<Route>
|
||||
<LayoutDesktopWrapper>
|
||||
<Switch>
|
||||
<Route
|
||||
path={"/generic/api-connection-demo"}
|
||||
component={APIConnectionDemo}
|
||||
/>
|
||||
<Route
|
||||
path={"/generic/:viewKey/:subviewKey"}
|
||||
component={GenericView}
|
||||
/>
|
||||
<Route path={"/generic/:viewKey"} component={GenericView} />
|
||||
<Route path={URL_MAIN.filesManager} component={FilesManager} />
|
||||
<Route path={URL_MAIN.messages} component={Messages} exact />
|
||||
<Route
|
||||
path={`${URL_MAIN.messages}${URL_TABS.templates}`}
|
||||
component={TemplatesContainer}
|
||||
/>
|
||||
<Route path={URL_MAIN.news} component={News} />
|
||||
<Route path={URL_OVERVIEW.root} component={Overview} exact />
|
||||
<Route path={URL_MAIN.contacts} component={Contacts} exact />
|
||||
<Route path={URL_MAIN.settings} component={BackupContainer} exact />
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.notifications}`}
|
||||
component={NotificationsContainer}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.audioConversion}`}
|
||||
component={AudioConversionContainer}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL_MAIN.settings}${URL_TABS.about}`}
|
||||
component={AboutContainer}
|
||||
/>
|
||||
</Switch>
|
||||
</LayoutDesktopWrapper>
|
||||
</Route>
|
||||
<Redirect to={URL_OVERVIEW.root} />
|
||||
</Switch>
|
||||
<ApiDeviceModals />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -18,8 +18,12 @@ import { useDeviceDetachedEffect } from "Core/core/hooks/use-device-detached-eff
|
||||
import { useDeviceConnectFailedEffect } from "Core/core/hooks/use-device-connect-failed-effect"
|
||||
import { useDiscoveryRedirectEffect } from "Core/core/hooks/use-discovery-redirect-effect"
|
||||
import { useRouterListener } from "Core/core/hooks"
|
||||
import { useAPISerialPortListeners, useOutbox } from "generic-view/store"
|
||||
|
||||
import {
|
||||
OutboxWrapper,
|
||||
useAPISerialPortListeners,
|
||||
useBackupList,
|
||||
useAppEventsListeners,
|
||||
} from "generic-view/store"
|
||||
|
||||
const BaseApp: FunctionComponent = () => {
|
||||
useRouterListener()
|
||||
@@ -33,10 +37,11 @@ const BaseApp: FunctionComponent = () => {
|
||||
useDiscoveryRedirectEffect()
|
||||
// API
|
||||
useAPISerialPortListeners()
|
||||
useOutbox()
|
||||
|
||||
useAppEventsListeners()
|
||||
useBackupList()
|
||||
return (
|
||||
<>
|
||||
<OutboxWrapper />
|
||||
<CrashDump />
|
||||
<NetworkStatusChecker />
|
||||
<ModalsManager />
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export class AppError<Type extends string = string> extends Error {
|
||||
export type AppErrorType = string | number
|
||||
|
||||
export class AppError<
|
||||
Type extends AppErrorType = AppErrorType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Payload = any
|
||||
> extends Error {
|
||||
constructor(
|
||||
public readonly type: Type,
|
||||
public readonly message: string = "",
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||
public readonly payload?: any
|
||||
public readonly payload?: Payload
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
*/
|
||||
|
||||
export { AppError } from "./app-error"
|
||||
export type { AppErrorType } from "./app-error"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ApiError } from "device/models"
|
||||
import { intl } from "Core/__deprecated__/renderer/utils/intl"
|
||||
import { defineMessages } from "react-intl"
|
||||
import {
|
||||
closeButtonStyles,
|
||||
IconButton,
|
||||
ModalBase,
|
||||
ModalCenteredContent,
|
||||
@@ -122,9 +123,9 @@ export const APIDeviceInitializationModalFlow: FunctionComponent = () => {
|
||||
variant={"small"}
|
||||
overlayHidden
|
||||
closeButton={
|
||||
<IconButton onClick={onModalClose}>
|
||||
<CloseButton onClick={onModalClose}>
|
||||
<ModalCloseIcon />
|
||||
</IconButton>
|
||||
</CloseButton>
|
||||
}
|
||||
>
|
||||
<ModalCenteredContent>
|
||||
@@ -151,3 +152,7 @@ const ConnectingText = styled.p`
|
||||
font-weight: ${({ theme }) => theme.fontWeight.bold};
|
||||
margin: 2.4rem 0 0;
|
||||
`
|
||||
|
||||
const CloseButton = styled(IconButton)`
|
||||
${closeButtonStyles};
|
||||
`
|
||||
|
||||
@@ -8,12 +8,14 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { DeviceManagerEvent } from "Core/device-manager/constants"
|
||||
import { setActiveDeviceRequest } from "Core/device-manager/requests"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { cleanBackupProcess } from "generic-view/store"
|
||||
|
||||
export const setActiveDevice = createAsyncThunk<
|
||||
DeviceId | undefined,
|
||||
DeviceId | undefined,
|
||||
{ state: ReduxRootState }
|
||||
>(DeviceManagerEvent.SetActiveDevice, async (payload) => {
|
||||
>(DeviceManagerEvent.SetActiveDevice, async (payload, { dispatch }) => {
|
||||
await setActiveDeviceRequest(payload)
|
||||
dispatch(cleanBackupProcess())
|
||||
return payload
|
||||
})
|
||||
|
||||
@@ -12,11 +12,18 @@ import { Result, ResultObject } from "Core/core/builder"
|
||||
import { deleteFiles } from "Core/files-manager/actions/delete-files.action"
|
||||
import { AppError } from "Core/core/errors"
|
||||
import { testError } from "Core/__deprecated__/renderer/store/constants"
|
||||
import { fulfilledAction, pendingAction } from "Core/__deprecated__/renderer/store"
|
||||
import {
|
||||
fulfilledAction,
|
||||
pendingAction,
|
||||
} from "Core/__deprecated__/renderer/store"
|
||||
import * as loadStorageInfoActionModule from "Core/device/actions/load-storage-info.action"
|
||||
import * as loadDeviceDataActionModule from "Core/device/actions/load-device-data.action"
|
||||
import { DeviceEvent } from "Core/device"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/files-manager/requests/delete-files.request")
|
||||
jest
|
||||
.spyOn(loadStorageInfoActionModule, "loadStorageInfoAction")
|
||||
@@ -26,14 +33,12 @@ jest
|
||||
type: pendingAction(DeviceEvent.LoadStorageInfo),
|
||||
} as unknown as jest.Mock)
|
||||
)
|
||||
jest
|
||||
.spyOn(loadDeviceDataActionModule, "loadDeviceData")
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
type: fulfilledAction(DeviceEvent.LoadDeviceData),
|
||||
} as unknown as jest.Mock)
|
||||
)
|
||||
jest.spyOn(loadDeviceDataActionModule, "loadDeviceData").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
type: fulfilledAction(DeviceEvent.LoadDeviceData),
|
||||
} as unknown as jest.Mock)
|
||||
)
|
||||
const filePaths = [
|
||||
"user/music/example_file_name.mp3",
|
||||
"user/music/second_example_file_name.wav",
|
||||
|
||||
@@ -18,16 +18,18 @@ import * as loadDeviceDataActionModule from "Core/device/actions/load-device-dat
|
||||
import { fulfilledAction } from "Core/__deprecated__/renderer/store"
|
||||
import { DeviceEvent } from "Core/device"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/files-manager/requests/get-files.request")
|
||||
|
||||
jest
|
||||
.spyOn(loadDeviceDataActionModule, "loadDeviceData")
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
type: fulfilledAction(DeviceEvent.LoadDeviceData),
|
||||
} as unknown as jest.Mock)
|
||||
)
|
||||
jest.spyOn(loadDeviceDataActionModule, "loadDeviceData").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
type: fulfilledAction(DeviceEvent.LoadDeviceData),
|
||||
} as unknown as jest.Mock)
|
||||
)
|
||||
|
||||
const successObjectResult: ResultObject<File[]> = Result.success([
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ import { HelpComponentTestIds } from "Core/help/components/help.enum"
|
||||
import Help from "Core/help/components/help.component"
|
||||
import { data } from "Core/__deprecated__/seeds/help"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
dialog: {
|
||||
showOpenDialog: jest.fn(),
|
||||
|
||||
@@ -16,6 +16,10 @@ import { Provider } from "react-redux"
|
||||
import { CheckForUpdateState } from "Core/update/constants/check-for-update-state.constant"
|
||||
import { CaseColour } from "Core/device"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/feature-flags")
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
|
||||
@@ -20,6 +20,10 @@ import { CheckForUpdateState } from "Core/update/constants/check-for-update-stat
|
||||
|
||||
// TODO [mw] add integration tests for update process - scope of the next PR (after all the changes from CP-1681 are done)
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/feature-flags")
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
|
||||
@@ -23,6 +23,10 @@ import { CheckForUpdateState } from "Core/update/constants/check-for-update-stat
|
||||
|
||||
type Props = ComponentProps<typeof Overview>
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
Menu: () => ({
|
||||
popup: jest.fn(),
|
||||
|
||||
@@ -15,6 +15,10 @@ import { initialState as update } from "Core/update/reducers"
|
||||
import { OsRelease } from "Core/update/dto"
|
||||
import { OsReleaseType, Product } from "Core/update/constants"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
const defaultProps: SystemUpdateTextProps = {
|
||||
checkForUpdateFailed: false,
|
||||
checkForUpdateInProgress: false,
|
||||
|
||||
@@ -22,6 +22,10 @@ import { renderWithThemeAndIntl } from "Core/__deprecated__/renderer/utils/rende
|
||||
import React from "react"
|
||||
import { CheckForUpdateState } from "Core/update/constants/check-for-update-state.constant"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("@electron/remote", () => ({
|
||||
Menu: () => ({
|
||||
popup: jest.fn(),
|
||||
|
||||
@@ -15,6 +15,10 @@ import { flags } from "Core/feature-flags"
|
||||
import store from "Core/__deprecated__/renderer/store"
|
||||
import { Provider } from "react-redux"
|
||||
|
||||
jest.mock("Core/settings/store/schemas/generate-application-id", () => ({
|
||||
generateApplicationId: () => "123",
|
||||
}))
|
||||
|
||||
jest.mock("Core/feature-flags")
|
||||
jest.mock("electron-better-ipc", () => {
|
||||
return {
|
||||
|
||||
28
libs/core/settings/store/schemas/generate-application-id.ts
Normal file
28
libs/core/settings/store/schemas/generate-application-id.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import getMAC from "getmac"
|
||||
|
||||
export const generateApplicationId = (): string | null => {
|
||||
const maxApplicationIdLength = 16
|
||||
|
||||
const mac = getMACOrNull()
|
||||
if (mac !== null) {
|
||||
const uniqueValue = mac.replace(/:/g, "").slice(-maxApplicationIdLength)
|
||||
const padLength = maxApplicationIdLength - uniqueValue.length
|
||||
const pad = Math.random().toString(16).slice(-padLength)
|
||||
return `${pad}${uniqueValue}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getMACOrNull = (): string | null => {
|
||||
try {
|
||||
return getMAC()
|
||||
} catch (ex) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -5,33 +5,11 @@
|
||||
|
||||
import path from "path"
|
||||
import { Schema } from "electron-store"
|
||||
import getMAC from "getmac"
|
||||
import getAppPath from "Core/__deprecated__/main/utils/get-app-path"
|
||||
import { Settings } from "Core/settings/dto"
|
||||
import { ConversionFormat, Convert } from "Core/settings/constants"
|
||||
import translationConfig from "App/translations.config.json"
|
||||
|
||||
const generateApplicationId = (): string | null => {
|
||||
const maxApplicationIdLength = 16
|
||||
|
||||
const mac = getMACOrNull()
|
||||
if (mac !== null) {
|
||||
const uniqueValue = mac.replace(/:/g, "").slice(-maxApplicationIdLength)
|
||||
const padLength = maxApplicationIdLength - uniqueValue.length
|
||||
const pad = Math.random().toString(16).slice(-padLength)
|
||||
return `${pad}${uniqueValue}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getMACOrNull = (): string | null => {
|
||||
try {
|
||||
return getMAC()
|
||||
} catch (ex) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
import { generateApplicationId } from "Core/settings/store/schemas/generate-application-id"
|
||||
|
||||
export const settingsSchema: Schema<Settings> = {
|
||||
applicationId: {
|
||||
|
||||
@@ -10,3 +10,6 @@ export * from "./lib/api-features"
|
||||
export * from "./lib/outbox"
|
||||
export * from "./lib/menu"
|
||||
export * from "./lib/server"
|
||||
export * from "./lib/file-transfer"
|
||||
export * from "./lib/file-manager"
|
||||
export * from "./lib/backup"
|
||||
|
||||
@@ -7,9 +7,14 @@ import { DeviceManager } from "Core/device-manager/services"
|
||||
import { APIConfigService } from "./api-config/api-config.service"
|
||||
import { APIFeaturesService } from "./api-features/api-features.service"
|
||||
import { APIBackupService } from "./backup"
|
||||
import { FileManager } from "./file-manager"
|
||||
import { APIMenuService } from "./menu"
|
||||
import { APIOutboxService } from "./outbox/outbox.service"
|
||||
import { ServerService } from "./server/server.service"
|
||||
import { APIFileTransferService } from "./file-transfer"
|
||||
import { ServiceBridge } from "./service-bridge"
|
||||
import { SystemUtilsModule } from "system-utils/feature"
|
||||
import { createSettingsService } from "Core/settings/containers/settings.container"
|
||||
|
||||
export class APIModule {
|
||||
private apiConfigService: APIConfigService
|
||||
@@ -18,14 +23,26 @@ export class APIModule {
|
||||
private apiMenuService: APIMenuService
|
||||
private serverService: ServerService
|
||||
private backupService: APIBackupService
|
||||
private fileTransferService: APIFileTransferService
|
||||
private fileManager: FileManager
|
||||
private serviceBridge: ServiceBridge
|
||||
|
||||
constructor(deviceManager: DeviceManager) {
|
||||
constructor(
|
||||
deviceManager: DeviceManager,
|
||||
systemUtilsModule: SystemUtilsModule
|
||||
) {
|
||||
this.serviceBridge = new ServiceBridge()
|
||||
this.apiConfigService = new APIConfigService(deviceManager)
|
||||
this.apiFeaturesService = new APIFeaturesService(deviceManager)
|
||||
this.apiOutboxService = new APIOutboxService(deviceManager)
|
||||
this.apiMenuService = new APIMenuService(deviceManager)
|
||||
this.serverService = new ServerService()
|
||||
this.backupService = new APIBackupService(deviceManager)
|
||||
this.fileTransferService = new APIFileTransferService(deviceManager)
|
||||
this.fileManager = new FileManager(deviceManager, this.serviceBridge)
|
||||
this.serviceBridge.systemUtilsModule = systemUtilsModule
|
||||
this.serviceBridge.fileTransfer = this.fileTransferService
|
||||
this.serviceBridge.settingsService = createSettingsService()
|
||||
}
|
||||
|
||||
public getAPIServices() {
|
||||
@@ -36,6 +53,8 @@ export class APIModule {
|
||||
this.apiMenuService,
|
||||
this.serverService,
|
||||
this.backupService,
|
||||
this.fileTransferService,
|
||||
this.fileManager,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export class APIBackupService {
|
||||
}
|
||||
|
||||
private parsePreBackupResponse(
|
||||
response: ResultObject<ApiResponse<unknown>, string, unknown>,
|
||||
response: ResultObject<ApiResponse<unknown>, string | number, unknown>,
|
||||
features: string[]
|
||||
) {
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ipcRenderer } from "electron-better-ipc"
|
||||
import { APIBackupServiceEvents, PreBackup } from "device/models"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
|
||||
export const getStartPreBackupRequest = (
|
||||
export const startPreBackupRequest = (
|
||||
features: string[],
|
||||
deviceId?: DeviceId
|
||||
): Promise<ResultObject<PreBackup>> => {
|
||||
|
||||
177
libs/device/feature/src/lib/file-manager/file-manager.service.ts
Normal file
177
libs/device/feature/src/lib/file-manager/file-manager.service.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { Result, ResultObject } from "Core/core/builder"
|
||||
import { IpcEvent } from "Core/core/decorators"
|
||||
import { ServiceBridge } from "../service-bridge"
|
||||
import { FileManagerServiceEvents, GeneralError } from "device/models"
|
||||
import { AppError } from "Core/core/errors"
|
||||
import { DeviceManager } from "Core/device-manager/services"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import packageInfo from "../../../../../../apps/mudita-center/package.json"
|
||||
import { writeFileSync, writeJSONSync, mkdirSync, readdirSync } from "fs-extra"
|
||||
import AES from "crypto-js/aes"
|
||||
import path from "path"
|
||||
|
||||
export class FileManager {
|
||||
constructor(
|
||||
private deviceManager: DeviceManager,
|
||||
private serviceBridge: ServiceBridge
|
||||
) {}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.SaveFile)
|
||||
public saveFileByTransferId({
|
||||
filePath,
|
||||
transferId,
|
||||
}: {
|
||||
filePath: string
|
||||
transferId: number
|
||||
}): ResultObject<undefined> {
|
||||
try {
|
||||
const file =
|
||||
this.serviceBridge.fileTransfer.getFileByTransferId(transferId)
|
||||
writeFileSync(filePath, file.chunks.join(), "base64")
|
||||
|
||||
return Result.success(undefined)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return Result.failed(new AppError(GeneralError.IncorrectResponse))
|
||||
}
|
||||
}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.GetBackupPath)
|
||||
public getBackupPath({ deviceId }: { deviceId?: DeviceId }) {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
|
||||
const vendorId = device.portInfo.vendorId ?? "unknown"
|
||||
const productId = device.portInfo.productId ?? "unknown"
|
||||
|
||||
const { osBackupLocation } =
|
||||
this.serviceBridge.settingsService.getSettings()
|
||||
const backupLocation = path.join(
|
||||
osBackupLocation,
|
||||
`${vendorId}-${productId}`
|
||||
)
|
||||
return Result.success(backupLocation)
|
||||
}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.OpenBackupDirectory)
|
||||
public openBackupDirectory({ deviceId }: { deviceId?: DeviceId }) {
|
||||
const backupDirectory = this.getBackupPath({ deviceId })
|
||||
if (!backupDirectory.ok) {
|
||||
return Result.failed(new AppError(GeneralError.InternalError, ""))
|
||||
}
|
||||
return this.serviceBridge.systemUtilsModule.directory.open({
|
||||
path: backupDirectory.data,
|
||||
})
|
||||
}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.SaveBackupFile)
|
||||
public saveBackupFile({
|
||||
deviceId,
|
||||
featureToTransferId,
|
||||
password,
|
||||
}: {
|
||||
deviceId?: DeviceId
|
||||
featureToTransferId: Record<string, number>
|
||||
password?: string
|
||||
}) {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
try {
|
||||
const vendorId = device.portInfo.vendorId ?? ""
|
||||
const productId = device.portInfo.productId ?? ""
|
||||
const serialNumber = device.portInfo.serialNumber ?? ""
|
||||
const timestamp = new Date().getTime()
|
||||
const appVersion = packageInfo.version
|
||||
const backupDirectory = this.getBackupPath({ deviceId })
|
||||
|
||||
if (!backupDirectory.ok) {
|
||||
return Result.failed(new AppError(GeneralError.InternalError, ""))
|
||||
}
|
||||
|
||||
const backupFilePath = path.join(
|
||||
backupDirectory.data,
|
||||
`${timestamp}_${serialNumber}.mcbackup`
|
||||
)
|
||||
|
||||
const data = Object.entries(featureToTransferId).reduce(
|
||||
(acc, [feature, transferId]) => {
|
||||
const transfer =
|
||||
this.serviceBridge.fileTransfer.getFileByTransferId(transferId)
|
||||
|
||||
const featureData = password
|
||||
? AES.encrypt(transfer.chunks.join(), password).toString()
|
||||
: transfer.chunks.join()
|
||||
|
||||
return { ...acc, [feature]: featureData }
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const fileToSave = {
|
||||
header: {
|
||||
vendorId,
|
||||
productId,
|
||||
serialNumber,
|
||||
appVersion,
|
||||
...(password && { crypto: "AES" }),
|
||||
},
|
||||
data,
|
||||
}
|
||||
|
||||
mkdirSync(backupDirectory.data, { recursive: true })
|
||||
|
||||
writeJSONSync(backupFilePath, fileToSave)
|
||||
|
||||
return Result.success(undefined)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return Result.failed(new AppError(GeneralError.InternalError))
|
||||
}
|
||||
}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.ReadDirectory)
|
||||
public readDirectory({ path }: { path: string }): ResultObject<string[]> {
|
||||
try {
|
||||
const result = readdirSync(path)
|
||||
|
||||
return Result.success(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return Result.failed(new AppError(GeneralError.InternalError))
|
||||
}
|
||||
}
|
||||
|
||||
@IpcEvent(FileManagerServiceEvents.ReadBackupDirectory)
|
||||
public readBackupDirectory({
|
||||
deviceId,
|
||||
}: {
|
||||
deviceId?: DeviceId
|
||||
}): ResultObject<string[]> {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
const pathResult = this.getBackupPath({ deviceId })
|
||||
|
||||
if (!pathResult.ok) {
|
||||
return Result.failed(new AppError(GeneralError.InternalError))
|
||||
}
|
||||
|
||||
return this.readDirectory({ path: pathResult.data })
|
||||
}
|
||||
}
|
||||
11
libs/device/feature/src/lib/file-manager/index.ts
Normal file
11
libs/device/feature/src/lib/file-manager/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export * from "./file-manager.service"
|
||||
export * from "./save-file.request"
|
||||
export * from "./save-backup-file.request"
|
||||
export * from "./open-backup-directory.request"
|
||||
export * from "./read-directory.request"
|
||||
export * from "./read-backup-directory.request"
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { FileManagerServiceEvents } from "device/models"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const openBackupDirectoryRequest = (
|
||||
deviceId?: DeviceId
|
||||
): Promise<ResultObject<string>> => {
|
||||
return ipcRenderer.callMain(FileManagerServiceEvents.OpenBackupDirectory, {
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { FileManagerServiceEvents } from "device/models"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const readBackupDirectoryRequest = (
|
||||
deviceId: DeviceId
|
||||
): Promise<ResultObject<string[]>> => {
|
||||
return ipcRenderer.callMain(FileManagerServiceEvents.ReadBackupDirectory, {
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { FileManagerServiceEvents } from "device/models"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const readDirectoryRequest = (
|
||||
path: string
|
||||
): Promise<ResultObject<string[]>> => {
|
||||
return ipcRenderer.callMain(FileManagerServiceEvents.ReadDirectory, {
|
||||
path,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { FileManagerServiceEvents } from "device/models"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const saveBackupFileRequest = (
|
||||
featureToTransferId: Record<string, number>,
|
||||
deviceId?: DeviceId,
|
||||
password?: string
|
||||
): Promise<ResultObject<undefined>> => {
|
||||
return ipcRenderer.callMain(FileManagerServiceEvents.SaveBackupFile, {
|
||||
featureToTransferId,
|
||||
deviceId,
|
||||
password,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { FileManagerServiceEvents } from "device/models"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const saveFileRequest = (
|
||||
filePath: string,
|
||||
transferId: number
|
||||
): Promise<ResultObject<undefined>> => {
|
||||
return ipcRenderer.callMain(FileManagerServiceEvents.SaveFile, {
|
||||
filePath,
|
||||
transferId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { Result, ResultObject } from "Core/core/builder"
|
||||
import { IpcEvent } from "Core/core/decorators"
|
||||
import { AppError, AppErrorType } from "Core/core/errors"
|
||||
import { DeviceManager } from "Core/device-manager/services"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import {
|
||||
ApiFileTransferError,
|
||||
ApiFileTransferServiceEvents,
|
||||
FileTransferStatuses,
|
||||
GeneralError,
|
||||
PreTransferGet,
|
||||
PreTransferGetValidator,
|
||||
PreTransferSendValidator,
|
||||
TransferGetValidator,
|
||||
TransferSend,
|
||||
TransferSendValidator,
|
||||
} from "device/models"
|
||||
import { readFileSync } from "fs-extra"
|
||||
import crc from "js-crc"
|
||||
|
||||
interface Transfer {
|
||||
crc32: string
|
||||
fileSize: number
|
||||
filePath: string
|
||||
chunks: string[]
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_REPEATS = 2
|
||||
|
||||
export class APIFileTransferService {
|
||||
constructor(
|
||||
private deviceManager: DeviceManager,
|
||||
private transfers: Record<string, Transfer> = {}
|
||||
) {}
|
||||
|
||||
private prepareFile(path: string) {
|
||||
const file = readFileSync(path, {
|
||||
encoding: "base64",
|
||||
})
|
||||
return {
|
||||
file,
|
||||
crc32: crc.crc32(file),
|
||||
}
|
||||
}
|
||||
|
||||
public getFileByTransferId(transferId: number) {
|
||||
return this.transfers[transferId]
|
||||
}
|
||||
|
||||
// Sending files to device
|
||||
@IpcEvent(ApiFileTransferServiceEvents.PreSend)
|
||||
public async preTransferSend({
|
||||
filePath,
|
||||
targetPath,
|
||||
deviceId,
|
||||
}: {
|
||||
filePath: string
|
||||
targetPath: string
|
||||
deviceId?: DeviceId
|
||||
}): Promise<
|
||||
ResultObject<{
|
||||
transferId: number
|
||||
chunksCount: number
|
||||
}>
|
||||
> {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
const { crc32, file } = this.prepareFile(filePath)
|
||||
|
||||
const response = await device.request({
|
||||
endpoint: "PRE_FILE_TRANSFER",
|
||||
method: "POST",
|
||||
body: {
|
||||
filePath: targetPath,
|
||||
fileSize: file.length,
|
||||
crc32,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const preTransferResponse = PreTransferSendValidator.safeParse(
|
||||
response.data.body
|
||||
)
|
||||
|
||||
const success = preTransferResponse.success
|
||||
|
||||
if (!success) {
|
||||
return handleError(response.data.status)
|
||||
}
|
||||
|
||||
this.transfers[preTransferResponse.data.transferId] = {
|
||||
crc32,
|
||||
fileSize: file.length,
|
||||
filePath,
|
||||
chunks:
|
||||
file.match(
|
||||
new RegExp(`.{1,${preTransferResponse.data.chunkSize}}`, "g")
|
||||
) || [],
|
||||
}
|
||||
|
||||
return Result.success({
|
||||
transferId: preTransferResponse.data.transferId,
|
||||
chunksCount:
|
||||
this.transfers[preTransferResponse.data.transferId].chunks.length,
|
||||
})
|
||||
}
|
||||
|
||||
return handleError(response.error.type)
|
||||
}
|
||||
|
||||
@IpcEvent(ApiFileTransferServiceEvents.Send)
|
||||
public async transferSend({
|
||||
transferId,
|
||||
chunkNumber,
|
||||
deviceId,
|
||||
repeats = 0,
|
||||
maxRepeats = DEFAULT_MAX_REPEATS,
|
||||
}: {
|
||||
transferId: number
|
||||
chunkNumber: number
|
||||
deviceId?: DeviceId
|
||||
repeats: number
|
||||
maxRepeats: number
|
||||
}): Promise<ResultObject<TransferSend>> {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
|
||||
const data = this.transfers[transferId].chunks[chunkNumber - 1]
|
||||
|
||||
const response = await device.request({
|
||||
endpoint: "FILE_TRANSFER",
|
||||
method: "POST",
|
||||
body: {
|
||||
transferId,
|
||||
chunkNumber,
|
||||
data,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (repeats < maxRepeats) {
|
||||
return this.transferSend({
|
||||
deviceId,
|
||||
transferId,
|
||||
chunkNumber,
|
||||
repeats: repeats + 1,
|
||||
maxRepeats,
|
||||
})
|
||||
} else {
|
||||
return handleError(response.error.type)
|
||||
}
|
||||
}
|
||||
|
||||
const transferResponse = TransferSendValidator.safeParse(response.data.body)
|
||||
|
||||
const success =
|
||||
transferResponse.success &&
|
||||
[
|
||||
FileTransferStatuses.WholeFileTransferred,
|
||||
FileTransferStatuses.FileChunkTransferred,
|
||||
].includes(response.data.status as number)
|
||||
|
||||
if (!success) {
|
||||
return handleError(response.data.status)
|
||||
}
|
||||
|
||||
return Result.success(transferResponse.data)
|
||||
}
|
||||
|
||||
private validateChecksum(transferId: number) {
|
||||
const transfer = this.transfers[transferId]
|
||||
const data = transfer.chunks.join("")
|
||||
const crc32 = crc.crc32(data)
|
||||
return crc32.toLowerCase() === transfer.crc32.toLowerCase()
|
||||
}
|
||||
|
||||
@IpcEvent(ApiFileTransferServiceEvents.PreGet)
|
||||
public async preTransferGet({
|
||||
filePath,
|
||||
deviceId,
|
||||
}: {
|
||||
filePath: string
|
||||
deviceId?: DeviceId
|
||||
}): Promise<ResultObject<PreTransferGet>> {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
|
||||
const response = await device.request({
|
||||
endpoint: "PRE_FILE_TRANSFER",
|
||||
method: "GET",
|
||||
body: {
|
||||
filePath,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const preTransferResponse = PreTransferGetValidator.safeParse(
|
||||
response.data.body
|
||||
)
|
||||
|
||||
const success = preTransferResponse.success
|
||||
|
||||
if (!success) {
|
||||
return handleError(response.data.status)
|
||||
}
|
||||
|
||||
this.transfers[preTransferResponse.data.transferId] = {
|
||||
crc32: preTransferResponse.data.crc32,
|
||||
fileSize: preTransferResponse.data.fileSize,
|
||||
filePath,
|
||||
chunks: [],
|
||||
}
|
||||
|
||||
return Result.success(preTransferResponse.data)
|
||||
}
|
||||
|
||||
return handleError(response.error.type)
|
||||
}
|
||||
|
||||
@IpcEvent(ApiFileTransferServiceEvents.Get)
|
||||
public async transferGet({
|
||||
deviceId,
|
||||
transferId,
|
||||
chunkNumber,
|
||||
repeats = 0,
|
||||
maxRepeats = DEFAULT_MAX_REPEATS,
|
||||
}: {
|
||||
deviceId?: DeviceId
|
||||
transferId: number
|
||||
chunkNumber: number
|
||||
repeats: number
|
||||
maxRepeats: number
|
||||
}): Promise<ResultObject<undefined>> {
|
||||
const device = deviceId
|
||||
? this.deviceManager.getAPIDeviceById(deviceId)
|
||||
: this.deviceManager.apiDevice
|
||||
|
||||
if (!device) {
|
||||
return Result.failed(new AppError(GeneralError.NoDevice, ""))
|
||||
}
|
||||
|
||||
const response = await device.request({
|
||||
endpoint: "FILE_TRANSFER",
|
||||
method: "GET",
|
||||
body: {
|
||||
transferId,
|
||||
chunkNumber,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (repeats < maxRepeats) {
|
||||
return this.transferGet({
|
||||
deviceId,
|
||||
transferId,
|
||||
chunkNumber,
|
||||
repeats: repeats + 1,
|
||||
maxRepeats,
|
||||
})
|
||||
} else {
|
||||
return handleError(response.error.type)
|
||||
}
|
||||
}
|
||||
|
||||
const transferResponse = TransferGetValidator.safeParse(response.data.body)
|
||||
|
||||
const success =
|
||||
transferResponse.success &&
|
||||
[
|
||||
FileTransferStatuses.WholeFileTransferred,
|
||||
FileTransferStatuses.FileChunkTransferred,
|
||||
].includes(response.data.status as number)
|
||||
|
||||
if (!success) {
|
||||
return handleError(response.data.status)
|
||||
}
|
||||
|
||||
this.transfers[transferId].chunks[chunkNumber - 1] =
|
||||
transferResponse.data.data
|
||||
|
||||
if (
|
||||
(response.data.status as number) ===
|
||||
FileTransferStatuses.WholeFileTransferred
|
||||
) {
|
||||
if (this.validateChecksum(transferId)) {
|
||||
return Result.success(undefined)
|
||||
} else {
|
||||
return Result.failed(
|
||||
new AppError(
|
||||
ApiFileTransferError.CRCMismatch,
|
||||
ApiFileTransferError[ApiFileTransferError.CRCMismatch]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return Result.success(undefined)
|
||||
}
|
||||
|
||||
@IpcEvent(ApiFileTransferServiceEvents.Clear)
|
||||
public transferClear({ transferId }: { transferId: number }) {
|
||||
delete this.transfers[transferId]
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (responseStatus: AppErrorType) => {
|
||||
if (ApiFileTransferError[responseStatus as ApiFileTransferError]) {
|
||||
return Result.failed<
|
||||
{ transferId?: number; filePath: string },
|
||||
AppErrorType
|
||||
>(
|
||||
new AppError(
|
||||
responseStatus,
|
||||
ApiFileTransferError[responseStatus as ApiFileTransferError]
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Result.failed(new AppError(GeneralError.IncorrectResponse, ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { ApiFileTransferServiceEvents } from "device/models"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
|
||||
export const getFileRequest = (
|
||||
transferId: number,
|
||||
chunkNumber: number,
|
||||
deviceId?: DeviceId
|
||||
): Promise<ResultObject<undefined>> => {
|
||||
return ipcRenderer.callMain(ApiFileTransferServiceEvents.Get, {
|
||||
transferId,
|
||||
chunkNumber,
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
11
libs/device/feature/src/lib/file-transfer/index.ts
Normal file
11
libs/device/feature/src/lib/file-transfer/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export * from "./file-transfer.service"
|
||||
export * from "./start-pre-send-file.request"
|
||||
export * from "./send-file.request"
|
||||
export * from "./send-clear.request"
|
||||
export * from "./start-pre-get-file.request"
|
||||
export * from "./get-file.request"
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { ApiFileTransferServiceEvents } from "device/models"
|
||||
|
||||
export const sendClearRequest = (transferId: number): Promise<void> => {
|
||||
return ipcRenderer.callMain(ApiFileTransferServiceEvents.Clear, {
|
||||
transferId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { ApiFileTransferServiceEvents, TransferSend } from "device/models"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
|
||||
export const sendFileRequest = (
|
||||
transferId: number,
|
||||
chunkNumber: number,
|
||||
deviceId?: DeviceId
|
||||
): Promise<ResultObject<TransferSend>> => {
|
||||
return ipcRenderer.callMain(ApiFileTransferServiceEvents.Send, {
|
||||
transferId,
|
||||
chunkNumber,
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { ApiFileTransferServiceEvents, PreTransferGet } from "device/models"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
|
||||
export const startPreGetFileRequest = (
|
||||
filePath: string,
|
||||
deviceId?: DeviceId
|
||||
): Promise<ResultObject<PreTransferGet>> => {
|
||||
return ipcRenderer.callMain(ApiFileTransferServiceEvents.PreGet, {
|
||||
filePath,
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { ApiFileTransferServiceEvents } from "device/models"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
|
||||
export const startPreSendFileRequest = (
|
||||
filePath: string,
|
||||
targetPath: string,
|
||||
deviceId?: DeviceId
|
||||
): Promise<
|
||||
ResultObject<{
|
||||
transferId: number
|
||||
chunksCount: number
|
||||
}>
|
||||
> => {
|
||||
return ipcRenderer.callMain(ApiFileTransferServiceEvents.PreSend, {
|
||||
filePath,
|
||||
targetPath,
|
||||
deviceId,
|
||||
})
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export class APIRestoreService {
|
||||
}
|
||||
|
||||
private parseRestoreResponse(
|
||||
response: ResultObject<ApiResponse<unknown>, string, unknown>
|
||||
response: ResultObject<ApiResponse<unknown>, string | number, unknown>
|
||||
) {
|
||||
if (!response.ok) {
|
||||
return Result.failed(response.error)
|
||||
|
||||
6
libs/device/feature/src/lib/service-bridge/index.ts
Normal file
6
libs/device/feature/src/lib/service-bridge/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export * from "./service-bridge"
|
||||
55
libs/device/feature/src/lib/service-bridge/service-bridge.ts
Normal file
55
libs/device/feature/src/lib/service-bridge/service-bridge.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { SettingsService } from "Core/settings/services"
|
||||
import { APIFileTransferService } from "../file-transfer"
|
||||
import { SystemUtilsModule } from "system-utils/feature"
|
||||
|
||||
export class ServiceBridge {
|
||||
private _fileTransfer?: APIFileTransferService
|
||||
|
||||
get fileTransfer(): APIFileTransferService {
|
||||
if (!this._fileTransfer) {
|
||||
throw new Error(
|
||||
"APIFileTransferService reference has not been set in ServiceBridge"
|
||||
)
|
||||
}
|
||||
return this._fileTransfer
|
||||
}
|
||||
set fileTransfer(value: APIFileTransferService) {
|
||||
this._fileTransfer = value
|
||||
}
|
||||
|
||||
private _settingsService?: SettingsService
|
||||
|
||||
get settingsService(): SettingsService {
|
||||
if (!this._settingsService) {
|
||||
throw new Error(
|
||||
"SettingsService reference has not been set in ServiceBridge"
|
||||
)
|
||||
}
|
||||
return this._settingsService
|
||||
}
|
||||
set settingsService(value: SettingsService) {
|
||||
this._settingsService = value
|
||||
}
|
||||
|
||||
private _systemUtilsModule?: SystemUtilsModule
|
||||
|
||||
get systemUtilsModule(): SystemUtilsModule {
|
||||
if (!this._systemUtilsModule) {
|
||||
throw new Error(
|
||||
"SystemUtilsService reference has not been set in ServiceBridge"
|
||||
)
|
||||
}
|
||||
return this._systemUtilsModule
|
||||
}
|
||||
|
||||
set systemUtilsModule(value: SystemUtilsModule) {
|
||||
this._systemUtilsModule = value
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
|
||||
@@ -13,4 +13,5 @@ export * from "./lib/api-config"
|
||||
export * from "./lib/server"
|
||||
export * from "./lib/general-error"
|
||||
export * from "./lib/backup"
|
||||
export * from "./lib/file-transfer"
|
||||
export * from "./lib/restore"
|
||||
|
||||
@@ -12,6 +12,8 @@ const APIEndpoints = {
|
||||
Outbox: "OUTBOX",
|
||||
PreBackup: "PRE_BACKUP",
|
||||
PostBackup: "POST_BACKUP",
|
||||
PreFileTransfer: "PRE_FILE_TRANSFER",
|
||||
FileTransfer: "FILE_TRANSFER",
|
||||
PreRestore: "PRE_RESTORE",
|
||||
Restore: "RESTORE",
|
||||
} as const
|
||||
@@ -36,6 +38,8 @@ const APIRequests = {
|
||||
OUTBOX: [APIMethods.GET],
|
||||
PRE_BACKUP: [APIMethods.POST, APIMethods.GET],
|
||||
POST_BACKUP: [APIMethods.POST],
|
||||
PRE_FILE_TRANSFER: [APIMethods.POST, APIMethods.GET],
|
||||
FILE_TRANSFER: [APIMethods.POST, APIMethods.GET],
|
||||
PRE_RESTORE: [APIMethods.POST],
|
||||
RESTORE: [APIMethods.POST, APIMethods.GET],
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export enum FileTransferStatuses {
|
||||
WholeFileTransferred = 200,
|
||||
FileChunkTransferred = 206,
|
||||
}
|
||||
33
libs/device/models/src/lib/file-transfer/file-transfer.ts
Normal file
33
libs/device/models/src/lib/file-transfer/file-transfer.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
export const PreTransferSendValidator = z.object({
|
||||
transferId: z.number(),
|
||||
chunkSize: z.number().positive(),
|
||||
})
|
||||
|
||||
export const TransferSendValidator = z.object({
|
||||
transferId: z.number(),
|
||||
chunkNumber: z.number().positive(),
|
||||
})
|
||||
|
||||
export type TransferSend = z.infer<typeof TransferSendValidator>
|
||||
|
||||
export const PreTransferGetValidator = z.object({
|
||||
transferId: z.number(),
|
||||
chunkSize: z.number().positive(),
|
||||
fileSize: z.number().positive(),
|
||||
crc32: z.string(),
|
||||
})
|
||||
|
||||
export type PreTransferGet = z.infer<typeof PreTransferGetValidator>
|
||||
|
||||
export const TransferGetValidator = z.object({
|
||||
transferId: z.number(),
|
||||
chunkNumber: z.number().positive(),
|
||||
data: z.string().min(1),
|
||||
})
|
||||
7
libs/device/models/src/lib/file-transfer/index.ts
Normal file
7
libs/device/models/src/lib/file-transfer/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export * from "./file-transfer"
|
||||
export * from "./file-transfer-statuses"
|
||||
@@ -6,8 +6,18 @@
|
||||
export enum GeneralError {
|
||||
NoDevice = "no-device",
|
||||
IncorrectResponse = "incorrect-response",
|
||||
InternalError = "internal-error",
|
||||
}
|
||||
|
||||
export enum ApiError {
|
||||
DeviceLocked = 423,
|
||||
}
|
||||
|
||||
export enum ApiFileTransferError {
|
||||
AccessRestricted = 403,
|
||||
IncorrectPath = 404,
|
||||
FileAlreadyExists = 409,
|
||||
CRCMismatch = 422,
|
||||
Unknown = 500,
|
||||
NotEnoughSpace = 507,
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ export enum APIBackupServiceEvents {
|
||||
StartPreBackup = "apiservice_backup-start-pre-backup",
|
||||
CheckPreBackup = "apiservice_backup-check-pre-backup",
|
||||
PostBackup = "apiservice_backup-post-backup",
|
||||
GetBackupList = "apiservice_backup-get-backup-list",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export enum ApiFileTransferServiceEvents {
|
||||
PreGet = "apiservice_file_transfer-pre-transfer-get",
|
||||
Get = "apiservice_file_transfer-transfer-get",
|
||||
PreSend = "apiservice_file_transfer-pre-transfer-send",
|
||||
Send = "apiservice_file_transfer-transfer-send",
|
||||
Clear = "apiservice_file_transfer-transfer-clear",
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
export enum FileManagerServiceEvents {
|
||||
SaveFile = "apiservice_file_manager-save-file",
|
||||
SaveBackupFile = "apiservice_file_manager-save-backup-file",
|
||||
GetBackupPath = "apiservice_file_manager-get-backup-path",
|
||||
OpenBackupDirectory = "apiservice_file_manager-open-directory",
|
||||
ReadDirectory = "apiservice_file_manager-read-directory",
|
||||
ReadBackupDirectory = "apiservice_file_manager-read-backup-directory",
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export * from "./api-config-service-events"
|
||||
export * from "./api-backup-service-events"
|
||||
export * from "./api-restore-service-events"
|
||||
export * from "./server-service-events"
|
||||
export * from "./api-file-transfer-service-events"
|
||||
export * from "./file-manager-service-events"
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
|
||||
export * from "./lib/generic-view"
|
||||
export * from "./lib/recursive-layout"
|
||||
export * from "./lib/api-device-modals"
|
||||
|
||||
40
libs/generic-view/feature/src/lib/api-device-modals.tsx
Normal file
40
libs/generic-view/feature/src/lib/api-device-modals.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from "react"
|
||||
import { GenericThemeProvider } from "generic-view/theme"
|
||||
import { BackupError, ModalBase, ModalCenteredContent } from "generic-view/ui"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import {
|
||||
cleanBackupProcess,
|
||||
selectActiveDevice,
|
||||
selectBackupProcessStatus,
|
||||
} from "generic-view/store"
|
||||
|
||||
export const ApiDeviceModals: FunctionComponent = () => {
|
||||
return (
|
||||
<GenericThemeProvider>
|
||||
<BackupErrorModal />
|
||||
</GenericThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const BackupErrorModal: FunctionComponent = () => {
|
||||
const dispatch = useDispatch()
|
||||
const activeDevice = useSelector(selectActiveDevice)
|
||||
const backupStatus = useSelector(selectBackupProcessStatus)
|
||||
const opened = backupStatus === "FAILED" && !activeDevice
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(cleanBackupProcess())
|
||||
}
|
||||
return (
|
||||
<ModalBase opened={opened} variant={"small"}>
|
||||
<ModalCenteredContent>
|
||||
<BackupError closeAction={{ type: "custom", callback: onClose }} />
|
||||
</ModalCenteredContent>
|
||||
</ModalBase>
|
||||
)
|
||||
}
|
||||
@@ -13,3 +13,9 @@ export * from "./lib/modals/reducer"
|
||||
export * from "./lib/modals/actions"
|
||||
export * from "./lib/backup/reducer"
|
||||
export * from "./lib/backup/actions"
|
||||
export * from "./lib/backup/create-backup.action"
|
||||
export * from "./lib/backup/refresh-backup-list.action"
|
||||
export * from "./lib/file-transfer/reducer"
|
||||
export * from "./lib/file-transfer/actions"
|
||||
export * from "./lib/file-transfer/send-file.action"
|
||||
export * from "./lib/file-transfer/get-file.action"
|
||||
|
||||
@@ -18,5 +18,18 @@ export enum ActionName {
|
||||
CloseAllModals = "generic-modals/close-all-modals",
|
||||
ReplaceModal = "generic-modals/replace-modal",
|
||||
CloseDomainModals = "generic-modals/close-domain-modals",
|
||||
AddBackupFiles = "generic-backups/add-backup-files",
|
||||
SetBackupProcess = "generic-backups/set-backup-process",
|
||||
CleanBackupProcess = "generic-backups/clean-backup-process",
|
||||
BackupProcessStatus = "generic-backups/backup-process-status",
|
||||
SetBackupProcessFileStatus = "generic-backups/set-backup-process-file-status",
|
||||
CreateBackup = "generic-backups/create-backup",
|
||||
RefreshBackupList = "generic-backups/refresh-backup-list",
|
||||
FileTransferSend = "generic-file-transfer/send",
|
||||
PreFileTransferSend = "generic-file-transfer/pre-send",
|
||||
ChunkFileTransferSend = "generic-file-transfer/chunk-sent",
|
||||
ClearFileTransferSendError = "generic-file-transfer/clear-send-errors",
|
||||
PreFileTransferGet = "generic-file-transfer/pre-get",
|
||||
FileTransferGet = "generic-file-transfer/get",
|
||||
ChunkFileTransferGet = "generic-file-transfer/chunk-get",
|
||||
ClearFileTransferGetError = "generic-file-transfer/clear-get-errors",
|
||||
}
|
||||
|
||||
@@ -4,8 +4,24 @@
|
||||
*/
|
||||
|
||||
import { createAction } from "@reduxjs/toolkit"
|
||||
import { Backup } from "./reducer"
|
||||
import {
|
||||
BackupProcess,
|
||||
BackupProcessFileStatus,
|
||||
BackupProcessStatus,
|
||||
} from "./reducer"
|
||||
import { ActionName } from "../action-names"
|
||||
|
||||
export const setBackupFiles = createAction<Backup[]>(ActionName.AddBackupFiles)
|
||||
export const setBackupProcess = createAction<BackupProcess>(
|
||||
ActionName.SetBackupProcess
|
||||
)
|
||||
|
||||
export const cleanBackupProcess = createAction(ActionName.CleanBackupProcess)
|
||||
|
||||
export const setBackupProcessFileStatus = createAction<{
|
||||
feature: string
|
||||
status: BackupProcessFileStatus
|
||||
}>(ActionName.SetBackupProcessFileStatus)
|
||||
|
||||
export const setBackupProcessStatus = createAction<BackupProcessStatus>(
|
||||
ActionName.BackupProcessStatus
|
||||
)
|
||||
|
||||
166
libs/generic-view/store/src/lib/backup/create-backup.action.ts
Normal file
166
libs/generic-view/store/src/lib/backup/create-backup.action.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import {
|
||||
checkPreBackupRequest,
|
||||
postBackupRequest,
|
||||
saveBackupFileRequest,
|
||||
sendClearRequest,
|
||||
startPreBackupRequest,
|
||||
} from "device/feature"
|
||||
|
||||
import { ActionName } from "../action-names"
|
||||
import { getFile } from "../file-transfer/get-file.action"
|
||||
import {
|
||||
setBackupProcess,
|
||||
setBackupProcessFileStatus,
|
||||
setBackupProcessStatus,
|
||||
} from "./actions"
|
||||
import { refreshBackupList } from "./refresh-backup-list.action"
|
||||
|
||||
export const createBackup = createAsyncThunk<
|
||||
undefined,
|
||||
{
|
||||
features: string[]
|
||||
password?: string
|
||||
},
|
||||
{ state: ReduxRootState }
|
||||
>(
|
||||
ActionName.CreateBackup,
|
||||
async (
|
||||
{ features, password },
|
||||
{ getState, dispatch, rejectWithValue, signal }
|
||||
) => {
|
||||
let aborted = false
|
||||
|
||||
const abortListener = async () => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
aborted = true
|
||||
abortFileRequest?.()
|
||||
await clearTransfers?.()
|
||||
if (backupId && deviceId) {
|
||||
await postBackupRequest(backupId, deviceId)
|
||||
}
|
||||
}
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setBackupProcess({
|
||||
status: "PRE_BACKUP",
|
||||
featureFilesTransfer: features.reduce((acc, item) => {
|
||||
return { ...acc, [item]: { done: false } }
|
||||
}, {}),
|
||||
})
|
||||
)
|
||||
|
||||
const deviceId = getState().genericViews.activeDevice
|
||||
|
||||
if (deviceId === undefined || aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
const startPreBackupResponse = await startPreBackupRequest(
|
||||
features,
|
||||
deviceId
|
||||
)
|
||||
|
||||
if (!startPreBackupResponse.ok || aborted) {
|
||||
console.log(startPreBackupResponse.error)
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
const backupId = startPreBackupResponse.data.backupId
|
||||
let backupFeaturesFiles = startPreBackupResponse.data.features
|
||||
let abortFileRequest: VoidFunction
|
||||
const featureToTransferId: Record<string, number> = {}
|
||||
|
||||
const clearTransfers = () => {
|
||||
return Promise.all(
|
||||
Object.values(featureToTransferId).map(async (transferId) => {
|
||||
await sendClearRequest(transferId)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
while (backupFeaturesFiles === undefined) {
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
const checkPreBackupResponse = await checkPreBackupRequest(
|
||||
backupId,
|
||||
features,
|
||||
deviceId
|
||||
)
|
||||
|
||||
if (!checkPreBackupResponse.ok) {
|
||||
console.log(checkPreBackupResponse.error)
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
backupFeaturesFiles = checkPreBackupResponse.data.features
|
||||
}
|
||||
dispatch(setBackupProcessStatus("FILES_TRANSFER"))
|
||||
|
||||
for (let i = 0; i < features.length; ++i) {
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
const feature = features[i]
|
||||
|
||||
dispatch(setBackupProcessFileStatus({ feature, status: "IN_PROGRESS" }))
|
||||
const filePromise = dispatch(
|
||||
getFile({
|
||||
deviceId,
|
||||
filePath: backupFeaturesFiles[feature],
|
||||
targetPath: "",
|
||||
})
|
||||
)
|
||||
abortFileRequest = filePromise.abort
|
||||
const file = await filePromise
|
||||
if (
|
||||
file.meta.requestStatus === "fulfilled" &&
|
||||
file.payload &&
|
||||
"transferId" in file.payload
|
||||
) {
|
||||
featureToTransferId[feature] = file.payload.transferId
|
||||
dispatch(setBackupProcessFileStatus({ feature, status: "DONE" }))
|
||||
} else if (!aborted) {
|
||||
console.log("Error while downloading file")
|
||||
await clearTransfers()
|
||||
await postBackupRequest(backupId, deviceId)
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
}
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
dispatch(setBackupProcessStatus("SAVE_FILE"))
|
||||
const saveBackupFileResponse = await saveBackupFileRequest(
|
||||
featureToTransferId,
|
||||
deviceId,
|
||||
password
|
||||
)
|
||||
|
||||
if (!saveBackupFileResponse.ok) {
|
||||
console.log("Error while saving file")
|
||||
await clearTransfers()
|
||||
await postBackupRequest(backupId, deviceId)
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
dispatch(refreshBackupList())
|
||||
|
||||
if (!aborted) {
|
||||
await postBackupRequest(backupId, deviceId)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
@@ -4,43 +4,91 @@
|
||||
*/
|
||||
|
||||
import { createReducer } from "@reduxjs/toolkit"
|
||||
import { setBackupFiles } from "./actions"
|
||||
import {
|
||||
cleanBackupProcess,
|
||||
setBackupProcess,
|
||||
setBackupProcessFileStatus,
|
||||
setBackupProcessStatus,
|
||||
} from "./actions"
|
||||
import { createBackup } from "./create-backup.action"
|
||||
import { refreshBackupList } from "./refresh-backup-list.action"
|
||||
|
||||
export interface Backup {
|
||||
fileName: string
|
||||
date: Date
|
||||
features: string[]
|
||||
device: {
|
||||
serialNumber: string
|
||||
vendorId: string
|
||||
productId: string
|
||||
osVersion: string
|
||||
}
|
||||
serialNumber: string
|
||||
}
|
||||
|
||||
export type BackupProcessStatus =
|
||||
| "PRE_BACKUP"
|
||||
| "FILES_TRANSFER"
|
||||
| "SAVE_FILE"
|
||||
| "DONE"
|
||||
| "FAILED"
|
||||
|
||||
export type BackupProcessFileStatus = "PENDING" | "IN_PROGRESS" | "DONE"
|
||||
|
||||
export interface BackupProcess {
|
||||
status: "PRE_BACKUP" | "FILES_TRANSFER" | "SAVE_FILE" | "DONE" | "FAILED"
|
||||
featureFilesTransfer: Record<
|
||||
string,
|
||||
{ transferId?: number; status: BackupProcessFileStatus }
|
||||
>
|
||||
}
|
||||
|
||||
interface BackupState {
|
||||
files: Backup[]
|
||||
lastBackupRefresh: number
|
||||
backups: Backup[]
|
||||
backupProcess?: BackupProcess
|
||||
}
|
||||
|
||||
const initialState: BackupState = {
|
||||
// Demo data
|
||||
files: [
|
||||
{
|
||||
fileName: "backup1.json",
|
||||
date: new Date(),
|
||||
features: ["calls", "messages"],
|
||||
device: {
|
||||
serialNumber: "0123456789ABCDEF",
|
||||
vendorId: "0e8d",
|
||||
productId: "2006",
|
||||
osVersion: "12",
|
||||
},
|
||||
},
|
||||
],
|
||||
lastBackupRefresh: 0,
|
||||
backups: [],
|
||||
}
|
||||
|
||||
export const genericBackupsReducer = createReducer(initialState, (builder) => {
|
||||
builder.addCase(setBackupFiles, (state, action) => {
|
||||
state.files = action.payload
|
||||
builder.addCase(cleanBackupProcess, (state, action) => {
|
||||
delete state.backupProcess
|
||||
})
|
||||
builder.addCase(setBackupProcess, (state, action) => {
|
||||
state.backupProcess = action.payload
|
||||
})
|
||||
builder.addCase(setBackupProcessFileStatus, (state, action) => {
|
||||
if (state.backupProcess) {
|
||||
state.backupProcess.featureFilesTransfer[action.payload.feature].status =
|
||||
action.payload.status
|
||||
}
|
||||
})
|
||||
builder.addCase(setBackupProcessStatus, (state, action) => {
|
||||
if (state.backupProcess) {
|
||||
state.backupProcess.status = action.payload
|
||||
}
|
||||
})
|
||||
builder.addCase(createBackup.rejected, (state, action) => {
|
||||
if (state.backupProcess) {
|
||||
state.backupProcess.status = "FAILED"
|
||||
} else {
|
||||
state.backupProcess = {
|
||||
status: "FAILED",
|
||||
featureFilesTransfer: {},
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(createBackup.fulfilled, (state, action) => {
|
||||
if (state.backupProcess) {
|
||||
state.backupProcess.status = "DONE"
|
||||
} else {
|
||||
state.backupProcess = {
|
||||
status: "DONE",
|
||||
featureFilesTransfer: {},
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(refreshBackupList.fulfilled, (state, action) => {
|
||||
if (state.lastBackupRefresh < action.payload.refreshTimestamp) {
|
||||
state.lastBackupRefresh = action.payload.refreshTimestamp
|
||||
state.backups = action.payload.backups
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { readBackupDirectoryRequest } from "device/feature"
|
||||
|
||||
import { ActionName } from "../action-names"
|
||||
import { Backup } from "./reducer"
|
||||
|
||||
export const refreshBackupList = createAsyncThunk<
|
||||
{ refreshTimestamp: number; backups: Backup[]; deviceId: DeviceId },
|
||||
undefined,
|
||||
{ state: ReduxRootState }
|
||||
>(
|
||||
ActionName.RefreshBackupList,
|
||||
async (_, { getState, dispatch, rejectWithValue }) => {
|
||||
const refreshTimestamp = new Date().getTime()
|
||||
|
||||
const deviceId = getState().genericViews.activeDevice
|
||||
if (!deviceId) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
const backupsList = await readBackupDirectoryRequest(deviceId)
|
||||
|
||||
if (!backupsList.ok) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
const backups =
|
||||
(backupsList.data
|
||||
.map((item) => {
|
||||
const isFormatValid = /^\d+[_][a-zA-Z0-9]+[.]mcbackup$/i.test(item)
|
||||
|
||||
if (!isFormatValid) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [fileName] = item.split(".")
|
||||
const [timestamp, serialNumber] = fileName.split("_")
|
||||
const result: Backup = {
|
||||
date: new Date(Number(timestamp)),
|
||||
fileName: item,
|
||||
serialNumber,
|
||||
}
|
||||
return result
|
||||
})
|
||||
.filter(Boolean) as Backup[]) ?? []
|
||||
|
||||
return {
|
||||
refreshTimestamp,
|
||||
backups,
|
||||
deviceId,
|
||||
}
|
||||
}
|
||||
)
|
||||
29
libs/generic-view/store/src/lib/file-transfer/actions.ts
Normal file
29
libs/generic-view/store/src/lib/file-transfer/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createAction } from "@reduxjs/toolkit"
|
||||
import { ActionName } from "../action-names"
|
||||
import { FileProgress } from "./reducer"
|
||||
import { SendFileErrorPayload } from "./send-file.action"
|
||||
import { GetFileErrorPayload } from "Libs/generic-view/store/src"
|
||||
|
||||
export const fileTransferSendPrepared = createAction<
|
||||
Pick<FileProgress, "chunksCount" | "transferId">
|
||||
>(ActionName.PreFileTransferSend)
|
||||
export const fileTransferChunkSent = createAction<
|
||||
Pick<FileProgress, "chunksTransferred" | "transferId">
|
||||
>(ActionName.ChunkFileTransferSend)
|
||||
export const fileTransferGetPrepared = createAction<
|
||||
Pick<FileProgress, "chunksCount" | "transferId" | "filePath">
|
||||
>(ActionName.PreFileTransferGet)
|
||||
export const fileTransferChunkGet = createAction<
|
||||
Pick<FileProgress, "chunksTransferred" | "transferId">
|
||||
>(ActionName.ChunkFileTransferGet)
|
||||
export const clearSendErrors = createAction<SendFileErrorPayload>(
|
||||
ActionName.ClearFileTransferSendError
|
||||
)
|
||||
export const clearGetErrors = createAction<GetFileErrorPayload>(
|
||||
ActionName.ClearFileTransferGetError
|
||||
)
|
||||
129
libs/generic-view/store/src/lib/file-transfer/get-file.action.ts
Normal file
129
libs/generic-view/store/src/lib/file-transfer/get-file.action.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import {
|
||||
getFileRequest,
|
||||
saveFileRequest,
|
||||
sendClearRequest,
|
||||
startPreGetFileRequest,
|
||||
} from "device/feature"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ActionName } from "../action-names"
|
||||
import { fileTransferChunkGet, fileTransferGetPrepared } from "./actions"
|
||||
import { AppError, AppErrorType } from "Core/core/errors"
|
||||
import { GeneralError, PreTransferGet } from "device/models"
|
||||
import { Result } from "Core/core/builder"
|
||||
|
||||
export type GetFileErrorPayload = {
|
||||
transferId?: number
|
||||
filePath: string
|
||||
}
|
||||
|
||||
interface GetFileError {
|
||||
deviceId: DeviceId
|
||||
error: AppError<AppErrorType, GetFileErrorPayload>
|
||||
}
|
||||
|
||||
export const getFile = createAsyncThunk<
|
||||
{ transferId: number },
|
||||
{
|
||||
deviceId: DeviceId
|
||||
filePath: string
|
||||
targetPath: string
|
||||
preTransfer?: PreTransferGet
|
||||
},
|
||||
{ state: ReduxRootState; rejectValue: GetFileError | undefined }
|
||||
>(
|
||||
ActionName.FileTransferGet,
|
||||
async (
|
||||
{ deviceId, filePath, targetPath, preTransfer },
|
||||
{ rejectWithValue, dispatch, signal }
|
||||
) => {
|
||||
let aborted = false
|
||||
|
||||
const abortListener = async () => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
aborted = true
|
||||
const transferId = preTransferResponse?.ok
|
||||
? preTransferResponse.data.transferId
|
||||
: preTransferResponse?.error.payload
|
||||
if (transferId) {
|
||||
await sendClearRequest(transferId)
|
||||
}
|
||||
}
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
const preTransferResponse = preTransfer
|
||||
? Result.success(preTransfer)
|
||||
: await startPreGetFileRequest(filePath, deviceId)
|
||||
|
||||
if (preTransferResponse.ok && !aborted) {
|
||||
const { transferId, chunkSize, fileSize } = preTransferResponse.data
|
||||
const chunksCount = Math.ceil(fileSize / chunkSize)
|
||||
dispatch(
|
||||
fileTransferGetPrepared({
|
||||
transferId,
|
||||
chunksCount,
|
||||
filePath,
|
||||
})
|
||||
)
|
||||
|
||||
for (let chunkNumber = 1; chunkNumber <= chunksCount; chunkNumber++) {
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
const { ok, error } = await getFileRequest(transferId, chunkNumber)
|
||||
if (!ok) {
|
||||
await sendClearRequest(transferId)
|
||||
return rejectWithValue({
|
||||
deviceId,
|
||||
error: {
|
||||
...error,
|
||||
payload: {
|
||||
transferId,
|
||||
filePath,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
dispatch(
|
||||
fileTransferChunkGet({
|
||||
transferId,
|
||||
chunksTransferred: chunkNumber,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (aborted) {
|
||||
return rejectWithValue(undefined)
|
||||
}
|
||||
|
||||
if (targetPath) {
|
||||
await saveFileRequest(targetPath, transferId)
|
||||
await sendClearRequest(transferId)
|
||||
}
|
||||
return { transferId }
|
||||
} else {
|
||||
return rejectWithValue({
|
||||
deviceId,
|
||||
error: {
|
||||
name: GeneralError.IncorrectResponse,
|
||||
type: GeneralError.IncorrectResponse,
|
||||
message: "Incorrect response",
|
||||
payload: {
|
||||
transferId: preTransferResponse.error?.payload,
|
||||
filePath,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
129
libs/generic-view/store/src/lib/file-transfer/reducer.ts
Normal file
129
libs/generic-view/store/src/lib/file-transfer/reducer.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createReducer } from "@reduxjs/toolkit"
|
||||
import { AppErrorType } from "Core/core/errors"
|
||||
import {
|
||||
clearGetErrors,
|
||||
clearSendErrors,
|
||||
fileTransferChunkGet,
|
||||
fileTransferChunkSent,
|
||||
fileTransferGetPrepared,
|
||||
fileTransferSendPrepared,
|
||||
} from "./actions"
|
||||
import { sendFile } from "./send-file.action"
|
||||
import { getFile } from "./get-file.action"
|
||||
|
||||
export interface FileTransferError {
|
||||
code?: AppErrorType
|
||||
message?: string
|
||||
filePath?: string
|
||||
transferId?: number
|
||||
}
|
||||
|
||||
export interface FileProgress {
|
||||
transferId: number
|
||||
chunksCount: number
|
||||
chunksTransferred: number
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
interface FileTransferState {
|
||||
sendingFilesProgress: {
|
||||
[transferId: number]: FileProgress
|
||||
}
|
||||
sendingErrors?: FileTransferError[]
|
||||
receivingFilesProgress: {
|
||||
[transferId: number]: FileProgress
|
||||
}
|
||||
receivingErrors?: FileTransferError[]
|
||||
}
|
||||
|
||||
const initialState: FileTransferState = {
|
||||
sendingFilesProgress: {},
|
||||
sendingErrors: [],
|
||||
receivingFilesProgress: {},
|
||||
receivingErrors: [],
|
||||
}
|
||||
|
||||
export const genericFileTransferReducer = createReducer(
|
||||
initialState,
|
||||
(builder) => {
|
||||
builder.addCase(fileTransferSendPrepared, (state, action) => {
|
||||
state.sendingFilesProgress[action.payload.transferId] = {
|
||||
transferId: action.payload.transferId,
|
||||
chunksCount: action.payload.chunksCount,
|
||||
chunksTransferred: 0,
|
||||
}
|
||||
})
|
||||
builder.addCase(fileTransferChunkSent, (state, action) => {
|
||||
state.sendingFilesProgress[action.payload.transferId].chunksTransferred =
|
||||
action.payload.chunksTransferred
|
||||
})
|
||||
builder.addCase(sendFile.fulfilled, (state, action) => {
|
||||
delete state.sendingFilesProgress[action.payload.transferId]
|
||||
})
|
||||
builder.addCase(sendFile.rejected, (state, action) => {
|
||||
const { transferId, filePath } = action.payload?.error.payload || {}
|
||||
if (transferId) {
|
||||
delete state.sendingFilesProgress[transferId]
|
||||
}
|
||||
state.sendingErrors?.push({
|
||||
code: action.payload?.error.type,
|
||||
message: action.payload?.error.message,
|
||||
transferId,
|
||||
filePath,
|
||||
})
|
||||
})
|
||||
builder.addCase(clearSendErrors, (state, action) => {
|
||||
state.sendingErrors = state.sendingErrors?.filter(
|
||||
(error) => error.transferId !== action.payload.transferId
|
||||
)
|
||||
})
|
||||
builder.addCase(fileTransferGetPrepared, (state, action) => {
|
||||
state.receivingFilesProgress[action.payload.transferId] = {
|
||||
transferId: action.payload.transferId,
|
||||
chunksCount: action.payload.chunksCount,
|
||||
chunksTransferred: 0,
|
||||
filePath: action.payload.filePath,
|
||||
}
|
||||
})
|
||||
builder.addCase(fileTransferChunkGet, (state, action) => {
|
||||
state.receivingFilesProgress[
|
||||
action.payload.transferId
|
||||
].chunksTransferred = action.payload.chunksTransferred
|
||||
})
|
||||
builder.addCase(getFile.fulfilled, (state, action) => {
|
||||
delete state.receivingFilesProgress[action.payload.transferId]
|
||||
})
|
||||
builder.addCase(getFile.rejected, (state, action) => {
|
||||
if (action.meta.aborted) {
|
||||
const transfer = Object.entries(state.receivingFilesProgress).find(
|
||||
([, item]) => item.filePath === action.meta.arg.filePath
|
||||
)
|
||||
if (transfer) {
|
||||
const transferId = Number(transfer[0])
|
||||
delete state.receivingFilesProgress[transferId]
|
||||
}
|
||||
} else {
|
||||
const { transferId, filePath } = action.payload?.error.payload || {}
|
||||
if (transferId) {
|
||||
delete state.receivingFilesProgress[transferId]
|
||||
}
|
||||
state.receivingErrors?.push({
|
||||
code: action.payload?.error.type,
|
||||
message: action.payload?.error.message,
|
||||
transferId,
|
||||
filePath: action.payload?.error.payload?.filePath,
|
||||
})
|
||||
}
|
||||
})
|
||||
builder.addCase(clearGetErrors, (state, action) => {
|
||||
state.receivingErrors = state.receivingErrors?.filter(
|
||||
(error) => error.transferId !== action.payload.transferId
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import {
|
||||
sendClearRequest,
|
||||
sendFileRequest,
|
||||
startPreSendFileRequest,
|
||||
} from "device/feature"
|
||||
import { DeviceId } from "Core/device/constants/device-id"
|
||||
import { ActionName } from "../action-names"
|
||||
import { fileTransferChunkSent, fileTransferSendPrepared } from "./actions"
|
||||
import { AppError, AppErrorType } from "Core/core/errors"
|
||||
import { GeneralError } from "device/models"
|
||||
|
||||
export type SendFileErrorPayload = {
|
||||
transferId?: number
|
||||
filePath: string
|
||||
}
|
||||
|
||||
interface SendFileError {
|
||||
deviceId: DeviceId
|
||||
error: AppError<AppErrorType, SendFileErrorPayload>
|
||||
}
|
||||
|
||||
export const sendFile = createAsyncThunk<
|
||||
{ transferId: number },
|
||||
{ deviceId: DeviceId; filePath: string; targetPath: string },
|
||||
{ state: ReduxRootState; rejectValue: SendFileError }
|
||||
>(
|
||||
ActionName.FileTransferSend,
|
||||
async ({ deviceId, filePath, targetPath }, { rejectWithValue, dispatch }) => {
|
||||
const preTransferResponse = await startPreSendFileRequest(
|
||||
filePath,
|
||||
targetPath,
|
||||
deviceId
|
||||
)
|
||||
|
||||
if (preTransferResponse.ok) {
|
||||
const { transferId, chunksCount } = preTransferResponse.data
|
||||
dispatch(
|
||||
fileTransferSendPrepared({
|
||||
transferId,
|
||||
chunksCount,
|
||||
})
|
||||
)
|
||||
|
||||
for (let chunkNumber = 1; chunkNumber <= chunksCount; chunkNumber++) {
|
||||
// TODO: consider using signal to abort
|
||||
const { ok, error } = await sendFileRequest(transferId, chunkNumber)
|
||||
if (!ok) {
|
||||
await sendClearRequest(transferId)
|
||||
return rejectWithValue({
|
||||
deviceId,
|
||||
error: {
|
||||
...error,
|
||||
payload: {
|
||||
transferId,
|
||||
filePath,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
dispatch(
|
||||
fileTransferChunkSent({
|
||||
transferId,
|
||||
chunksTransferred: chunkNumber,
|
||||
})
|
||||
)
|
||||
}
|
||||
await sendClearRequest(transferId)
|
||||
return { transferId }
|
||||
} else {
|
||||
return rejectWithValue({
|
||||
deviceId,
|
||||
error: {
|
||||
name: GeneralError.IncorrectResponse,
|
||||
type: GeneralError.IncorrectResponse,
|
||||
message: "Incorrect response",
|
||||
payload: {
|
||||
transferId: preTransferResponse.error.payload,
|
||||
filePath,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -7,3 +7,5 @@ export * from "./use-api-serial-port-listeners"
|
||||
export * from "./use-outbox"
|
||||
export * from "./use-screen-title"
|
||||
export * from "./use-locked-device-handler"
|
||||
export * from "./use-backup-list"
|
||||
export * from "./use-app-events-listeners"
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
|
||||
import { Device } from "Core/device-manager/reducers/device-manager.interface"
|
||||
import { useEffect } from "react"
|
||||
import { useDispatch } from "react-redux"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { answerMain, DeviceManagerMainEvent } from "shared/utils"
|
||||
import { detachDevice } from "../views/actions"
|
||||
import { getAPIConfig } from "../get-api-config"
|
||||
import { Dispatch } from "Core/__deprecated__/renderer/store"
|
||||
import { DeviceType } from "Core/device"
|
||||
import { setBackupProcessStatus } from "../backup/actions"
|
||||
import { closeAllModals } from "../modals/actions"
|
||||
import { selectBackupProcessStatus } from "../selectors"
|
||||
|
||||
export const useAPISerialPortListeners = () => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const backupProcess = useSelector(selectBackupProcessStatus)
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterFailListener = answerMain<Device>(
|
||||
@@ -39,12 +43,16 @@ export const useAPISerialPortListeners = () => {
|
||||
)
|
||||
const unregisterDetachedListener = answerMain<Device>(
|
||||
DeviceManagerMainEvent.DeviceDetached,
|
||||
(properties) => {
|
||||
async (properties) => {
|
||||
const { id, deviceType } = properties
|
||||
if (deviceType !== DeviceType.APIDevice) {
|
||||
return
|
||||
}
|
||||
dispatch(detachDevice({ deviceId: id }))
|
||||
dispatch(closeAllModals())
|
||||
if (backupProcess) {
|
||||
dispatch(setBackupProcessStatus("FAILED"))
|
||||
}
|
||||
}
|
||||
)
|
||||
return () => {
|
||||
@@ -52,5 +60,5 @@ export const useAPISerialPortListeners = () => {
|
||||
unregisterConnectListener()
|
||||
unregisterFailListener()
|
||||
}
|
||||
}, [dispatch])
|
||||
}, [backupProcess, dispatch])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useDispatch } from "react-redux"
|
||||
import { answerMain, AppEvents } from "shared/utils"
|
||||
import { Dispatch } from "Core/__deprecated__/renderer/store"
|
||||
import { refreshBackupList } from "../backup/refresh-backup-list.action"
|
||||
|
||||
export const useAppEventsListeners = () => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterWindowFocusedListener = answerMain(
|
||||
AppEvents.WindowFocused,
|
||||
() => {
|
||||
dispatch(refreshBackupList())
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
unregisterWindowFocusedListener()
|
||||
}
|
||||
}, [dispatch])
|
||||
}
|
||||
24
libs/generic-view/store/src/lib/hooks/use-backup-list.ts
Normal file
24
libs/generic-view/store/src/lib/hooks/use-backup-list.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { Dispatch } from "Core/__deprecated__/renderer/store"
|
||||
import { useEffect } from "react"
|
||||
import { useDispatch } from "react-redux"
|
||||
import { refreshBackupList } from "../backup/refresh-backup-list.action"
|
||||
|
||||
export const useBackupList = () => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
dispatch(refreshBackupList())
|
||||
}, 5000)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -31,3 +31,8 @@ export const useOutbox = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeDevice, lastRefreshTimestamp])
|
||||
}
|
||||
|
||||
export const OutboxWrapper = () => {
|
||||
useOutbox()
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { selectActiveDevice } from "./active-device"
|
||||
|
||||
export const selectActiveDeviceConfiguration = createSelector(
|
||||
[
|
||||
selectActiveDevice,
|
||||
(state: ReduxRootState) => state.genericViews.devicesConfiguration,
|
||||
],
|
||||
(activeDevice, devicesConfiguration) => {
|
||||
if (activeDevice) {
|
||||
return devicesConfiguration[activeDevice]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
@@ -4,18 +4,11 @@
|
||||
*/
|
||||
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { selectActiveDevice } from "./active-device"
|
||||
import { selectActiveDeviceConfiguration } from "./active-device-configuration"
|
||||
|
||||
export const selectActiveDeviceFeatures = createSelector(
|
||||
[
|
||||
selectActiveDevice,
|
||||
(state: ReduxRootState) => state.genericViews.devicesConfiguration,
|
||||
],
|
||||
(activeDevice, devicesConfiguration) => {
|
||||
if (activeDevice) {
|
||||
return devicesConfiguration[activeDevice].features
|
||||
}
|
||||
return undefined
|
||||
selectActiveDeviceConfiguration,
|
||||
(activeDeviceConfiguration) => {
|
||||
return activeDeviceConfiguration?.features
|
||||
}
|
||||
)
|
||||
|
||||
18
libs/generic-view/store/src/lib/selectors/backup-location.ts
Normal file
18
libs/generic-view/store/src/lib/selectors/backup-location.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { selectActiveDeviceConfiguration } from "./active-device-configuration"
|
||||
|
||||
export const selectBackupLocation = createSelector(
|
||||
selectActiveDeviceConfiguration,
|
||||
(state: ReduxRootState) => state.settings.osBackupLocation,
|
||||
(deviceConfiguration, location) => {
|
||||
const { vendorId, productId } = deviceConfiguration?.apiConfig || {}
|
||||
const deviceDirectory = `${vendorId}_${productId}`
|
||||
return vendorId && productId ? `${location}/${deviceDirectory}` : location
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
|
||||
export const selectBackupProcessStatus = createSelector(
|
||||
(state: ReduxRootState) => state.genericBackups.backupProcess?.status,
|
||||
(status) => status
|
||||
)
|
||||
40
libs/generic-view/store/src/lib/selectors/backup-progress.ts
Normal file
40
libs/generic-view/store/src/lib/selectors/backup-progress.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { createSelector } from "@reduxjs/toolkit"
|
||||
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
|
||||
export const backupProgress = createSelector(
|
||||
[(state: ReduxRootState) => state.genericBackups.backupProcess],
|
||||
(backupProcess) => {
|
||||
if (!backupProcess) {
|
||||
return { progress: 0 }
|
||||
}
|
||||
|
||||
if (backupProcess.status === "DONE") {
|
||||
return { progress: 100 }
|
||||
}
|
||||
|
||||
const features = Object.values(backupProcess.featureFilesTransfer)
|
||||
const downloadedFilesCount = features.filter(
|
||||
(item) => item.status === "DONE"
|
||||
).length
|
||||
|
||||
if (features.length <= downloadedFilesCount) {
|
||||
return { progress: 90 }
|
||||
}
|
||||
|
||||
const [featureInProgress] =
|
||||
Object.entries(backupProcess.featureFilesTransfer).find(
|
||||
([, item]) => item.status === "IN_PROGRESS"
|
||||
) ?? []
|
||||
|
||||
return {
|
||||
progress: Math.floor(10 + (downloadedFilesCount / features.length) * 80),
|
||||
featureInProgress,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -9,10 +9,10 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
|
||||
export const selectDeviceBackups = createSelector(
|
||||
selectActiveDevice,
|
||||
(state: ReduxRootState) => state.genericBackups.files,
|
||||
(state: ReduxRootState) => state.genericBackups.backups,
|
||||
(activeDevice, backups) => {
|
||||
return backups
|
||||
.filter((backup) => backup.device.serialNumber === activeDevice)
|
||||
.filter((backup) => backup.serialNumber === activeDevice)
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,3 +12,6 @@ export * from "./component"
|
||||
export * from "./view"
|
||||
export * from "./api-error"
|
||||
export * from "./device-backups"
|
||||
export * from "./backup-progress"
|
||||
export * from "./backup-process-status"
|
||||
export * from "./backup-location"
|
||||
|
||||
@@ -50,6 +50,7 @@ interface GenericState {
|
||||
lastResponse: unknown
|
||||
lastRefresh?: number
|
||||
activeDevice?: DeviceId
|
||||
pendingDevice?: DeviceId
|
||||
devicesConfiguration: Record<string, DeviceConfiguration>
|
||||
apiErrors: Record<ApiError, boolean>
|
||||
}
|
||||
@@ -152,12 +153,17 @@ export const genericViewsReducer = createReducer(initialState, (builder) => {
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
if (state.activeDevice === undefined && state.pendingDevice === deviceId) {
|
||||
state.activeDevice = deviceId
|
||||
state.pendingDevice = undefined
|
||||
}
|
||||
})
|
||||
builder.addCase(activateDevice, (state, action) => {
|
||||
const { deviceId } = action.payload
|
||||
state.activeDevice = state.devicesConfiguration?.[deviceId]?.apiConfig
|
||||
? deviceId
|
||||
: undefined
|
||||
state.pendingDevice = deviceId
|
||||
})
|
||||
builder.addCase(detachDevice, (state, action) => {
|
||||
const { deviceId } = action.payload
|
||||
@@ -167,6 +173,9 @@ export const genericViewsReducer = createReducer(initialState, (builder) => {
|
||||
if (state.activeDevice === deviceId) {
|
||||
state.activeDevice = undefined
|
||||
}
|
||||
if (state.pendingDevice === deviceId) {
|
||||
state.pendingDevice = undefined
|
||||
}
|
||||
})
|
||||
builder.addCase(getOutboxData.fulfilled, (state, action) => {
|
||||
const { deviceId, timestamp } = action.payload
|
||||
|
||||
@@ -13,4 +13,5 @@ export const color = {
|
||||
grey4: "#D2D6DB",
|
||||
grey5: "#F4F5F6",
|
||||
grey6: "#FBFBFB",
|
||||
red1: "#E96A6A",
|
||||
} as const
|
||||
|
||||
@@ -17,4 +17,6 @@ export const fontSize = {
|
||||
buttonLink: "1.4rem",
|
||||
buttonText: "1.2rem",
|
||||
detailText: "1.2rem",
|
||||
labelText: "1.2rem",
|
||||
modalTitle: "2rem",
|
||||
} as const
|
||||
|
||||
@@ -58,5 +58,23 @@ const GlobalStyle = createGlobalStyle<{ theme: Theme }>`
|
||||
box-shadow: 0 2rem 10rem 0 ${({ theme }) => theme.color.black + "26"};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.modal-close-button:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: ${({ theme }) => theme.color.grey2};
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -17,4 +17,6 @@ export const lineHeight = {
|
||||
buttonLink: "2.2rem",
|
||||
buttonText: "2rem",
|
||||
detailText: "2rem",
|
||||
labelText: "2rem",
|
||||
modalTitle: "3.2rem",
|
||||
} as const
|
||||
|
||||
@@ -13,7 +13,9 @@ import { buttons } from "./lib/buttons/buttons"
|
||||
export { default as Icon } from "./lib/icon/icon"
|
||||
export * from "./lib/api-connection-demo"
|
||||
export * from "./lib/interactive/modal/modal-base"
|
||||
export * from "./lib/interactive/modal/modal-helpers"
|
||||
export * from "./lib/shared/shared"
|
||||
export * from "./lib/predefined/backup/backup-error"
|
||||
|
||||
const apiComponents = {
|
||||
...predefinedComponents,
|
||||
@@ -27,4 +29,3 @@ const apiComponents = {
|
||||
export default apiComponents
|
||||
|
||||
export type APIComponents = typeof apiComponents
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ import { DefaultButton } from "../../shared/button"
|
||||
interface Props extends HTMLAttributes<HTMLButtonElement> {
|
||||
action: ButtonAction
|
||||
viewKey?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const ButtonBase: FunctionComponent<Props> = ({ action, ...props }) => {
|
||||
const callButtonAction = useButtonAction(props.viewKey as string)
|
||||
const callAction = () => callButtonAction(action)
|
||||
|
||||
return <DefaultButton {...props} onClick={callAction} />
|
||||
return <DefaultButton {...props} onClick={callAction} type={"button"} />
|
||||
}
|
||||
|
||||
@@ -7,20 +7,31 @@ import {
|
||||
closeAllModals,
|
||||
closeDomainModals,
|
||||
closeModal,
|
||||
createBackup,
|
||||
openModal,
|
||||
replaceModal,
|
||||
selectActiveDevice,
|
||||
sendFile,
|
||||
useScreenTitle,
|
||||
} from "generic-view/store"
|
||||
import { ButtonAction } from "generic-view/utils"
|
||||
import { useDispatch } from "react-redux"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { useHistory } from "react-router"
|
||||
import { Dispatch, ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { getPaths } from "shared/app-state"
|
||||
import { PayloadAction } from "@reduxjs/toolkit"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
import { platform } from "Core/__deprecated__/renderer/utils/platform"
|
||||
|
||||
export const useButtonAction = (viewKey: string) => {
|
||||
const dispatch = useDispatch()
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const navigate = useHistory()
|
||||
const currentViewName = useScreenTitle(viewKey)
|
||||
const restore = useButtonRestoreAction()
|
||||
const backup = useButtonBackupAction()
|
||||
|
||||
return (action: ButtonAction) => {
|
||||
return (action?: ButtonAction) => {
|
||||
if (!action) return
|
||||
switch (action.type) {
|
||||
case "open-modal":
|
||||
dispatch(
|
||||
@@ -57,6 +68,60 @@ export const useButtonAction = (viewKey: string) => {
|
||||
},
|
||||
})
|
||||
break
|
||||
case "restore-data":
|
||||
void restore()
|
||||
break
|
||||
case "backup-data":
|
||||
void backup()
|
||||
break
|
||||
case "custom":
|
||||
action.callback()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useButtonRestoreAction = () => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const osBackupLocation = useSelector(
|
||||
(state: ReduxRootState) => state.settings.osBackupLocation
|
||||
)
|
||||
const deviceId = useSelector(selectActiveDevice)
|
||||
|
||||
return async () => {
|
||||
const { payload: getPathsPayload } = (await dispatch(
|
||||
getPaths({
|
||||
properties: ["openFile"],
|
||||
defaultPath: osBackupLocation,
|
||||
})
|
||||
)) as PayloadAction<ResultObject<string[] | undefined>>
|
||||
const location = getPathsPayload.ok && (getPathsPayload.data as string[])[0]
|
||||
if (location && deviceId) {
|
||||
const [fileName] = location
|
||||
.split(platform.windows() ? "\\" : "/")
|
||||
.reverse()
|
||||
dispatch(
|
||||
sendFile({
|
||||
deviceId: deviceId,
|
||||
filePath: location,
|
||||
targetPath: `/storage/emulated/0/Documents/${fileName}`,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useButtonBackupAction = () => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const deviceId = useSelector(selectActiveDevice)
|
||||
|
||||
return async () => {
|
||||
if (deviceId) {
|
||||
await dispatch(
|
||||
createBackup({ features: ["CONTACTS_LIST", "MESSAGES", "CALL_LOG"] })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import { ButtonBase } from "./button-base/button-base"
|
||||
import Icon from "../icon/icon"
|
||||
import { withConfig } from "../utils/with-config"
|
||||
|
||||
interface Config {
|
||||
export interface Config {
|
||||
text: string
|
||||
icon?: IconType
|
||||
action: ButtonAction
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const ButtonPrimary: APIFC<undefined, Config> = ({
|
||||
@@ -22,7 +23,7 @@ export const ButtonPrimary: APIFC<undefined, Config> = ({
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Button {...props} action={config!.action}>
|
||||
<Button {...props} disabled={config?.disabled} action={config!.action}>
|
||||
{config?.icon && <Icon data={{ type: config.icon }} />}
|
||||
<span>{config?.text}</span>
|
||||
</Button>
|
||||
|
||||
@@ -5,15 +5,9 @@
|
||||
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
import { APIFC, ButtonAction, IconType } from "generic-view/utils"
|
||||
import { APIFC } from "generic-view/utils"
|
||||
import { withConfig } from "../utils/with-config"
|
||||
import { ButtonPrimary } from "./button-primary"
|
||||
|
||||
interface Config {
|
||||
text: string
|
||||
icon?: IconType
|
||||
action: ButtonAction
|
||||
}
|
||||
import { ButtonPrimary, Config } from "./button-primary"
|
||||
|
||||
export const ButtonSecondary: APIFC<undefined, Config> = (props) => {
|
||||
return <Button {...props} />
|
||||
|
||||
@@ -25,12 +25,8 @@ export const ButtonText: APIFC<undefined, Config> = ({
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
action={config!.action}
|
||||
$modifiers={config?.modifiers}
|
||||
>
|
||||
{config?.icon && <Icon data={{ type: config.icon }} />}
|
||||
<Button {...props} action={config!.action} $modifiers={config?.modifiers}>
|
||||
{config?.icon && <Icon className={"icon"} data={{ type: config.icon }} />}
|
||||
<span>{config?.text}</span>
|
||||
</Button>
|
||||
)
|
||||
@@ -39,30 +35,39 @@ export const ButtonText: APIFC<undefined, Config> = ({
|
||||
export default withConfig(ButtonText)
|
||||
|
||||
const Button = styled(ButtonBase)<{ $modifiers?: ButtonModifiers[] }>`
|
||||
color: ${({ theme }) => theme.color.grey1};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.color.black};
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: ${({ theme }) => theme.fontSize.buttonLink};
|
||||
line-height: ${({ theme }) => theme.lineHeight.buttonLink};
|
||||
color: ${({ theme }) => theme.color.grey1};
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: unset;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
&:hover span {
|
||||
color: ${({ theme }) => theme.color.black};
|
||||
}
|
||||
|
||||
${({ $modifiers }) => $modifiers?.includes("link") && buttonLinkModifier};
|
||||
${({ $modifiers }) =>
|
||||
$modifiers?.includes("uppercase") && buttonUpperCaseModifier};
|
||||
${({ $modifiers }) =>
|
||||
$modifiers?.includes("hover-underline") && buttonHoverUnderlineModifier};
|
||||
|
||||
.icon {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
|
||||
svg * {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const buttonLinkModifier = css`
|
||||
span {
|
||||
color: ${({ theme }) => theme.color.blue2};
|
||||
}
|
||||
&:hover span {
|
||||
color: ${({ theme }) => theme.color.blue2};
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.color.blue2};
|
||||
}
|
||||
`
|
||||
@@ -73,6 +78,7 @@ const buttonUpperCaseModifier = css`
|
||||
font-size: ${({ theme }) => theme.fontSize.buttonText};
|
||||
line-height: ${({ theme }) => theme.lineHeight.buttonText};
|
||||
letter-spacing: 0.1em;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ import CloseIcon from "Core/__deprecated__/renderer/svg/close.svg"
|
||||
import Device from "Core/__deprecated__/renderer/svg/device.svg"
|
||||
import Mudita from "Core/__deprecated__/renderer/svg/mudita.svg"
|
||||
import Spinner from "Core/__deprecated__/renderer/svg/spinner.svg"
|
||||
|
||||
import Backup from "./svg/backup.svg"
|
||||
import Settings from "./svg/settings.svg"
|
||||
import PasswordShow from "./svg/password-show.svg"
|
||||
import PasswordHide from "./svg/password-hide.svg"
|
||||
import Success from "./svg/confirm.svg"
|
||||
import Failure from "./svg/failed.svg"
|
||||
import Folder from "./svg/folder.svg"
|
||||
|
||||
import { IconType } from "generic-view/utils"
|
||||
|
||||
const typeToIcon: Record<IconType, typeof BatteryHigh> = {
|
||||
@@ -56,6 +65,13 @@ const typeToIcon: Record<IconType, typeof BatteryHigh> = {
|
||||
[IconType.Device]: Device,
|
||||
[IconType.Mudita]: Mudita,
|
||||
[IconType.Spinner]: Spinner,
|
||||
[IconType.Backup]: Backup,
|
||||
[IconType.Settings]: Settings,
|
||||
[IconType.PasswordShow]: PasswordShow,
|
||||
[IconType.PasswordHide]: PasswordHide,
|
||||
[IconType.Success]: Success,
|
||||
[IconType.Failure]: Failure,
|
||||
[IconType.Folder]: Folder
|
||||
}
|
||||
|
||||
export const getIcon = (
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { IconType } from "generic-view/utils"
|
||||
import { APIFC } from "generic-view/utils"
|
||||
import { APIFC, IconType } from "generic-view/utils"
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
import { getIcon } from "./get-icon.helper"
|
||||
|
||||
interface IconProps {
|
||||
interface Data {
|
||||
type: IconType
|
||||
}
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
color: pink;
|
||||
color: inherit;
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
& > * {
|
||||
@@ -23,7 +22,7 @@ const StyledIcon = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const Icon: APIFC<IconProps> = ({ className, data, ...rest }) => {
|
||||
const Icon: APIFC<Data> = ({ data, ...rest }) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
@@ -31,11 +30,7 @@ const Icon: APIFC<IconProps> = ({ className, data, ...rest }) => {
|
||||
const SVGToDisplay = getIcon(data.type)
|
||||
|
||||
return (
|
||||
<StyledIcon
|
||||
className={className}
|
||||
data-testid={`icon-${data.type}`}
|
||||
{...rest}
|
||||
>
|
||||
<StyledIcon data-testid={`icon-${data.type}`} {...rest}>
|
||||
<SVGToDisplay />
|
||||
</StyledIcon>
|
||||
)
|
||||
|
||||
8
libs/generic-view/ui/src/lib/icon/svg/backup.svg
Normal file
8
libs/generic-view/ui/src/lib/icon/svg/backup.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_17419_28735" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="6" width="36" height="36">
|
||||
<path d="M38 6H10C7.79086 6 6 7.79086 6 10V38C6 40.2091 7.79086 42 10 42H38C40.2091 42 42 40.2091 42 38V10C42 7.79086 40.2091 6 38 6Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_17419_28735)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0002 13C13.0002 12.4477 13.4479 12 14.0002 12H21.7425C22.0904 12 22.4133 12.1808 22.5951 12.4774L23.5284 14H32.6464C34.3033 14 35.6464 15.3431 35.6464 17V17.131C36.9849 17.5409 37.9108 18.8597 37.7473 20.3313L36.1917 34.3313C36.0229 35.8506 34.7387 37 33.2101 37H14.7902C13.2616 37 11.9774 35.8506 11.8086 34.3313L10.1297 19.2209C9.99802 18.0361 10.9254 17 12.1174 17H13.0002V13ZM15.0002 17H33.6464C33.6464 16.4477 33.1987 16 32.6464 16H22.9685C22.6206 16 22.2977 15.8192 22.1159 15.5226L21.1826 14H15.0002V17ZM12.1174 19L13.7964 34.1104C13.8526 34.6169 14.2807 35 14.7902 35H33.2101C33.7196 35 34.1477 34.6169 34.204 34.1104L35.7595 20.1104C35.8253 19.5181 35.3616 19 34.7656 19H12.1174Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user