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