Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Nov 29, 2024
1 parent 2ca4a3a commit 461da63
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 60 deletions.
36 changes: 34 additions & 2 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { app, BrowserWindow, protocol, screen } from 'electron'
import { app, BrowserWindow, ipcMain, protocol, screen } from 'electron'
import { join } from 'path'

import { electronStorage } from '../src/libs/electron/filesystemStorageRendererAPI'

export const ROOT_PATH = {
dist: join(__dirname, '..'),
}
Expand Down Expand Up @@ -59,7 +61,37 @@ protocol.registerSchemesAsPrivileged([
},
])

app.whenReady().then(createWindow)
app.whenReady().then(async () => {
console.log('Electron app is ready.')
console.log(`Cockpit version: ${app.getVersion()}`)

console.log('Creating window...')
await createWindow()

console.log('Setting up filesystem storage...')
setupFilesystemStorage()
})

const setupFilesystemStorage = (): void => {
ipcMain.on('setItem', (event, data) => {
electronStorage.setItem(data.key, data.value)
})
ipcMain.on('getItem', (event, key) => {
return electronStorage.getItem(key)
})
ipcMain.on('removeItem', (event, key) => {
electronStorage.removeItem(key)
})
ipcMain.on('clear', () => {
electronStorage.clear()
})
ipcMain.on('keys', () => {
return electronStorage.keys()
})
ipcMain.on('iterate', (event, callback) => {
electronStorage.iterate(callback)
})
}

