Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
return await ipcRenderer.invoke('keys', { subFolders })
},
openCockpitFolder: () => ipcRenderer.invoke('open-cockpit-folder'),
getCockpitFolderPath: (): Promise<string> => ipcRenderer.invoke('get-cockpit-folder-path'),
getDefaultCockpitFolderPath: (): Promise<string> => ipcRenderer.invoke('get-default-cockpit-folder-path'),
setCockpitFolderPath: (newPath: string): Promise<void> => ipcRenderer.invoke('set-cockpit-folder-path', newPath),
selectCockpitFolder: (): Promise<string | null> => ipcRenderer.invoke('select-cockpit-folder'),
openVideoFolder: () => ipcRenderer.invoke('open-video-folder'),
openVideoFile: (fileName: string) => ipcRenderer.invoke('open-video-file', fileName),
openVideoChunksFolder: () => ipcRenderer.invoke('open-temp-video-chunks-folder'),
Expand Down
7 changes: 7 additions & 0 deletions src/electron/services/config-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Store from 'electron-store'

const electronStoreSchema = {
cockpitFolderPath: {
type: 'string',
},
windowBounds: {
type: 'object',
properties: {
Expand All @@ -25,6 +28,10 @@ const electronStoreSchema = {
* Stores configuration data
*/
export interface ElectronStoreSchema {
/**
* Custom Cockpit folder path, overriding the default ~/Cockpit
*/
cockpitFolderPath: string | undefined
/**
* Window bounds
*/
Expand Down
37 changes: 34 additions & 3 deletions src/electron/services/storage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { dialog, ipcMain, shell } from 'electron'
import { app } from 'electron'
import { mkdirSync } from 'fs'
import * as fs from 'fs/promises'
import { dirname, join } from 'path'

import type { FileDialogOptions, FileStats } from '@/types/storage'

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

const defaultCockpitFolderPath = join(app.getPath('home'), 'Cockpit')
let cockpitFolderPath = store.get('cockpitFolderPath') ?? defaultCockpitFolderPath
mkdirSync(cockpitFolderPath, { recursive: true })

export const filesystemStorage = {
async setItem(key: string, value: ArrayBuffer, subFolders?: string[]): Promise<void> {
Expand Down Expand Up @@ -88,6 +91,28 @@ export const setupFilesystemStorage = (): void => {
await shell.openPath(tempChunksFolderPath)
})

ipcMain.handle('get-cockpit-folder-path', () => cockpitFolderPath)

ipcMain.handle('get-default-cockpit-folder-path', () => defaultCockpitFolderPath)

ipcMain.handle('set-cockpit-folder-path', async (_, newPath: string) => {
cockpitFolderPath = newPath
await fs.mkdir(cockpitFolderPath, { recursive: true })
store.set('cockpitFolderPath', cockpitFolderPath)
})

ipcMain.handle('select-cockpit-folder', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
title: 'Select Cockpit folder',
defaultPath: cockpitFolderPath,
})
if (result.canceled || result.filePaths.length === 0) {
return null
}
return result.filePaths[0]
})

/**
* Get file stats for a file
* @param pathOrKey - Either a full file path, or a key (filename) if subFolders is provided
Expand Down Expand Up @@ -135,3 +160,9 @@ export const setupFilesystemStorage = (): void => {
return result.filePaths[0]
})
}

/**
* Returns the current Cockpit folder path
* @returns {string} The active Cockpit folder path
*/
export const getCockpitFolderPath = (): string => cockpitFolderPath
10 changes: 5 additions & 5 deletions src/electron/services/video-recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { LiveConcatProcessResult, LiveStreamProcess, ZipExtractionResult }

import { videoFilename, videoThumbnailFilename } from '../../utils/video'
import { getFFmpegPath } from './ffmpeg-path'
import { cockpitFolderPath, filesystemStorage } from './storage'
import { filesystemStorage, getCockpitFolderPath } from './storage'

/**
* Live video streaming service for Electron
Expand Down Expand Up @@ -72,7 +72,7 @@ const startVideoRecording = async (
const tempDir = await createTempDirectory(`cockpit_video_recording_${recordingHash}`)

// Get video folder path and construct output path
const videosPath = join(cockpitFolderPath, 'videos')
const videosPath = join(getCockpitFolderPath(), 'videos')
await fs.mkdir(videosPath, { recursive: true })

// Output directly as MP4 with fragmented format
Expand Down Expand Up @@ -553,7 +553,7 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {
console.debug(`Creating ZIP file for chunk group ${hash}`)

// Find all chunk files for this hash
const tempChunksPath = join(cockpitFolderPath, 'videos', 'temporary-video-chunks')
const tempChunksPath = join(getCockpitFolderPath(), 'videos', 'temporary-video-chunks')

let chunkFiles: string[] = []
try {
Expand Down Expand Up @@ -603,7 +603,7 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {
}

// Generate ZIP filename
const tempChunksFolderPath = join(cockpitFolderPath, 'videos', 'temporary-video-chunks')
const tempChunksFolderPath = join(getCockpitFolderPath(), 'videos', 'temporary-video-chunks')
const zipFilename = `${defaultFileName}.zip`
const zipFilePath = join(tempChunksFolderPath, zipFilename)

Expand Down Expand Up @@ -653,7 +653,7 @@ const createVideoChunksZip = async (hash: string): Promise<string> => {

// Add .ass telemetry file if found - ALWAYS process this regardless of chunk failures
if (assFileName) {
const assFilePath = join(cockpitFolderPath, 'videos', assFileName)
const assFilePath = join(getCockpitFolderPath(), 'videos', assFileName)

fs.access(assFilePath)
.then(() => fs.stat(assFilePath))
Expand Down
21 changes: 21 additions & 0 deletions src/libs/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,27 @@ declare global {
* Open cockpit folder
*/
openCockpitFolder: () => void
/**
* Get the current Cockpit folder path
* @returns {Promise<string>} The current folder path
*/
getCockpitFolderPath: () => Promise<string>
/**
* Get the default Cockpit folder path
* @returns {Promise<string>} The default folder path
*/
getDefaultCockpitFolderPath: () => Promise<string>
/**
* Set a new Cockpit folder path
* @param {string} newPath - The new folder path
* @returns {Promise<void>}
*/
setCockpitFolderPath: (newPath: string) => Promise<void>
/**
* Open a native folder picker to select a Cockpit folder
* @returns {Promise<string | null>} The selected path, or null if cancelled
*/
selectCockpitFolder: () => Promise<string | null>
/**
* Open video folder
*/
Expand Down
126 changes: 114 additions & 12 deletions src/views/ConfigurationGeneralView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,6 @@
<v-btn size="x-small" class="bg-[#FFFFFF22] shadow-1" variant="flat" @click="openTutorial">
Show tutorial
</v-btn>
<v-btn
v-if="isElectron()"
size="x-small"
class="bg-[#FFFFFF22] shadow-1"
variant="flat"
@click="openCockpitFolder"
>
Open Cockpit folder
</v-btn>
<v-btn
size="x-small"
class="bg-[#FFFFFF22] shadow-1"
Expand All @@ -70,6 +61,51 @@
{{ interfaceStore.pirateMode ? 'Disable pirate mode' : 'Enable pirate mode' }}
</v-btn>
</div>
<v-divider v-if="isElectron()" class="w-full opacity-[0.08]" />
<div v-if="isElectron()" class="flex flex-col w-full py-4 gap-1">
<span class="text-md mb-1 text-slate-200">Cockpit folder location:</span>
<div class="flex items-center gap-6">
<v-tooltip
:text="cockpitFolderPath"
:disabled="cockpitFolderPath !== defaultCockpitFolderPath"
location="bottom"
open-delay="300"
>
<template #activator="{ props: tooltipProps }">
<v-text-field
v-bind="tooltipProps"
:model-value="cockpitFolderPath"
variant="filled"
density="compact"
hide-details
readonly
class="cursor-pointer"
@click="browseCockpitFolder"
>
<template #append-inner>
<v-icon
v-if="cockpitFolderPath !== defaultCockpitFolderPath"
v-tooltip.bottom="'Reset to default folder location'"
color="white"
@click.stop="resetCockpitFolderPath"
>
mdi-restore
</v-icon>
</template>
</v-text-field>
</template>
</v-tooltip>
<v-btn
size="small"
append-icon="mdi-folder-open-outline"
class="bg-[#FFFFFF22] shadow-2"
variant="flat"
@click="openCockpitFolder"
>
Open folder
</v-btn>
</div>
</div>
</div>
</template>
</ExpansiblePanel>
Expand Down Expand Up @@ -117,7 +153,7 @@
>
<template #append-inner>
<v-icon v-tooltip.bottom="'Reset global address'" color="white" @click="resetGlobalAddress">
mdi-refresh
mdi-restore
</v-icon>
</template>
</v-text-field>
Expand Down Expand Up @@ -173,7 +209,7 @@
:disabled="!mainVehicleStore.customMAVLink2RestWebsocketURI.enabled"
@click="resetMainVehicleConnectionURI"
>
mdi-refresh
mdi-restore
</v-icon>
</template>
</v-text-field>
Expand Down Expand Up @@ -239,7 +275,7 @@
:disabled="!mainVehicleStore.customWebRTCSignallingURI.enabled"
@click="resetWebRTCSignallingURI"
>
mdi-refresh
mdi-restore
</v-icon>
</template>
</v-text-field>
Expand Down Expand Up @@ -392,6 +428,7 @@ import { defaultGlobalAddress } from '@/assets/defaults'
import ManageCockpitSettings from '@/components/configuration/CockpitSettingsManager.vue'
import ExpansiblePanel from '@/components/ExpansiblePanel.vue'
import VehicleDiscoveryDialog from '@/components/VehicleDiscoveryDialog.vue'
import { useInteractionDialog } from '@/composables/interactionDialog'
import { useSnackbar } from '@/composables/snackbar'
import * as Connection from '@/libs/connection/connection'
import { ConnectionManager } from '@/libs/connection/connection-manager'
Expand All @@ -416,12 +453,76 @@ const mainVehicleStore = useMainVehicleStore()
const interfaceStore = useAppInterfaceStore()
const missionStore = useMissionStore()
const { openSnackbar } = useSnackbar()
const { showDialog, closeDialog } = useInteractionDialog()

const globalAddressForm = ref()
const globalAddressFormValid = ref(false)
const newGlobalAddress = ref(mainVehicleStore.globalAddress)
const showCockpitSettingsDialog = ref(false)

const cockpitFolderPath = ref('')
const defaultCockpitFolderPath = ref('')

const loadCockpitFolderPath = async (): Promise<void> => {
if (!isElectron() || !window.electronAPI) return
cockpitFolderPath.value = await window.electronAPI.getCockpitFolderPath()
defaultCockpitFolderPath.value = await window.electronAPI.getDefaultCockpitFolderPath()
}

const applyFolderPath = async (path: string): Promise<void> => {
if (!window.electronAPI) return
await window.electronAPI.setCockpitFolderPath(path)
cockpitFolderPath.value = path
}

const browseCockpitFolder = async (): Promise<void> => {
if (!window.electronAPI) return
const selected = await window.electronAPI.selectCockpitFolder()
if (!selected) return

const folderName = selected.split(/[/\\]/).filter(Boolean).pop()
if (folderName === 'Cockpit') {
await applyFolderPath(selected)
return
}

const selectedName = selected.split(/[/\\]/).filter(Boolean).pop() ?? ''
showDialog({
title: 'Cockpit folder location',
message:
`The selected folder is not named "Cockpit". Would you like to use ` +
`${selected} directly, or create and use a Cockpit subfolder inside it?`,
variant: 'info',
persistent: true,
maxWidth: 700,
actions: [
{ text: 'Cancel', size: 'small', action: () => closeDialog() },
{
text: `Use ${selectedName}`,
size: 'small',
action: () => {
closeDialog()
applyFolderPath(selected)
},
},
{
text: `Use ${selectedName}/Cockpit`,
size: 'small',
action: () => {
closeDialog()
applyFolderPath(`${selected}/Cockpit`)
},
},
],
})
}

const resetCockpitFolderPath = async (): Promise<void> => {
if (!window.electronAPI) return
await window.electronAPI.setCockpitFolderPath(defaultCockpitFolderPath.value)
cockpitFolderPath.value = defaultCockpitFolderPath.value
}

const setGlobalAddress = async (): Promise<void> => {
await globalAddressForm.value.validate()

Expand Down Expand Up @@ -643,6 +744,7 @@ const newGenericWebSocketUrl = ref(exampleGenericWebSocketUrl)
let unsubscribeGenericWebSocket: (() => void) | null = null

onMounted(() => {
loadCockpitFolderPath()
tryToPrettifyRtcConfig()
unsubscribeGenericWebSocket = listenToGenericWebSocketConnections((connections) => {
genericWebSocketConnections.value = connections
Expand Down
2 changes: 1 addition & 1 deletion src/views/ConfigurationLogsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@
<div class="flex justify-end w-full mt-2">
<v-btn size="x-small" variant="text" class="mr-2" @click="resetAllChips">
Reset Positions
<v-icon size="18" class="ml-2">mdi-refresh</v-icon>
<v-icon size="18" class="ml-2">mdi-restore</v-icon>
</v-btn>
</div>
</div>
Expand Down
Loading