Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Dec 10, 2024
1 parent 595c5d8 commit c1b0dbb
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 65 deletions.
10 changes: 9 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app, BrowserWindow, protocol, screen } from 'electron'
import { join } from 'path'

import { setupNetworkService } from './services/network'
import { setupFilesystemStorage } from './services/storage'

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

setupFilesystemStorage()
setupNetworkService()

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

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

app.on('before-quit', () => {
// @ts-ignore: import.meta.env does not exist in the types
Expand Down
17 changes: 17 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,21 @@ import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
getInfoOnSubnets: () => ipcRenderer.invoke('get-info-on-subnets'),
setItem: async (key: string, value: Blob, subFolders?: string[]) => {
const arrayBuffer = await value.arrayBuffer()
await ipcRenderer.invoke('setItem', { key, value: new Uint8Array(arrayBuffer), subFolders })
},
getItem: async (key: string, subFolders?: string[]) => {
const arrayBuffer = await ipcRenderer.invoke('getItem', { key, subFolders })
return arrayBuffer ? new Blob([arrayBuffer]) : null
},
removeItem: async (key: string, subFolders?: string[]) => {
await ipcRenderer.invoke('removeItem', { key, subFolders })
},
clear: async (subFolders?: string[]) => {
await ipcRenderer.invoke('clear', { subFolders })
},
keys: async (subFolders?: string[]) => {
return await ipcRenderer.invoke('keys', { subFolders })
},
})
61 changes: 61 additions & 0 deletions electron/services/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ipcMain } from 'electron'
import { app } from 'electron'
import * as fs from 'fs/promises'
import { dirname, join } from 'path'

// Create a new storage interface for filesystem
const cockpitFolderPath = join(app.getPath('home'), 'Cockpit')
fs.mkdir(cockpitFolderPath, { recursive: true })

export const filesystemStorage = {
async setItem(key: string, value: ArrayBuffer, subFolders?: string[]): Promise<void> {
const buffer = Buffer.from(value)
const filePath = join(cockpitFolderPath, ...(subFolders ?? []), key)
await fs.mkdir(dirname(filePath), { recursive: true })
await fs.writeFile(filePath, buffer)
},
async getItem(key: string, subFolders?: string[]): Promise<ArrayBuffer | null> {
const filePath = join(cockpitFolderPath, ...(subFolders ?? []), key)
try {
return await fs.readFile(filePath)
} catch (error) {
if (error.code === 'ENOENT') return null
throw error
}
},
async removeItem(key: string, subFolders?: string[]): Promise<void> {
const filePath = join(cockpitFolderPath, ...(subFolders ?? []), key)
await fs.unlink(filePath)
},
async clear(subFolders?: string[]): Promise<void> {
const dirPath = join(cockpitFolderPath, ...(subFolders ?? []))
await fs.rm(dirPath, { recursive: true })
},
async keys(subFolders?: string[]): Promise<string[]> {
const dirPath = join(cockpitFolderPath, ...(subFolders ?? []))
try {
return await fs.readdir(dirPath)
} catch (error) {
if (error.code === 'ENOENT') return []
throw error
}
},
}

export const setupFilesystemStorage = (): void => {
ipcMain.handle('setItem', async (_, data) => {
await filesystemStorage.setItem(data.key, data.value, data.subFolders)
})
ipcMain.handle('getItem', async (_, data) => {
return await filesystemStorage.getItem(data.key, data.subFolders)
})
ipcMain.handle('removeItem', async (_, data) => {
await filesystemStorage.removeItem(data.key, data.subFolders)
})
ipcMain.handle('clear', async (_, data) => {
await filesystemStorage.clear(data.subFolders)
})
ipcMain.handle('keys', async (_, data) => {
return await filesystemStorage.keys(data.subFolders)
})
}
9 changes: 5 additions & 4 deletions src/components/VideoLibraryModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,12 @@ const fetchVideosAndLogData = async (): Promise<void> => {
const logFileOperations: Promise<VideoLibraryLogFile>[] = []
// Fetch processed videos and logs
await videoStore.videoStoringDB.iterate((value, key) => {
const keys = await videoStore.videoStorage.keys()
for (const key of keys) {
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 +911,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 All @@ -923,7 +924,7 @@ const fetchVideosAndLogData = async (): Promise<void> => {
})()
)
}
})
}
// Fetch unprocessed videos
const unprocessedVideos = await videoStore.unprocessedVideos
Expand Down
3 changes: 2 additions & 1 deletion src/components/mini-widgets/MiniVideoRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ 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 keys = await videoStore.videoStorage.keys()
const nProcessedVideos = keys.filter((k) => videoStore.isVideoFilename(k)).length
const nFailedUnprocessedVideos = Object.keys(videoStore.keysFailedUnprocessedVideos).length
numberOfVideosOnDB.value = nProcessedVideos + nFailedUnprocessedVideos
}
Expand Down
3 changes: 2 additions & 1 deletion src/libs/cosmos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isBrowser } from 'browser-or-node'