app.on('before-quit', () => {
// @ts-ignore: import.meta.env does not exist in the types
Expand Down
12 changes: 12 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
setItem: (key: string, value: Blob) => ipcRenderer.send('setItem', { key, value }),
getItem: (key: string) => ipcRenderer.invoke('getItem', key),
removeItem: (key: string) => ipcRenderer.send('removeItem', key),
clear: () => ipcRenderer.send('clear'),
keys: () => ipcRenderer.invoke('keys'),
iterate: (callback: (value: unknown, key: string, iterationNumber: number) => void) =>
ipcRenderer.on('iterate', (_event, data) => callback(data.value, data.key, data.iterationNumber)),
})
6 changes: 3 additions & 3 deletions src/components/VideoLibraryModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,11 @@ const fetchVideosAndLogData = async (): Promise<void> => {
const logFileOperations: Promise<VideoLibraryLogFile>[] = []
// Fetch processed videos and logs
await videoStore.videoStoringDB.iterate((value, key) => {
await videoStore.videoStorage.iterate((value, key) => {
if (videoStore.isVideoFilename(key)) {
videoFilesOperations.push(
(async () => {
const videoBlob = await videoStore.videoStoringDB.getItem<Blob>(key)
const videoBlob = await videoStore.videoStorage.getItem(key)
let url = ''
let isProcessed = true
if (videoBlob instanceof Blob) {
Expand All @@ -910,7 +910,7 @@ const fetchVideosAndLogData = async (): Promise<void> => {
if (key.endsWith('.ass')) {
logFileOperations.push(
(async () => {
const videoBlob = await videoStore.videoStoringDB.getItem<Blob>(key)
const videoBlob = await videoStore.videoStorage.getItem(key)
let url = ''
if (videoBlob instanceof Blob) {
url = URL.createObjectURL(videoBlob)
Expand Down
2 changes: 1 addition & 1 deletion src/components/mini-widgets/MiniVideoRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ watch(nameSelectedStream, (newName) => {
// Fetch number of temporary videos on storage
const fetchNumberOfTempVideos = async (): Promise<void> => {
const nProcessedVideos = (await videoStore.videoStoringDB.keys()).filter((k) => videoStore.isVideoFilename(k)).length
const nProcessedVideos = (await videoStore.videoStorage.keys()).filter((k) => videoStore.isVideoFilename(k)).length
const nFailedUnprocessedVideos = Object.keys(videoStore.keysFailedUnprocessedVideos).length
numberOfVideosOnDB.value = nProcessedVideos + nFailedUnprocessedVideos
}
Expand Down
31 changes: 30 additions & 1 deletion src/libs/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,38 @@ declare global {
registerActionCallback: typeof registerActionCallback
unregisterActionCallback: typeof unregisterActionCallback
executeActionCallback: typeof executeActionCallback
/* eslint-enable jsdoc/require-jsdoc */
}
/**
* Electron API for update management
*/
electronAPI: {
/**
* Set an item in the filesystem storage
*/
setItem: (key: string, value: Blob) => Promise<Blob>
/**
* Get an item from the filesystem storage
*/
getItem: (key: string) => Promise<Blob | null | undefined>
/**
* Remove an item from the filesystem storage
*/
removeItem: (key: string) => Promise<void>
/**
* Clear the filesystem storage
*/
clear: () => Promise<void>
/**
* Get all keys from the filesystem storage
*/
keys: () => Promise<string[]>
/**
* Iterate over the items in the filesystem storage
*/
iterate: (callback: (value: unknown, key: string, iterationNumber: number) => void) => Promise<void>
}
}
/* eslint-enable jsdoc/require-jsdoc */
}

// Use global as window when running for browsers
Expand Down
65 changes: 65 additions & 0 deletions src/libs/electron/filesystemStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { app } from 'electron'
import fs from 'fs/promises'
import { dirname, join } from 'path'

import { StorageDB } from '@/types/general'

import { isElectron } from '../utils'

// Create a new storage interface for filesystem
const cockpitFolderPath = join(app.getPath('userData'), 'Cockpit')
const filesystemOnlyInElectronErrorMsg = 'Filesystem storage is only available in Electron.'

export const filesystemStorage: StorageDB = {
async setItem(key: string, value: Blob): Promise<Blob> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
console.log('setItem on electron', key)
const filePath = join(cockpitFolderPath, key)
await fs.mkdir(dirname(filePath), { recursive: true })
await fs.writeFile(filePath, Buffer.from(await value.arrayBuffer()))
return value
},
async getItem(key: string): Promise<Blob | null> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
console.log('getItem on electron', key)
const filePath = join(cockpitFolderPath, key)
try {
const buffer = await fs.readFile(filePath)
return new Blob([buffer])
} catch (error) {
if (error.code === 'ENOENT') return null
throw error
}
},
async removeItem(key: string): Promise<void> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
const filePath = join(cockpitFolderPath, key)
await fs.unlink(filePath)
},
async clear(): Promise<void> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
throw new Error(
`Clear functionality is not available in the filesystem storage, so we don't risk losing important data. If you
want to clear the storage, please delete the Cockpit folder in your user data directory manually.`
)
},
async keys(): Promise<string[]> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
const dirPath = cockpitFolderPath
try {
return await fs.readdir(dirPath)
} catch (error) {
if (error.code === 'ENOENT') return []
throw error
}
},
async iterate(callback: (value: unknown, key: string, iterationNumber: number) => void): Promise<void> {
if (!isElectron()) throw new Error(filesystemOnlyInElectronErrorMsg)
const keys = await this.keys()
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = await this.getItem(key)
callback(value, key, i)
}
},
}
45 changes: 45 additions & 0 deletions src/libs/electron/filesystemStorageRendererAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { StorageDB } from '@/types/general'

import { isElectron } from '../utils'

const throwIfNotElectron = (): void => {
if (!isElectron()) {
console.warn('Filesystem storage is only available in Electron.')
return
}
if (!window.electronAPI) {
console.error('electronAPI is not available on window object')
console.debug('Available window properties:', Object.keys(window))
throw new Error('Electron filesystem API is not properly initialized. This is likely a setup issue.')
}
}

export const electronStorage: StorageDB = {
setItem: async (key: string, value: Blob): Promise<Blob> => {
throwIfNotElectron()
const arrayBuffer = await value.arrayBuffer()
await window.electronAPI.setItem(key, new Blob([arrayBuffer]))
return value
},
getItem: async (key: string): Promise<Blob | null | undefined> => {
throwIfNotElectron()
const arrayBuffer = await window.electronAPI.getItem(key)
return arrayBuffer ? new Blob([arrayBuffer]) : null
},
removeItem: async (key: string): Promise<void> => {
throwIfNotElectron()
await window.electronAPI.removeItem(key)
},
clear: async (): Promise<void> => {
throwIfNotElectron()
await window.electronAPI.clear()
},
keys: async (): Promise<string[]> => {
throwIfNotElectron()
return await window.electronAPI.keys()
},
iterate: async (callback: (value: unknown, key: string, iterationNumber: number) => void): Promise<void> => {
throwIfNotElectron()
await window.electronAPI.iterate(callback)
},
}
9 changes: 2 additions & 7 deletions src/libs/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { useInteractionDialog } from '@/composables/interactionDialog'

const { showDialog } = useInteractionDialog()

export const constrain = (value: number, min: number, max: number): number => {
return Math.max(Math.min(value, max), min)
}
Expand Down Expand Up @@ -135,7 +131,7 @@ export const tryOrAlert = async (tryFunction: () => Promise<void>): Promise<void
try {
await tryFunction()
} catch (error) {
showDialog({ message: error as string, variant: 'error' })
console.error(error as string)
}
}

Expand All @@ -145,8 +141,7 @@ export const tryOrAlert = async (tryFunction: () => Promise<void>): Promise<void
*/
export const reloadCockpit = (timeout = 500): void => {
const restartMessage = `Restarting Cockpit in ${timeout / 1000} seconds...`
console.log(restartMessage)
showDialog({ message: restartMessage, variant: 'info', timer: timeout })
console.info(restartMessage)
setTimeout(() => location.reload(), timeout)
}

Expand Down
25 changes: 25 additions & 0 deletions src/libs/videoStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import localforage from 'localforage'

import { electronStorage } from '@/libs/electron/filesystemStorageRendererAPI'
import { StorageDB } from '@/types/general'

import { isElectron } from './utils'

const tempVideoChunksIndexdedDB = localforage.createInstance({
driver: localforage.INDEXEDDB,
name: 'Cockpit - Temporary Video',
storeName: 'cockpit-temp-video-db',
version: 1.0,
description: 'Database for storing the chunks of an ongoing recording, to be merged afterwards.',
})

const videoStoringIndexedDB = localforage.createInstance({
driver: localforage.INDEXEDDB,
name: 'Cockpit - Video Recovery',
storeName: 'cockpit-video-recovery-db',
version: 1.0,
description: 'Local backups of Cockpit video recordings to be retrieved in case of failure.',
})

export const videoStorage: StorageDB = isElectron() ? electronStorage : videoStoringIndexedDB
export const tempVideoStorage: StorageDB = isElectron() ? electronStorage : tempVideoChunksIndexdedDB
Loading

0 comments on commit 461da63

Please sign in to comment.