Skip to content

Vue UI Guide — OS

OS plugins run as standalone Electron windows, which changes how you structure layouts, handle navigation, and interact with the host system. This guide covers the OS-specific patterns for building your Vue pages.

Window Layout

Because your plugin occupies its own full Electron window, the root <template> of every page should fill the entire available space. Use h-full on the outermost wrapper:

vue
<template>
  <div class="h-full p-3">
    <!-- page content -->
  </div>
</template>

The m-2 wrapper is typical for content sections inside a page:

vue
<template>
  <div class="h-full">
    <div class="m-2">
      <DataTable :value="items" />
    </div>
  </div>
</template>

Unlike EA/SA plugins, you do not need appendTo="#containerElement" on dialogs or overlays — your plugin owns the whole window.

Toolbar and Menu (configured in script.js)

Top-level navigation is declared in script.js via setToolbar() (icon buttons) and setMenu() (labeled menu items). When users click a toolbar button or menu item, your Vue Router handles the transition.

Back Button

For sub-pages (details, forms), use a back button that pushes to the parent route:

vue
<template>
  <div class="h-full p-3">
    <div class="flex align-items-center gap-2 mb-3">
      <Button
        icon="pi pi-arrow-left"
        text
        @click="$router.push('/')"
      />
      <h2 class="m-0">Track Detail</h2>
    </div>
    <!-- content -->
  </div>
</template>

Root-Relative Routes

OS routes are root-relative. Always use / prefixed paths in $router.push():

js
// Navigate to settings
this.$router.push('/setting')

// Navigate to a detail page with a param
this.$router.push(`/detail/${track.id}`)

// Go back to root/home
this.$router.push('/')

Data — Local Database

Use the api module imported at the top of your <script> block to read and write local JSON data. No axios, no remote server needed:

vue
<script>
import api from 'api'

export default {
  data() {
    return {
      tracks: []
    }
  },
  async mounted() {
    const db = api.database('music')
    this.tracks = await db.readJson(['data.json']) || []
  },
  methods: {
    async save() {
      const db = api.database('music')
      await db.writeJson(['data.json'], this.tracks)
    }
  }
}
</script>

api.database(name) creates or accesses a named local JSON store scoped to your plugin.

Native File Dialogs

OS plugins can open native system dialogs using ipcRenderer from Electron. This lets users pick files or folders from the OS file system:

vue
<script>
import { ipcRenderer } from 'electron'

export default {
  methods: {
    async pickFile() {
      const result = await ipcRenderer.invoke('openDialog', {
        properties: ['openFile'],
        filters: [
          { name: 'Audio Files', extensions: ['mp3', 'wav', 'flac', 'm4a'] }
        ]
      })
      if (!result.canceled && result.filePaths.length) {
        this.selectedFile = result.filePaths[0]
      }
    },

    async pickFolder() {
      const result = await ipcRenderer.invoke('openDialog', {
        properties: ['openDirectory']
      })
      if (!result.canceled && result.filePaths.length) {
        this.folder = result.filePaths[0]
      }
    }
  }
}
</script>

This is an OS-exclusive feature — SA and EA plugins do not have access to ipcRenderer.

Forms and Input

Use PrimeVue components and PrimeFlex utilities for forms. The same patterns from EA/SA apply:

vue
<template>
  <div class="h-full p-3">
    <!-- Page header with back button -->
    <div class="flex align-items-center gap-2 mb-4">
      <Button icon="pi pi-arrow-left" text @click="$router.push('/')" />
      <h2 class="m-0">Add Track</h2>
    </div>

    <!-- Form -->
    <div class="formgrid grid">
      <div class="field col-12 md:col-6">
        <label for="title">Title</label>
        <InputText id="title" v-model="form.title" class="w-full" />
      </div>
      <div class="field col-12 md:col-6">
        <label for="artist">Artist</label>
        <InputText id="artist" v-model="form.artist" class="w-full" />
      </div>
      <div class="field col-12">
        <label>File</label>
        <div class="flex gap-2">
          <InputText v-model="form.path" class="w-full" readonly />
          <Button icon="pi pi-folder-open" @click="pickFile" />
        </div>
      </div>
    </div>

    <Button label="Save" icon="pi pi-check" @click="save" />
  </div>
</template>

Data Tables

Use DataTable from PrimeVue wrapped in m-2 for list views:

vue
<template>
  <div class="h-full">
    <div class="m-2">
      <DataTable :value="tracks" paginator :rows="20" stripedRows>
        <Column field="title"  header="Title"  />
        <Column field="artist" header="Artist" />
        <Column header="Actions">
          <template #body="{ data }">
            <Button
              icon="pi pi-pencil"
              text
              @click="$router.push(`/detail/${data.id}`)"
            />
            <Button
              icon="pi pi-trash"
              text
              severity="danger"
              @click="remove(data)"
            />
          </template>
        </Column>
      </DataTable>
    </div>
  </div>
</template>

Toast / Feedback

Use the useToast composable (or $toast in Options API) for feedback:

vue
<script>
import { useToast } from 'primevue/usetoast'

export default {
  setup() {
    const toast = useToast()
    return { toast }
  },
  methods: {
    async save() {
      try {
        await api.database('music').writeJson(['data.json'], this.tracks)
        this.toast.add({ severity: 'success', summary: 'Saved', life: 3000 })
      } catch {
        this.toast.add({ severity: 'error', summary: 'Save failed', life: 3000 })
      }
    }
  }
}
</script>

<template>
  <Toast />
  <!-- rest of template -->
</template>

Settings Page Pattern

The setting.vue page follows a consistent pattern — a back button at the top and a form below:

vue
<template>
  <div class="h-full p-3">
    <div class="flex align-items-center gap-2 mb-4">
      <Button icon="pi pi-arrow-left" text @click="$router.push('/')" />
      <h2 class="m-0">Settings</h2>
    </div>

    <div class="formgrid grid">
      <div class="field col-12">
        <label for="musicFolder">Music Folder</label>
        <div class="flex gap-2">
          <InputText id="musicFolder" v-model="settings.folder" class="w-full" readonly />
          <Button icon="pi pi-folder-open" @click="pickFolder" />
        </div>
      </div>
    </div>

    <Button label="Save Settings" icon="pi pi-save" @click="saveSettings" />
  </div>
</template>

i18n

Access translations with $t() using the namespace you defined in addLanguage():

vue
<template>
  <div class="h-full p-3">
    <h1>{{ $t('music.label') }}</h1>
    <Button :label="$t('music.tracks')" />
  </div>
</template>

Key Differences from SA/EA

PatternOSSA / EA
Root layouth-full full-windowEmbedded container
Back button$router.push('/')Not typically needed
Route pathsRoot-relative (/setting)Relative (setting)
File dialogsipcRenderer.invoke('openDialog', ...)Not available
Dialog appendToNot neededappendTo="#containerElement"
Content wrapperm-2Component-specific
Main viewindex.vueindex.vue