import { ElectronStorageDB } from '@/types/general'
import { NetworkInfo } from '@/types/network'

import {
Expand Down Expand Up @@ -107,7 +108,7 @@ declare global {
/**
* Electron API exposed through preload script
*/
electronAPI?: {
electronAPI?: ElectronStorageDB & {
/**
* Get network information from the main process
* @returns Promise containing subnet information
Expand Down
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
127 changes: 127 additions & 0 deletions src/libs/videoStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import localforage from 'localforage'

import type { ElectronStorageDB, 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.')
}
}

/**
* Electron storage implementation.
* Uses the exposed IPC renderer API to store and retrieve data in the filesystem.
*/
class ElectronStorage implements ElectronStorageDB {
subFolders: string[]
electronAPI: ElectronStorageDB

/**
* Creates a new instance of the ElectronStorage class.
* @param {string[]} subFolders - The subfolders to store the data in.
*/
constructor(subFolders: string[]) {
throwIfNotElectron()

this.subFolders = subFolders
this.electronAPI = window.electronAPI as StorageDB
}

setItem = async (key: string, value: Blob): Promise<void> => {
throwIfNotElectron()
await this.electronAPI.setItem(key, value, this.subFolders)
}

getItem = async (key: string): Promise<Blob | null | undefined> => {
throwIfNotElectron()
return await this.electronAPI.getItem(key, this.subFolders)
}

removeItem = async (key: string): Promise<void> => {
throwIfNotElectron()
await this.electronAPI.removeItem(key, this.subFolders)
}

clear = async (): Promise<void> => {
throwIfNotElectron()
await this.electronAPI.clear(this.subFolders)
}

keys = async (): Promise<string[]> => {
throwIfNotElectron()
return await this.electronAPI.keys(this.subFolders)
}
}

/**
* LocalForage storage implementation.
* Uses the localforage library to store and retrieve data in the IndexedDB.
*/
class LocalForageStorage implements StorageDB {
localForage: LocalForage

/**
* Creates a new instance of the LocalForageStorage class.
* @param {string} name - The name of the localforage instance.
* @param {string} storeName - The name of the store to store the data in.
* @param {number} version - The version of the localforage instance.
* @param {string} description - The description of the localforage instance.
*/
constructor(name: string, storeName: string, version: number, description: string) {
this.localForage = localforage.createInstance({
driver: localforage.INDEXEDDB,
name: name,
storeName: storeName,
version: version,
description: description,
})
}

setItem = async (key: string, value: Blob): Promise<void> => {
await this.localForage.setItem(key, value)
}

getItem = async (key: string): Promise<Blob | null | undefined> => {
return await this.localForage.getItem(key)
}

removeItem = async (key: string): Promise<void> => {
await this.localForage.removeItem(key)
}

clear = async (): Promise<void> => {
await this.localForage.clear()
}

keys = async (): Promise<string[]> => {
return await this.localForage.keys()
}
}

const tempVideoChunksIndexdedDB: StorageDB = new LocalForageStorage(
'Cockpit - Temporary Video',
'cockpit-temp-video-db',
1.0,
'Database for storing the chunks of an ongoing recording, to be merged afterwards.'
)

const videoStoringIndexedDB: StorageDB = new LocalForageStorage(
'Cockpit - Video Recovery',
'cockpit-video-recovery-db',
1.0,
'Cockpit video recordings and their corresponding telemetry subtitles.'
)

const electronVideoStorage = new ElectronStorage(['videos'])
const temporaryElectronVideoStorage = new ElectronStorage(['videos', 'temporary-video-chunks'])

export const videoStorage = isElectron() ? electronVideoStorage : videoStoringIndexedDB
export const tempVideoStorage = isElectron() ? temporaryElectronVideoStorage : tempVideoChunksIndexdedDB
Loading

0 comments on commit c1b0dbb

Please sign in to comment.